1. Register an endpoint
POST /api/webhooks takes a name, your target_url and the events[] you want:
nameis required (1 to 255 characters). Omitting it fails validation.target_urlmust behttps://and must not resolve to a loopback, private or link-local address. DNS names are resolved and checked too, not just IP literals.- Each
events[]value is validated against the platform event catalog, which currently carries 94 types across the services. - The same
target_urlcannot be registered twice in one team. events: ["*"]subscribes to the entire catalog, including event types added later.
secret, a 32-character hex string, exactly once. Store it: every later read masks it, and it is what you verify signatures with.
Webhooks are capped per plan. Creating one past the cap answers
402 with
webhook_limit_reached and a body carrying used, limit and a suggested action. The
free Sandbox plan’s cap is 0, so webhooks need a paid plan. See
billing and plans.Filter what you receive
Two optional filters narrow deliveries at the source, so your endpoint is not woken by events it would discard anyway:filters.account_sidrestricts deliveries to one account (ln_ac_...for LinkedIn,em_ac_...for email). The sid is matched verbatim, so channels added later work here without a change.filters.wheretakes a small filter expression over the event payload. It is validated strictly at write time: nesting depth is capped at 5 and the whole expression at 20 leaf conditions, answeringfilter_grammar_invalidorfilter_leaves_limit_exceededwhen it exceeds either.
2. Verify deliveries
Every real delivery is an HTTPSPOST with a JSON body and these headers:
The body is a fixed envelope, with the event’s own data nested under
payload:
HMAC-SHA256(secret, "{t}.{raw_body}") over the raw request body, with the timestamp mixed in to block replays. Verify before parsing, and reject signatures older than about 5 minutes:
3. Test before relying on it
POST /api/webhooks/{sid}/test fires a synthetic delivery at your endpoint, so you can confirm reachability, signature handling and parsing without waiting for a real event.
Three things to know about it: it is rate-limited to 10 calls per minute per caller (the budget is shared across all your webhooks), it is rejected with invalid_transition while the webhook’s status is off, and it writes no row to the delivery log. It also sets a subset of the headers above, so build your verifier on the signature, timestamp, event and id headers rather than requiring all six.
4. Retries and auto-disable
A delivery is attempted up to 5 times, with waits of 1 minute, 5 minutes, 30 minutes and 2 hours between attempts. The fifth failure is terminal, so a dead endpoint settles about 2.5 hours after the first attempt.
After 20 consecutive failed attempts, or a single
410 Gone, the subscription flips to failed and stops delivering. That transition itself emits webhooks.failed, so a second webhook can page you when the first one dies.
5. Use the delivery log
The log holds one row per event per subscription, not one per attempt: a retry updates that row in place, bumpingretry_count and overwriting the response code. The row is the delivery, and its history is the counter.
POST /api/webhook-logs/searchlists deliveries with status, response code and timing; filter by webhook or event type.POST /api/webhook-logs/{sid}/retryre-sends. Allowed frompending,retrying,failedandsuccess(a manual re-send of a delivered event is legitimate), and only while the parent subscription is still live: a deleted oroffwebhook rejects the retry.POST /api/webhook-logs/{sid}/cancelstops a delivery that has not settled. Allowed frompending,retryingandin_progress.POST /api/webhook-logs/metricsaggregates outcomes.periodwithfromandtois required and may span at most 90 days.
retry and cancel answer 409 with invalid_transition when the row is not in a state that allows the verb, and the error names the state it found.