# Webhooks

A webhook is an HTTP request a provider makes to a URL you gave it, when something happens on their side. It replaces polling and it inverts the reliability problem: you no longer control when the call arrives, how often it repeats, or in what order. A receiver that verifies the sender, answers immediately and tolerates duplicates is correct; one that does any of those loosely will lose events or process them twice.

- **Status:** Reference
- **Audience:** developer
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/technology/webhook/

## Four rules, and the failure each one prevents

| Rule | Prevents |
|---|---|
| Verify the signature over the raw request bytes | Accepting a forged event. Your URL is public; anyone can POST to it |
| Answer 2xx quickly, then do the work | Provider timeouts that trigger redelivery of an event you are already processing |
| Key on the provider's event identifier | The same delivery being applied twice when a retry overtakes your acknowledgement |
| Never derive identity from the body alone | A payload that names a workspace, mailbox or account it has no right to name |

The second rule fights the third. Answering fast means acknowledging before you have finished, which means a crash after the acknowledgement loses the event, and a slow acknowledgement gets you a duplicate. There is no arrangement without one of those risks — so choose duplicates, and make them harmless. Store the raw event first, acknowledge, then process from storage.

## Safe to receive twice

Delivery is at-least-once, always, whatever a provider's marketing says. The mechanism that makes that survivable is a unique constraint, not a check. Reading "have I seen this?" and then writing is two operations with a gap between them, and two concurrent deliveries will both read *no*. A uniqueness constraint on the provider's event identifier is decided by the database in one operation, and the loser catching a conflict is the correct outcome rather than an error to report.

1. Read the raw bytes and verify the signature before parsing anything.
   - Result: A forged or mangled body is rejected without ever becoming a record.
2. Insert the event keyed on the provider's identifier.
   - Result: A duplicate collides and is discarded. The first arrival wins, and which one it was does not matter.
3. Acknowledge with 2xx.
   - Result: The provider stops retrying. Nothing downstream has run yet, and nothing needs to have.
4. Process from the stored record.
   - Result: A failure here is your problem to retry on your own schedule, rather than a redelivery storm you cannot pause.

> **Careful** Returning 5xx because your processing failed asks the provider to send it again — and again. A persistent bug plus automatic redelivery is an outage that grows. Return 5xx only when you genuinely could not accept the event, not when you could not act on it.

## Order, and why you cannot have it

Events arrive in the order the network delivers them, which is not the order they happened. A message-status update can land before the message it describes; a call-ended event can beat a call-answered one. Any receiver that assumes sequence will eventually build a record of something that never happened.

The fix is to make state derived rather than accumulated: hold the provider's own timestamps, apply the latest one wins, and let a late arrival be ignored rather than replayed. State that can be recomputed from what you have stored survives out-of-order delivery; state you incremented as events arrived does not.

## Does Connect use webhooks?

**Used, inbound, on every real-time channel.** WhatsApp messages arrive by webhook (`whatsapp_inbound`), inbound SMS arrives by webhook (`sms_inbound`), and inbound calls reach Connect through the carrier's callbacks (`inbound_call`). Each one lands in the provider-shaped `tenant_*` tables first and is bridged into the canonical `threads`, `messages` and `contacts` the engine reads — which is precisely why adding a provider changes nothing downstream.

That two-stage shape is also what makes duplicate delivery cheap here. The provider-shaped row is the record of *what arrived*; the canonical row is the record of *what it means*. A second copy of an arrival does not become a second conversation.

## Questions

### Is a shared secret in the URL good enough instead of a signature?

No. A URL travels through logs, proxies and browser history, and it authenticates nothing about the body — an attacker who learns the URL can send any payload they like to it. A signature covers the content as well as the sender.

### Should a webhook receiver do the work inline if it is quick?

Only if 'quick' is guaranteed, which it is not once a model call, a provider round trip or a slow query is involved. The cost of storing first and processing after is one insert; the cost of getting it wrong is redelivery while you are already busy.

### What if I miss events entirely — a receiver down for an hour?

Providers retry for a while and then stop, so a long outage means gaps. That is the argument for a reconciliation path that can re-read state from the provider rather than relying only on pushes, and for treating webhooks as the fast path rather than the only one.

## Related

- [Provider webhook signatures](https://connectbyjbrh.com/docs/technology/webhook-signature/)
- [Idempotency](https://connectbyjbrh.com/docs/technology/idempotency/)
- [Retries and backoff](https://connectbyjbrh.com/docs/technology/retry/)
- [Webhooks or polling](https://connectbyjbrh.com/developers/webhooks-vs-polling/)
- [Outbound webhooks](https://connectbyjbrh.com/developers/webhooks-outbound/)
- [Idempotency for retried telephony webhooks](https://connectbyjbrh.com/research/idempotent-telephony-webhooks/)

## What this page is based on

- Connect capability registry (docs-source/facts.py) — `whatsapp_inbound`, `sms_inbound`, `inbound_call`
- Connect source pack (docs-source/sources/GENERAL.md §3) — provider tables bridged to canonical records
- https://www.rfc-editor.org/rfc/rfc9110 — HTTP Semantics
