REST API
Webhooks
Events we send you and how to verify their signature.
Webhooks send a signed JSON event to your server when something happens: a click, an install, a link
change, a domain verification. Create an endpoint in the dashboard (Webhooks) or with
POST /v1/webhooks, choose the events, and keep the signing secret it shows once.
Events#
| Event | When |
|---|---|
install.attributed, install.organic | A first open was matched to a click, or was not |
open.reengaged | An existing install opened your app through a link |
event.tracked | An event from the SDK or your server was stored |
link.created, link.updated, link.disabled, link.blocked | A link changed |
campaign.created, campaign.ended | A campaign started or ended |
domain.verified, domain.failed | A domain check finished |
click.created | Every click. High volume: never preselected, choose it deliberately |
The request#
POST to your https URL, with Content-Type: application/json:
{
"id": "01JA2B3C4D5E6F7G8H9J0K1M2N",
"type": "install.attributed",
"created_at": "2026-10-10T08:15:00Z",
"project_id": "01J9ZZ…",
"environment": "live",
"data": {}
}| Header | Meaning |
|---|---|
X-BeckLink-Signature | t=<unix seconds>,v1=<hex> (see below) |
X-BeckLink-Event | The event type |
X-BeckLink-Delivery | The id of this delivery attempt |
User-Agent | BeckLink-Webhooks/1 |
The event id is stable: deduplicate on it, because an event can be delivered more than once
(retries, replays).
Verify the signature#
Take the raw body exactly as received (before any JSON parsing), the timestamp t and every v1
value from the header. The signed text is t, a dot, and the body. Compute
HMAC-SHA256(secret, "<t>.<body>") as lower-case hex, with the signing secret (whsec_…, used as is) as the
key, and compare it to the v1 values with a constant-time comparison. We recommend rejecting a
timestamp more than five minutes away from your clock, so an old request cannot be replayed (the
examples do).
During a secret rotation (24 hours) the header holds one v1 per secret; accept the request if
any matches.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody, header, secret, toleranceSeconds = 300) {
let t = NaN;
const signatures = [];
for (const part of header.split(',')) {
const [key, value] = part.split('=');
if (key === 't') t = Number(value);
else if (key === 'v1') signatures.push(value);
}
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac('sha256', secret).update(t + '.' + rawBody).digest();
return signatures.some((hex) => {
const given = Buffer.from(hex, 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
});
}Answering and retrying#
Answer with any 2xx within 10 seconds; do the work after you have answered. Redirects are not
followed. Anything else is a failure and is retried: up to 8 attempts, waiting about 1 minute, 5 minutes,
30 minutes, 2 hours, 6 hours, 12 hours and 24 hours between them (with a little jitter).
- An endpoint that fails with no success for 3 days is turned off (
auto_disabled); enable it again once it works. - The delivery log keeps the first 2 KB of each request and response. Replay a delivery from the
dashboard or with
POST /v1/webhooks/{webhook}/deliveries/{delivery}/replay(a replay is one attempt without automatic retries). - Your URL must use a public host name over
https: IP addresses, local and internal names are refused.
Verify it works#
Use Send test event in the dashboard (POST /v1/webhooks/{webhook}/test) and check that your
endpoint verifies the signature and answers 200. Then break it on purpose (change one byte of the
body) and check that it is rejected. See the Webhooks reference.