Skip to main content
An outbound webhook tells your systems that something happened in Leadey — a lead’s status changed, an opportunity was won, a meeting was cancelled — without you polling for it.
This is the opposite direction to Inbound webhooks, which are how leads get pushed into a Leadey campaign.

Setting one up

Go to Settings → Webhooks, add your URL, and choose which events you want. Selecting none means you receive everything, including events added later. You can do the same over the API:
The response contains a signing secret, shown once and never again. Store it before you close the dialog. Your URL must be https and publicly reachable. Loopback and private addresses are rejected — our servers make the request, so accepting them would let a webhook be pointed at internal infrastructure.

What arrives

Three things worth knowing about the shape:
  • actorUserId is the member who caused it, or null when nothing human did — a sweeper, an inbound provider callback, a calendar sync.
  • changes carries what actually moved, so you don’t have to diff against your own copy to find out.
  • Entities match the REST API. A lead here has the same field names and types as GET /v1/leads/{id}, so you write one mapper, not two.

Verifying the signature

Always verify. Your endpoint is a public URL; the signature is what tells you a request genuinely came from Leadey. The header is t=<unix seconds>,v1=<hex>, where the hex is an HMAC-SHA256 over "{timestamp}.{raw body}" using your signing secret. The timestamp is inside the signed material, so a captured request can’t be replayed later with a fresh clock. Verify against the raw request body, before any JSON parsing — re-serialising changes the bytes and the signature will not match.
Use a constant-time comparison — compare_digest or timingSafeEqual — not ==.

Delivery, retries and replay

Respond with any 2xx as soon as you’ve stored the event. Do your processing afterwards: we time out after 10 seconds, and a slow handler turns into a retry you didn’t need. A failed delivery is retried six times over about nine hours, backing off roughly 10s → 1m → 5m → 30m → 2h → 6h. Redirects are not followed. Delivery is at-least-once, so build your handler to be idempotent. The id on the payload is stable across every retry and across a manual replay — key on it and ignore anything you’ve already seen. If an endpoint fails continuously it is paused automatically and the reason is shown in Settings. Fix the endpoint and re-enable it; that also clears the failure count. Delivery history is kept for 10 days. In Settings you can filter to just what’s failing and replay anything, or over the API:
Retrying means it will arrive on its own. Gave up means it won’t, and needs replaying.

Events

Leads

lead.created · lead.updated · lead.status_changed · lead.deleted

Opportunities

opportunity.created · opportunity.updated · opportunity.stage_changed · opportunity.won · opportunity.lost

Meetings

meeting.booked · meeting.rescheduled · meeting.cancelled · meeting.attended · meeting.no_showed

Calls

call.completed · call.recording_ready

Tasks

task.created · task.completed

Messages and forms

message.received · form.submitted
call.recording_ready is separate from call.completed because transcription finishes minutes later — subscribe to it rather than polling after every call. GET /api/webhook-event-types returns the current list, which is always exactly what can fire.
Not available. lead.merged (Leadey has no lead merge operation — creating a lead that resolves to an existing person returns that person rather than duplicating), lead.owner_changed (leads have no per-lead owner; opportunity owners change via opportunity.updated), message.sent, and activity.logged (custom activities don’t exist yet). We would rather list an event we don’t have than publish one that never fires.

Testing before you go live

The Test button in Settings — or POST /api/webhook-subscriptions/{id}/test — sends a sample event immediately and shows the real response code and body. The sample payload carries "test": true so nothing downstream mistakes it for a real change.