Module · Webhooks
Outgoing webhook
Lesson 3 of 11 · 9 min
Now you are the system with the news. Something changes in your CRM and another system needs to know — a fulfilment service, a data warehouse, a partner's dashboard, an internal tool somebody wrote.
You could do that with an API Request node in a workflow, and sometimes you should. But there is a second mechanism that is not a node at all: subscribe a URL to a list of events, and Expedify calls it whenever one happens, with no workflow involved.
What you are creating
An outgoing webhook is a subscription. Four things define it:
- A URL. Theirs, this time. Where the calls go.
- A list of events. Which things you want them told about.
- A secret. Generated for you, so the receiving end can verify the call came from you and not from someone imitating you.
- A delivery policy. Three retries and a thirty-second timeout, by default.
The URL must resolve when you create it. Registering a subscription against a host that does not exist is rejected outright, with the message naming the host. That is a small kindness — it catches the typo at the point you make it rather than after a week of silent failures — and it means you cannot register an endpoint before it is built.
What you can subscribe to
Twenty events across six categories. They are the lifecycle of the main CRM objects, plus voice calls:
| Category | Events |
|---|---|
contact.created · contact.updated · contact.deleted | |
company.created · company.updated · company.deleted | |
deal.created · deal.updated · deal.stage_changed · deal.deleted | |
product.created · product.updated · product.deleted | |
tasks.created · tasks.updated · tasks.deleted | |
voice_calls.created · voice_calls.updated · voice_call.status_changed · voice_calls.deleted |
- Events
contact.created·contact.updated·contact.deleted
- Events
company.created·company.updated·company.deleted
- Events
deal.created·deal.updated·deal.stage_changed·deal.deleted
- Events
product.created·product.updated·product.deleted
- Events
tasks.created·tasks.updated·tasks.deleted
- Events
voice_calls.created·voice_calls.updated·voice_call.status_changed·voice_calls.deleted
The event names are not consistent, so do not guess them. Contacts, companies, deals and products are singular — contact.created. Tasks and voice calls are plural — tasks.created, voice_calls.created. And inside that same voice category, one event is singular: voice_call.status_changed. Pick them from the list rather than typing what you expect them to be.
deal.stage_changed is worth noticing on its own. It is the only event that fires on a specific kind of change rather than on any update, and it is usually what people mean when they subscribe to deal.updated and then complain about the volume.
What the receiving end sees
POST https://their-system.example/hooks/expedify
Content-Type: application/json
(signature header, verified with the secret)
{
"event": "deal.stage_changed",
"data": { ... the record ... }
}Every delivery is recorded: which event, what status code came back, how long it took, and the payload that was sent. That log is the thing that makes outgoing webhooks supportable — when a partner says they did not receive something, you can answer from evidence rather than belief.
Retries, and what they mean for the other end
A failed delivery is retried three times by default, and the timeout is thirty seconds. That is generous, and it has a consequence the receiving developer needs to know: they will sometimes get the same event more than once, including when their system did process it and was merely slow to say so.
This is the same duplicate-delivery problem from the incoming lesson, seen from the sending side. If you are on both ends of an integration, it is worth being explicit about it once rather than discovering it in production twice.
What breaks
A subscription is not a workflow, and does not appear on any canvas. This is the single most common confusion. Nothing about an outgoing webhook shows up in the workflow builder, its deliveries are not workflow executions, and looking for it in the execution log will not find it. It lives on the Webhooks page and nowhere else.
Subscribing to *.updated is louder than you think. Every field change, every automation touching a record, every bulk import fires it. A subscription to contact updates on a busy CRM can send tens of thousands of calls a day, and the other system will start rate-limiting you. Subscribe to the narrowest event that answers your question.
The receiving end must be fast, not just correct. Thirty seconds is the ceiling and it is not a budget to spend. If their handler does real work before replying, they will occasionally exceed it, you will retry, and they will process it twice. The standard answer is for them to acknowledge immediately and do the work afterwards — worth saying to whoever is building the other end.
There is no replay. Once retries are exhausted, that event is gone. If a partner's endpoint was down for an afternoon, the events from that afternoon are not waiting anywhere. Watch the failure count; it is the only warning you get.
Try it
- Create a subscription pointing at any endpoint you control, subscribed to
contact.createdonly. - Create a contact and read the delivery record — status code, response time, and the payload that went out. That payload is your integration contract.
- Now add
contact.updatedand edit a few contacts. Compare the volumes. That comparison decides which events you keep. - Point one subscription at a URL that returns a 500 and watch the retries and the failure count. That is what a partner outage looks like from your side.
- Finally try to create one against a hostname that does not exist. The rejection names the host, and it is the fastest typo check available.
Next: Webhook or API Request — the two ways to send data out, and how to tell which one a job wants.

