Webhooks
Register a webhook for your brand, verify the HMAC-SHA256 signature, and handle the three events.
Webhooks 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
POST /admin/brands/{brandId}/webhooks
curl -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 |
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; after retries are exhausted 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.
For Vouch web-proof webhooks, which use a different signature scheme, see Verifying Vouch webhooks.