Webhooks
Webhooks let your application react to events as they happen instead of polling. You subscribe a URL
to a set of event types on a network; AdBridge then POSTs a signed JSON envelope to that URL
whenever a matching event occurs.
Manage subscriptions under /v1/networks/{networkId}/webhooks
(administrator role required).
Event types
| Event | Fires when |
|---|---|
offer.accepted | A campaign offer is accepted. |
offer.rejected | A campaign offer is rejected. |
optin.completed | A podcast completes opt-in. |
invitation.accepted | A network invitation is accepted. |
creative.ready | A host-voiced creative finished generating and its audio is ready. |
creative.failed | A creative failed to generate. |
The creative.ready / creative.failed events are how you learn the outcome of the asynchronous
creative generation kicked off by accepting an offer.
Creating a subscription
- cURL
- Python
- Node.js
- Go
- Java
curl -X POST https://api.adbridge.ai/v1/networks/7Hb2Kp9QvL3mNx0R8tZ4/webhooks \
-H "Authorization: Bearer $ADBRIDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/adbridge",
"events": ["offer.accepted", "creative.ready"]
}'
import os
import requests
resp = requests.post(
"https://api.adbridge.ai/v1/networks/7Hb2Kp9QvL3mNx0R8tZ4/webhooks",
headers={"Authorization": f"Bearer {os.environ['ADBRIDGE_API_KEY']}"},
json={
"url": "https://example.com/webhooks/adbridge",
"events": ["offer.accepted", "creative.ready"],
},
)
resp.raise_for_status()
secret = resp.json()["secret"] # store this — shown only once
const resp = await fetch(
"https://api.adbridge.ai/v1/networks/7Hb2Kp9QvL3mNx0R8tZ4/webhooks",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ADBRIDGE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://example.com/webhooks/adbridge",
events: ["offer.accepted", "creative.ready"],
}),
},
);
const { secret } = await resp.json(); // store this — shown only once
body := strings.NewReader(`{
"url": "https://example.com/webhooks/adbridge",
"events": ["offer.accepted", "creative.ready"]
}`)
req, _ := http.NewRequest(http.MethodPost,
"https://api.adbridge.ai/v1/networks/7Hb2Kp9QvL3mNx0R8tZ4/webhooks", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("ADBRIDGE_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
// Parse resp.Body and store the "secret" field — shown only once.
String body = """
{"url":"https://example.com/webhooks/adbridge","events":["offer.accepted","creative.ready"]}""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.adbridge.ai/v1/networks/7Hb2Kp9QvL3mNx0R8tZ4/webhooks"))
.header("Authorization", "Bearer " + System.getenv("ADBRIDGE_API_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
// Parse response.body() and store the "secret" field — shown only once.
The response includes a secret (e.g. whsec_…) once — store it; it is never returned again and
is used to verify deliveries.
Delivery format
Each delivery is a POST of a JSON envelope:
{
"id": "b1d2c3e4-...",
"type": "offer.accepted",
"occurred_at": "2026-01-15T09:30:00Z",
"data": { "offer_id": "7Hb2Kp9QvL3mNx0R8tZ4", "campaign_id": "..." }
}
data contains only curated, client-facing fields — no vendor or storage internals. Headers:
| Header | Meaning |
|---|---|
X-AdBridge-Event | The event type wire string (e.g. offer.accepted). |
X-AdBridge-Delivery | A unique delivery id (UUID), equal to the envelope id. |
X-AdBridge-Signature | Lowercase-hex HMAC-SHA256 of the raw request body, keyed by the subscription secret. |
Verifying the signature
Recompute the HMAC over the exact raw body and compare it to X-AdBridge-Signature using a
constant-time comparison. Reject the delivery if it does not match.
- Shell (openssl)
- Python
- Node.js
- Go
- Java
# Given the raw body in body.json and the secret in $WEBHOOK_SECRET:
expected=$(openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" body.json | awk '{print $2}')
# Compare $expected to the X-AdBridge-Signature header value.
echo "$expected"
import hashlib
import hmac
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header)
const crypto = require("node:crypto");
function verify(rawBody, signatureHeader, secret) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
func verify(rawBody []byte, signatureHeader, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signatureHeader))
}
static boolean verify(byte[] rawBody, String signatureHeader, String secret) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] digest = mac.doFinal(rawBody);
StringBuilder hex = new StringBuilder();
for (byte b : digest) hex.append(String.format("%02x", b));
return MessageDigest.isEqual(
hex.toString().getBytes(StandardCharsets.UTF_8),
signatureHeader.getBytes(StandardCharsets.UTF_8));
}
Compute the HMAC over the unparsed request body. Re-serializing the JSON first can change bytes (key order, whitespace) and break verification.
Delivery semantics
- Best-effort, at-least-once. A delivery is retried a few times on connection errors or
5xxresponses. Respond2xxquickly to acknowledge. - Be idempotent. Use
X-AdBridge-Deliveryto de-duplicate; the same event may arrive more than once. - No durable queue today. A process crash mid-delivery may drop an event, so treat webhooks as a fast-path notification and reconcile critical state by reading the API when needed.
Pause deliveries without losing the subscription by setting its status to disabled via
PATCH /v1/networks/{networkId}/webhooks/{id}.