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: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
actorUserIdis the member who caused it, ornullwhen nothing human did — a sweeper, an inbound provider callback, a calendar sync.changescarries what actually moved, so you don’t have to diff against your own copy to find out.- Entities match the REST API. A
leadhere has the same field names and types asGET /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 ist=<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.
compare_digest or timingSafeEqual — not ==.
Delivery, retries and replay
Respond with any2xx 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.deletedOpportunities
opportunity.created · opportunity.updated · opportunity.stage_changed · opportunity.won · opportunity.lostMeetings
meeting.booked · meeting.rescheduled · meeting.cancelled · meeting.attended · meeting.no_showedCalls
call.completed · call.recording_readyTasks
task.created · task.completedMessages and forms
message.received · form.submittedcall.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 — orPOST /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.