Webhooks
Register a webhook for your brand, verify the HMAC-SHA256 signature, and handle the three events.
Webhooks report the outcome of engagement verification attempts, which is available on request; data lookups need no webhook. They are registered per brand. Each delivery is a POST with a JSON body signed with HMAC-SHA256 using the secret returned when you created the webhook.
Register a webhook
/admin/brands/{brandId}/webhookscurl -X POST {API_BASE_URL}/admin/brands/1/webhooks \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/engagement",
"events": ["creator.verified", "reward.issued", "verification.failed"],
"description": "Production webhook"
}'{
"data": {
"id": 1,
"url": "https://example.com/webhooks/engagement",
"events": ["creator.verified", "reward.issued", "verification.failed"],
"active": true,
"description": "Production webhook",
"lastTriggeredAt": null,
"createdAt": "2026-06-01T09:00:00.000Z",
"secret": "whsec_a1b2c3…"
}
}The secret is shown once
secret is returned only in this 201 response. Store it now. GET, PATCH, and DELETE on /admin/brands/{brandId}/webhooks/{webhookId} never return it; if it is lost, delete the webhook and create a new one.
events must contain at least one of the three event names. PATCH accepts url, events, description, and active; set active: false to pause deliveries without deleting.
Delivery
Every delivery carries these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
X-Vlayer-Event | The event name, for example creator.verified |
X-Vlayer-Signature | Base64 HMAC-SHA256 of the raw request body, keyed with your whsec_* secret |
Deprecated headers
Deliveries also carry X-TYB-Event and X-TYB-Signature, with the same values as the X-Vlayer- headers. They will be removed no earlier than December 31, 2026, so read the X-Vlayer- headers in new receivers and switch existing ones before then.
Body:
{
"event": "reward.issued",
"timestamp": "2026-06-02T15:05:02.000Z",
"data": {
"challengeId": 1,
"userId": 42,
"externalUserId": "cust_abc123",
"username": "creator_jane",
"platform": "instagram",
"campaignTitle": "Summer Challenge 2026",
"attemptId": 39,
"points": 100
}
}Respond with any 2xx within 10 seconds. Other responses and timeouts are retried up to five times with exponential backoff; after that you can replay with the redeliver route.
Verify the signature
Compute the HMAC over the raw body bytes, before JSON parsing, and compare with a constant-time function.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifySignature(rawBody: Buffer, header: string | undefined, secret: string): boolean {
if (!header) return false;
const expected = createHmac("sha256", secret).update(rawBody).digest("base64");
const received = Buffer.from(header);
const wanted = Buffer.from(expected);
return received.length === wanted.length && timingSafeEqual(received, wanted);
}// Express: keep the raw body for the signature check
app.post("/webhooks/engagement", express.raw({ type: "application/json" }), (req, res) => {
if (!verifySignature(req.body, req.header("X-Vlayer-Signature"), process.env.ENGAGEMENT_WEBHOOK_SECRET!)) {
return res.status(401).end();
}
const { event, data } = JSON.parse(req.body.toString("utf8"));
// handle event …
res.status(204).end();
});Events
| Event | When | Extra fields in data |
|---|---|---|
creator.verified | A comment or like matched, or an admin force-verified the attempt | commentText (comment challenges) |
reward.issued | Points were issued for a verified attempt | points |
verification.failed | Verification did not find a match | reason: retryable (the creator can claim again) or terminal (no further retries) |
Fields present on every event:
| Field | Type | Notes |
|---|---|---|
challengeId | integer | |
userId | integer | Internal creator id |
externalUserId | string | Your externalId for the creator. Omitted when not set |
username | string | The creator's linked handle |
platform | instagram or tiktok | |
campaignTitle | string | |
attemptId | integer |
Deliveries can arrive more than once. Key your handling on attemptId + event so a replay is a no-op.
Retries are per event, not per endpoint. If your brand has more than one active webhook subscribed to the same event and any one of them fails, the whole delivery is retried and every subscribed endpoint receives the event again, including those that already returned 2xx.
For Vouch web-proof webhooks, which use a different signature scheme, see Verifying Vouch webhooks (opens in a new tab).
Last updated on