Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: External services

External services

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's webhooks API declares, or /webhooks when it declares none, in which case the routes belong to no declared API and have no record or OpenAPI document of their own; jadpo routes lists them under webhooks /webhooks. In a declared webhooks API 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 (OPTIONS and 405 with Allow: OPTIONS, POST), so an app route there is route.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>, each v1 tried, over <t>.<body> under the secret as written. standard_webhooks: webhook-id, webhook-timestamp and webhook-signature (or Svix's svix- headers), each v1,<base64> tried, over <id>.<timestamp>.<body> under the Base64 after the secret's whsec_ (the secret as written when it has none). hmac_sha256: the body's HMAC in the header header: names, as 64 hex digits or Base64, with or without sha256=; it signs no time, so its age isn't checked. basic: Authorization: Basic against the secret as user:password. A signed time more than five minutes from the host's clock, either way, is refused. Any refusal is 401 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_request otherwise). The event's id is the text or whole number at event_id:, its type the text at event_type: (400 when either is missing). A type the package declares no event for is answered 204 and 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, its wire_name: and wire: spellings included, every member it doesn't declare left out; a body it doesn't read is 422 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 with webhooks: declares occurred_at: Instant, with no wire_name: or wire: (package.wire_invalid), and the runtime fills it, whatever the body holds of that name: from the place webhooks: { occurred_at: } names (a whole number of seconds since 1970, or RFC 3339 text; 400 when it holds neither), else the signature's time, else when the request arrived. Each event names the type its provider sends with a text wire_name:, no two of one package alike (package.wire_name_invalid). An app's own event gives wire_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> in jadpo_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, traceparent continued; depth 0; within: counted from arrival). An id already recorded is a repeat: answered 204 as 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 is 503 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's secret: is event.store_required. jadpo_received_events is in that store's migration only when some connection receives webhooks.
  • The connection. webhooks: takes secret:, a secret setting (connection.value_invalid otherwise, connection.key_missing when it is left out), and replies: ("Webhook replies, handshakes and further signature schemes"); given to a package whose provider declares no webhooks: it is connection.key_invalid. A package with webhooks: and no connection giving a secret receives nothing.
  • What answers. Every request is answered once it is kept or dropped: 204 with no body, deliveries running after the response. An event whose payload is over 64 KB, every event's limit (Limits and runtime faults), is 413 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 being package.not_public; the package's own subscribers hear it ("Package subscribers and recipes in sample/").

A field read by the package's own decode: and encode: isn't built yet on a webhook's event, since those run at a call's site: it is toolchain.not_built_yet.

Markdown for agents: /docs/how-webhooks-are-received.md.Built from Jadpo main at a707efb.