vlayer logovlayer

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:

HeaderValue
Content-Typeapplication/json
X-Vlayer-EventThe event name, for example creator.verified
X-Vlayer-SignatureBase64 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

EventWhenExtra fields in data
creator.verifiedA comment or like matched, or an admin force-verified the attemptcommentText (comment challenges)
reward.issuedPoints were issued for a verified attemptpoints
verification.failedVerification did not find a matchreason: retryable (the creator can claim again) or terminal (no further retries)

Fields present on every event:

FieldTypeNotes
challengeIdinteger
userIdintegerInternal creator id
externalUserIdstringYour externalId for the creator. Omitted when not set
usernamestringThe creator's linked handle
platforminstagram or tiktok
campaignTitlestring
attemptIdinteger

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.