Connect by JBRH Open Connect

Outbound webhooks

The outbound webhook contract is written down and published as AsyncAPI: one signed POST per event, at-least-once, retried with backoff and then paused. What is not written down anywhere in the capability registry is a delivery service you can switch on. Treat this page as a specification to build a receiver against, not as a feature to plan around.

Status
Not yet Planned What this means
Audience
developer
Last verified
Product version
6.3.2

Status first, because it changes what you do with the rest#

CAPABILITY_STATUS in the capability registry records asyncapi — the description — as available. It records nothing for outbound delivery. The status word for this page is therefore not_yet: designed and written down, not recorded as implemented. Nothing below describes behaviour you can rely on receiving today.

That is an unusual thing for a developer page to say, and it is the reason this corpus has a status vocabulary at all. A published contract is evidence that somebody decided the shape of the thing. It is not evidence that the thing runs. The two get conflated constantly, and a developer who builds a queue, a retry table and an alert around events that do not arrive has paid for the conflation.

What you can do with the contract: build and unit-test a receiver against it, including the signature check, using fixtures of your own. That work does not expire, and it is small.

The delivery model as specified#

ClauseWhat it saysWhat a receiver must do
TransportHTTPS POST to the URL the workspace registered, one event per requestAccept POST on one path; do not expect a batch array
Bodyapplication/json, the Event envelopeRead the raw bytes before parsing — the signature covers them
GuaranteeAt-least-onceExpect duplicates as normal traffic, not as a fault
Identityid is stable across retriesDe-duplicate on id; store it long enough to outlive the retry window
FailureA slow or failing receiver is retried with backoff, then pausedReturn quickly; do the work after acknowledging
VisibilityThe pause is visible in the workspaceA person can see that you stopped receiving; you may not be able to

The pause matters more than the backoff. Retry with backoff is ordinary; what the specification adds is an end state in which delivery stops and somebody is told. That is the correct design — a receiver that has been returning 500 for two days is not a transient problem — but it means a receiver's own monitoring cannot be "we have not seen an event in an hour", because silence is also what a healthy weekend looks like.

Writing the receiver#

  1. Terminate TLS, read the raw body as bytes, and verify X-Connect-Signature before parsing anything.

    Result A malformed or hostile body never reaches your JSON parser, and the signature covers the bytes you actually received.

  2. Reject a X-Connect-Timestamp more than a few minutes from now.

    Result A captured request cannot be replayed indefinitely, even if the secret has not changed.

  3. Look up id. If you have seen it, answer 2xx and stop.

    Result The at-least-once guarantee costs you one index lookup instead of a duplicate record.

  4. Write the event to your own queue and answer 2xx immediately.

    Result Your processing time stops being the sender's latency problem, and a slow downstream system cannot get your subscription paused.

  5. Dispatch on type, and ignore any type you do not know.

    Result A new event type is additive rather than an outage in your handler.

What events would not tell you#

The six types are business events: a conversation was received or replied to, a follow-up fell due or was completed, an item was created in Needs You, a call ended. They are not a change feed over the data model. There is no person.updated, no opportunity.stage_changed, and the data object carries identifiers and a reason rather than a full record.

So even with delivery running, a system that needs the current state of a record reads it; an event tells it when to read. That is the usual shape for at-least-once delivery and it is worth designing for now, because a receiver that treats the payload as the record has to be rewritten the first time a field it wanted is not in data.

The alternative today is to poll what is actually published — which for the public surface means the documentation and status manifests rather than workspace data. Webhooks or polling sets the two side by side without pretending both are available.

Questions#

Can I register a subscriber URL today?

The capability registry records no outbound delivery capability and this corpus documents no public write endpoint, so there is no published way to register one. If your workspace has been given something out of band, that is between you and the operator; it is not part of the documented public surface.

Is the inbound webhook from my telephony provider the same mechanism?

No, and the two are deliberately kept apart. Provider callbacks are the application's own inbound surface, authenticated by the provider's signature and shaped by the provider. They are not described in the AsyncAPI document and are not a developer API.

What should my receiver return?

A 2xx as soon as you have durably accepted the event. Any non-2xx is a failure for retry purposes, and a body Connect never reads. Do not return 2xx before the event is safe on your side — at-least-once protects you from loss in flight, not from your own crash after acknowledging.