Webhooks
Webhooks let your server react to platform events (lead created, sequence sent, agent run completed) without polling. Subscribe under Settings → Developers → Webhooks in the product, or via the flows API by setting ExternalTrigger.target_type: "webhook".
How to verify a webhook signature
Every delivery includes X-10ex-Signature: sha256=<hex>. The hex is HMAC-SHA256 of the raw request body using the workspace’s webhook secret. Verify before trusting the payload.
import { createHmac, timingSafeEqual } from 'node:crypto'
function verify(rawBody: string, header: string, secret: string) {
const expected = `sha256=${createHmac('sha256', secret).update(rawBody).digest('hex')}`
return timingSafeEqual(Buffer.from(expected), Buffer.from(header))
}A few things that catch people:
- Verify against the raw body, not a re-serialized JSON object. JSON whitespace differences will break the HMAC.
- Use a constant-time comparison (
timingSafeEqual) to avoid timing-attack side channels. - The header is exactly
X-10ex-Signature, notX-Tenex-Signature.
Retries
Non-2xx responses retry up to 5 times with exponential backoff: 1s, 2s, 4s, 8s, 16s. After the final failure, the delivery is marked failed and surfaced in the workspace’s webhook delivery log.
Return a 2xx fast (under 5 seconds) and process the payload asynchronously. Anything slower risks a timeout retry that creates duplicate work on your side.
Common mistakes
- Returning 200 only after expensive downstream work finishes. If that work takes 30 seconds, we’ll retry while it’s still running.
- Skipping signature verification in dev. Build it in from day one so you don’t ship an unauthenticated endpoint by accident.
- Forgetting that retries are at-least-once. Make your handler idempotent on the event id.
For event types and payload shapes, see Reference → Webhooks.