Outbound Webhooks: Signed, Retried, and Kept When They Fail
The direction most vendors do badly: something happens here and something has to happen somewhere else. Payload mapping, signed timestamps, an idempotency key you did not have to invent, a retry policy and a dead-letter queue — and why the dead letter is the part that matters.
Outbound webhooks are where integration quality actually shows. Sending an HTTP request when something happens is a morning's work. Sending it reliably — signed so the receiver can trust it, with an idempotency key so a retry does not duplicate, retried on failure, and kept somewhere visible when it eventually fails for good — is the difference between a feature and a liability.
The liability version is common and easy to spot: it fires, and if the receiver was down, the event is gone. Nobody notices for three weeks, because the failure is silent by construction.
What actually goes out
The request a connector sends
Header or element What the receiver gets
A JSON body you shaped yourself Payload mapping, so the field names are the receiver's, not ours
A connector-type header Which connector sent it, if you run several
An event-type header What happened, so the receiver can route without parsing the body
A timestamp header The moment it was signed — the receiver can reject anything stale
An HMAC-SHA256 signature over timestamp and body Proof of origin and integrity, and it is not replayable at leisure
An idempotency key So a retried delivery can be recognised and ignored by the receiver
Any auth headers you configured For receivers that want a bearer token or an API key of their own
The signature covers the timestamp and the body rather than the body alone. That is deliberate: signing only the payload lets a captured request be replayed forever, because the signature stays valid. Including the timestamp means the receiver can enforce a freshness window and a stolen request expires.
Payload mapping, which is the feature people underestimate
A connector sends the shape you define, not a fixed internal structure. This is the difference between an outbound webhook that works with the system you already have and one that requires somebody to build a translator in the middle.
It also means the receiving end can be something dumb. A Google Apps Script, a small endpoint on your website, a channel in a chat tool — none of them need to understand our data model, because the payload arrives already speaking their language. If you are choosing between integration approaches, this is usually the cheapest one that still counts as real integration.
Retries, and the dead letter
A connector has a retry policy: a maximum number of attempts, tried in sequence, with success recorded against the connector so its health is visible. If every attempt fails, the delivery is written to a dead-letter record rather than discarded.
What happens when the receiver is down
Most SME systems discard a failed webhook and log a line nobody reads. Keeping the payload is what makes the difference between "the integration broke last month and we lost the events" and "here are the eleven events that did not arrive".
Check the connector health, not the logs
Because success and failure are recorded against the connector itself, a connector that stopped working three weeks ago is visible without anybody reading a log file. Put it in the monthly rhythm — it takes ten seconds and it is the only thing that catches a silently dead integration.
How a connector gets triggered
It hangs off a workflow action, which means the trigger is one of your own rules rather than a fixed list of system events. That is a meaningfully different design from "we fire on invoice.created": you decide what constitutes an event worth sending, using the same conditions you would use to route an approval or raise an alert.
-
Define the rule first, without the connector
Get the condition right and have it raise a notification you can see. A rule that fires on the wrong events will send those wrong events perfectly reliably.
-
Build the receiver and make it accept anything
First version returns 200 and writes the body somewhere you can read it. Do not validate yet — you are trying to see what arrives.
-
Send the test payload
The connector can be tested with a sample payload before it is live. Use it. Look at the headers as well as the body.
-
Shape the mapping to what the receiver wants
Now that you can see both ends. This is the step that would otherwise become a translator service nobody maintains.
-
Verify the signature on the receiving side
Recompute the HMAC over the timestamp and body with your secret and compare. If you skip this, you have built an endpoint anyone can post to, and its URL will end up in a screenshot eventually.
-
Then enforce a freshness window
Reject anything with a timestamp older than a few minutes. This is what the timestamp in the signature is for and it is two lines of code.
The simpler cousin: notification channels
Not every outbound need is a webhook. If the requirement is "somebody should be told", the notification channels are the better answer: paste a webhook URL from Teams, Discord, Google Chat or Slack, or connect a chat tool, and events land where people already look. No mapping, no signature, no receiver to build. Chat alerts covers those.
Use a connector when a system has to act. Use a channel when a person has to know. Conflating the two produces either a chat room full of machine noise or an integration nobody can debug.
What AWRA OpsHub does today
- Per-connector payload mapping, so the body speaks the receiver's field names.
- HMAC-SHA256 signing over timestamp plus body, with configurable auth headers alongside for receivers that want their own token.
- An idempotency key generated per delivery and sent as a header.
- A configurable retry policy, with each attempt logged as its own call record.
- A dead-letter record holding the payload, headers and idempotency key when the policy is exhausted.
- Success and failure recorded against the connector, so a dead integration is visible.
- A test send with a sample payload before going live.
What it does not do
- No automatic re-drive of dead letters — the payload is kept, and replaying it is a manual decision rather than a button that flushes a queue.
- No per-event subscription model. Triggers come from workflow rules, so there is no published catalogue of events another developer can subscribe to.
- No exponential backoff schedule spread over hours; the retry policy is attempts in sequence, not a long-tail redelivery over a day.
- No signature verification helper library for the receiving side. You compute the HMAC yourself, which is four lines in any language.
Our take
Shape the payload to the receiver rather than building a translator, verify the signature and enforce a freshness window on the receiving side, and check connector health monthly rather than reading logs. The dead-letter record is the feature to value most when comparing vendors — ask any of them what happens to a webhook after the last retry fails, and listen for whether the payload survives.
Signed, retried, and kept when it fails
Payload mapping to the receiver's field names, HMAC signing over timestamp and body, an idempotency key per delivery, a retry policy, and a dead-letter record you can actually open.
See plans & pricingFrequently asked questions
What does an outbound webhook send?
A JSON body in a shape you define through payload mapping, plus headers naming the connector and the event, a timestamp, an HMAC-SHA256 signature over the timestamp and body, an idempotency key, and any auth headers you configured. The signature covers the timestamp as well as the body specifically so a captured request cannot be replayed indefinitely.
What happens if the receiving system is down?
Each attempt is logged individually, the configured retry policy runs, and when it is exhausted the delivery is written to a dead-letter record holding the payload, headers and idempotency key. The connector itself is marked with the failure, so a broken integration is visible rather than merely quiet. Replaying a dead letter is a manual decision — there is no automatic re-drive.
Can we control the shape of the payload?
Yes, per connector. That is the feature most worth having, because it means the receiving end can be something simple — a small endpoint on your website, a Google Apps Script — that never needs to understand our data model. Without mapping, every integration needs a translator in the middle that somebody has to maintain.
What triggers an outbound webhook?
A workflow action, which means the trigger is one of your own rules rather than a fixed list of system events. You decide what counts as an event worth sending using the same conditions you would use to route an approval. The trade-off is that there is no published event catalogue another developer can subscribe to.
How should the receiving end verify a request?
Recompute the HMAC-SHA256 over the timestamp and the raw body using your shared secret and compare it to the signature header, then reject anything whose timestamp is older than a few minutes. Both steps are a handful of lines in any language, and skipping them leaves you with an endpoint anyone can post to — whose URL will end up in a screenshot eventually.
When should we use a chat notification instead of a connector?
Use a notification channel when a person needs to know, and a connector when a system needs to act. Channels are a pasted webhook URL from Teams, Discord, Google Chat or Slack with no mapping, no signature and no receiver to build. Conflating the two gives you either a chat room full of machine noise or an integration nobody can debug.