How webhooks are received
This is the detail behind Webhooks as events: the generated route, how each signature scheme is checked, and how an event is read and kept once. Replies, handshakes and the further schemes are under Webhook replies, handshakes and further signature schemes, and queues under How queues are received.
- Where the route is.
POST <prefix>/<connection>, the prefix the app'swebhooksAPI declares, or/webhookswhen it declares none, in which case the routes belong to no declared API and have no record or OpenAPI document of their own;jadpo routeslists them underwebhooks /webhooks. In a declaredwebhooksAPI each is in its record (generated webhook, its connection,authentication signature,status 204) and its OpenAPI document. The path changes only with the API's prefix. Any method is the runtime's at that path (OPTIONSand405withAllow: OPTIONS, POST), so an app route there isroute.path_overlap. - The signature. Checked over the raw body with the connection's
secret, and the value it replaced while a deployment rotates it, every
candidate compared in constant time.
stripe:Stripe-Signature: t=<seconds>,v1=<hex>, eachv1tried, over<t>.<body>under the secret as written.standard_webhooks:webhook-id,webhook-timestampandwebhook-signature(or Svix'ssvix-headers), eachv1,<base64>tried, over<id>.<timestamp>.<body>under the Base64 after the secret'swhsec_(the secret as written when it has none).hmac_sha256: the body's HMAC in the headerheader:names, as 64 hex digits or Base64, with or withoutsha256=; it signs no time, so its age isn't checked.basic:Authorization: Basicagainst the secret asuser:password. A signed time more than five minutes from the host's clock, either way, is refused. Any refusal is401 unauthenticated, "The webhook's signature doesn't match.", saying nothing of why; the host's log line says (unsigned,stale,mismatch,no secret). - Reading the body. It is JSON (
400 invalid_requestotherwise). The event's id is the text or whole number atevent_id:, its type the text atevent_type:(400when either is missing). A type the package declares no event for is answered204and not kept, since a provider sends every type the account subscribes to. The event's fields are read from the body's root with its type's codec on the provider's wire, itswire_name:andwire:spellings included, every member it doesn't declare left out; a body it doesn't read is422 invalid_input, each problem by the provider's field, so the provider keeps it and sends it again once the package is fixed. occurred_at. Every event of a package withwebhooks:declaresoccurred_at: Instant, with nowire_name:orwire:(package.wire_invalid), and the runtime fills it, whatever the body holds of that name: from the placewebhooks: { occurred_at: }names (a whole number of seconds since 1970, or RFC 3339 text;400when it holds neither), else the signature's time, else when the request arrived. Each event names the type its provider sends with a textwire_name:, no two of one package alike (package.wire_name_invalid). An app's own event giveswire_name:for a queue ("How queues are received").- Keeping it once. In one read-committed transaction in the events'
store, the provider's id is recorded under
webhook <connection>injadpo_received_events, then the event's outbox row and a delivery for each subscriber are written, as an action's events are (its trace the request's,traceparentcontinued; depth 0;within:counted from arrival). An id already recorded is a repeat: answered204as the first was, nothing kept. The tick deletes ids received more than 30 days ago, longer than any provider retries. A store that can't be reached is503 unavailable, nothing kept, so the provider sends it again. - The store.
events: { store: }, or the only store that holds the app's records; with none, or several and none named, the connection'ssecret:isevent.store_required.jadpo_received_eventsis in that store's migration only when some connection receives webhooks. - The connection.
webhooks:takessecret:, a secret setting (connection.value_invalidotherwise,connection.key_missingwhen it is left out), andreplies:("Webhook replies, handshakes and further signature schemes"); given to a package whose provider declares nowebhooks:it isconnection.key_invalid. A package withwebhooks:and no connection giving a secret receives nothing. - What answers. Every request is answered once it is kept or dropped:
204with no body, deliveries running after the response. An event whose payload is over 64 KB, every event's limit (Limits and runtime faults), is413 request_too_large. It is counted against its address's rate limit, whatever credential it carries. A non-public package event is kept and reaches no subscriber, subscribing to one beingpackage.not_public; the package's own subscribers hear it ("Package subscribers and recipes insample/").
A field read by the package's own
decode:andencode:isn't built yet on a webhook's event, since those run at a call's site: it istoolchain.not_built_yet.
Markdown for agents: /docs/how-webhooks-are-received.md.Built from Jadpo main at a707efb.