# How webhooks are received

This is the detail behind [Webhooks as events](receiving-from-outside.md#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](webhook-replies-handshakes-and-further-signature-schemes.md),
and queues under [How queues are received](how-queues-are-received.md).

- **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](limits-and-runtime-faults.md)),
  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`.
