Webhooks
Subscribe to connection, action and approval events, verify the signature, and handle retries and failing endpoints.
Webhooks push events to your endpoint instead of you polling the audit log for changes. Add one from the Webhooks page.

Event types
| Event | Fires when |
|---|---|
connection.created | A user completes a connection |
connection.expired | A connection's refresh token dies and it needs reconnecting |
connection.revoked | A connection is revoked, by the user or from the dashboard |
action.blocked | A policy denies a call outright |
action.failed | An upstream call errors |
approval.requested | An ask rule holds a call |
approval.decided | An approval is approved, rejected or expires |
user.created | A new connected user is created |
Subscribe to approval.requested and approval.decided if your app needs to act on
approvals from code rather than waiting on the approver's email; see
Approvals.
Payload
Every delivery carries the same envelope, with the event-specific fields under data:
{
"id": "wev_01k61z9m4c5f0rce9dxyt3h7qb",
"type": "connection.expired",
"created": "2026-09-27T14:02:11Z",
"project": "prj_desk",
"environment": "production",
"data": {
"id": "conn_z663h86fl3z2",
"app": "salesforce",
"user": "u_8f2",
"status": "expired",
"status_reason": "invalid_grant: token has been expired or revoked"
}
}Verifying the signature
Every request carries a signature over the raw body. Verify it before trusting the payload — an unverified endpoint is a public write API to anyone who finds the URL.
Arc0-Signature: t=1758981600,v1=5257a869e7...t is a Unix timestamp and v1 is the hex HMAC-SHA256 of {t}.{body}, computed with your
endpoint's signing secret (whsec_••••••••{last4}, shown under Reveal).
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(rawBody: string, header: string, secret: string): boolean {
const [tPart, vPart] = header.split(',');
const timestamp = tPart.split('=')[1];
const signature = vPart.split('=')[1];
const expected = createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const fresh = Date.now() / 1000 - Number(timestamp) < 300; // 5 minutes
return (
fresh &&
timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
);
}Verify against the raw request body, before any JSON parsing. Re-serializing the body changes whitespace and key order, and the signature will never match.
Retries and failing endpoints
A delivery that doesn't return a 2xx is retried up to 5 attempts, backing off: 0, 1
minute, 10 minutes, 1 hour, then 6 hours. The delivery's Next retry column shows a time,
Queued, or Gave up once attempts run out.
An endpoint that keeps failing is marked Failing, showing the status since it started — "HTTP 500 since ..." — and stops receiving new deliveries until you fix it. Left long enough, it moves to Disabled.

Return your 2xx quickly and do the real work after — acknowledge first. Retries mean a
delivery can arrive more than once, so key your handling on id, not on delivery count.
Delivery detail
Every attempt is logged, with the request body, headers, response and duration:

Test events
Use Send test event on any endpoint to fire a sample payload without waiting for a real connection or call to trigger one — useful for checking your signature verification before you go live.


