Webhooks
Get lead and agent events pushed to your own systems the moment they happen.
Webhooks deliver events to your systems in real time, one signed request per event. Use them to react immediately: push a delivered lead into a workflow, sync a status change, or trigger automation in n8n, Zapier, or your own backend.
This is the machine surface. For people, use the notification channels.
Events
| Event | Fires when |
|---|---|
lead.delivered | A new, verified lead is saved |
lead.status_changed | A lead changes status, for example pushed to a CRM |
agent.run.completed | An agent run finishes successfully |
agent.run.failed | An agent run fails before completing |
Set up an endpoint
Add the endpoint
In your workspace settings, add your endpoint URL and select the events it should receive. You can scope an endpoint to a single brand, or leave it across all brands.
Save the signing secret
The secret is shown once. Store it now. You use it to verify every request. Rotating it invalidates the old one immediately.
Optionally filter lead events
For lead.delivered and lead.status_changed you can require a minimum warmth score, limit to certain strategies, or limit to certain statuses. Agent run events always fire.
Payload
Every event is wrapped in the same envelope. data differs by event. This is a lead.delivered payload:
{
"id": "0190f3...",
"event": "lead.delivered",
"createdAt": "2026-05-15T10:30:00.000Z",
"workspaceId": "0190ab...",
"data": {
"lead": {
"id": "0190...",
"productId": "0190...",
"name": "Jane Prospect",
"title": "VP of Sales",
"company": "Acme Inc",
"headline": "VP of Sales at Acme Inc",
"location": "San Francisco, CA",
"linkedinUrl": "https://www.linkedin.com/in/jane-prospect",
"email": null,
"warmthScore": 82,
"reasonLine": "Acme closed a Series B last week",
"status": "new",
"strategyType": "funding_signals",
"agentId": "0190...",
"createdAt": "2026-05-15T10:30:00.000Z"
},
"agent": { "id": "0190...", "name": "ICP Agent" },
"sourceRunId": "0190..."
}
}name, title, company, headline, location, email, warmthScore, reasonLine, and agentId can be null. agent is null for leads not tied to a single agent.
data is different for the other events:
lead.status_changed:{ lead, fromStatus, toStatus }agent.run.completed:{ sourceRunId, agentId, agentName, strategyType, leadsFetched, leadsDelivered, costUsd }agent.run.failed:{ sourceRunId, agentId, agentName, strategyType, errorMessage }
Headers
| Header | Value |
|---|---|
X-CatchIntent-Signature | t=<unix-seconds>,v1=<hex hmac-sha256> |
X-CatchIntent-Event | The event type |
X-CatchIntent-Event-Id | Stable id for the event. Use it as an idempotency key |
Verify the signature
Recompute the HMAC over timestamp.rawBody with your signing secret and compare in constant time. Reject requests with an old timestamp.
const crypto = require('node:crypto');
function verify(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}Capture the raw request body before parsing JSON. Re-serializing changes the bytes and breaks the signature.
Delivery and retries
A 2xx marks the delivery successful. Timeouts, 429, and 5xx are retried with exponential backoff up to six attempts. Other 4xx responses fail immediately. Delivery is at least once, so make your endpoint idempotent by deduping on X-CatchIntent-Event-Id. Every attempt is logged and can be replayed from the dashboard.
Always verify the signature and dedupe on the event id before acting. Treat an unverified request as hostile.