Skip to content

docs: webhook delivery contract, manage-webhooks how-to, and build-webhook-receiver tutorial - #78

Merged
addisonkline merged 1 commit into
kline/v2-docsfrom
mpm/v2-docs-webhook
Jun 23, 2026
Merged

docs: webhook delivery contract, manage-webhooks how-to, and build-webhook-receiver tutorial#78
addisonkline merged 1 commit into
kline/v2-docsfrom
mpm/v2-docs-webhook

Conversation

@rheaton64

Copy link
Copy Markdown
Contributor

Webhook docs for the v2 release (first of two offers from the dev-list thread)

Adds the webhook contract documentation that
docs/references/http-api.md currently stubs at and that
docs/explanations/delivery-model.md gestures at without
specifying. Written from the chorus-side implementer's view —
I built and shipped a webhook receiver for MAIL last month, so
the bytes-and-headers details have ground truth.

New pages

  • docs/explanations/webhook-delivery.md — the conceptual
    contract: payload shape (including PR feat: support message replies and tags #74's reply_to and
    tags), HMAC-SHA256 signing (timestamp.body, NOT
    body.timestamp — explicitly called out), headers MAIL
    sends, receiver verification checklist, retry ladder (6
    attempts: immediate / +1s / +30s / +5min / +1h / +6h; ~7h31m
    total), retry conditions (timeout / 5xx / 429 retry; 2xx /
    other 4xx don't), and the load-bearing "inbox is source of
    truth" contract.
  • docs/howtos/manage-webhooks.md — operator's guide. Secret
    generation, register / list / inspect / patch / delete via
    the admin API, plus the secret-rotation coordination note.
  • docs/tutorials/build-webhook-receiver.md — implementer's
    walkthrough. A single-file FastAPI receiver with the right
    HMAC verification, dedup, skew check, and error codes.
    Explicitly calls out the two most common bugs (re-encoded
    JSON breaking signature; missing the timestamp. prefix on
    HMAC input).

Index updates

docs/{explanations,howtos,tutorials}/README.md each gain a
row linking to the new page.

Verification

All four facts easiest to get wrong, verified against
src/mail/server/src/mail_server/backends/base.py:653:

  • HMAC inputs: timestamp.raw_body (not raw_body.timestamp).
  • X-MAIL-Timestamp value: Unix seconds as a string.
  • Signature header format: sha256=<hex>.
  • Body bytes: payload.model_dump_json() posted as-is — no
    re-encoding.

Caveats / notes

  • The branch is based on kline/v2-docs. PR feat: support message replies and tags #74 (reply_to +
    tags) landed on main after the v2-docs branch was cut; my
    payload-shape docs reflect the canonical main-branch
    shape. Worth checking that v2-docs gets rebased onto main
    before this merges, or that the merge picks up the right
    webhook schema.
  • I deliberately left docs/references/http-api.md alone in
    this PR (it's a stub for the entire HTTP API, not just
    webhooks). Could sketch the webhook section in a follow-up.

Followups in scope of the original offer

  • Lists docs (second piece of the offer) coming as a separate
    PR on a fresh branch once this one is reviewed.

— minichorus-pm

…bhook-receiver tutorial

Three new pages plus three index updates, addressing the webhook
documentation gap in the v2 docs branch (the first of the two
docs offers from the dev-list thread on Final Prep for v2).

## docs/explanations/webhook-delivery.md (new)

The conceptual contract. What webhooks are for, the payload shape
(with v2's reply_to and tags from PR #74), the HMAC-SHA256 scheme
(timestamp.body, not body.timestamp — bug-shape worth flagging
explicitly), the headers MAIL sends, the receiver verification
checklist, the retry ladder (6 attempts: immediate, +1s, +30s,
+5min, +1h, +6h — total window ~7h31m), the retry conditions
(timeout / 5xx / 429 retry; 2xx / other 4xx don't), and the
'inbox is source of truth' contract that shapes how receivers
should handle internal failures.

Sources verified against
src/mail/server/src/mail_server/backends/base.py
(_handle_webhook_delivered and _webhook_delivered_post on origin
main; the v2-docs branch is currently behind main on the schema
changes from PR #74). Wrote against the canonical main-branch
shape so the docs match the v2 release contract; the docs branch
needs the rebase before merge.

## docs/howtos/manage-webhooks.md (new)

Operator's guide. Secret generation, POST /admin/webhooks (with
events and url), GET /admin/webhooks for listing, GET
/admin/webhooks/{id} for inspection, PATCH /admin/webhooks/{id}
for URL / secret rotation (event types are immutable in v2),
DELETE /admin/webhooks/{id}. Includes the rotation coordination
note (both sides must update at the same moment to avoid signature
failures in flight).

## docs/tutorials/build-webhook-receiver.md (new)

Implementer's walkthrough. A single-file FastAPI receiver with:

- verify_signature using HMAC over raw bytes (calls out the two
  most common bugs: re-encoded JSON breaking signature; missing
  the timestamp.body prefix).
- is_duplicate / mark_processed for event_id-based dedup with a
  24-hour garbage-collection window.
- is_timestamp_in_window for 5-min skew rejection.
- The full endpoint composing them with the right error codes
  (503 if secret not configured, 408 for skew, 403 for bad
  signature, 200 with status=duplicate for retries).
- Registration command for end-to-end test.
- Diagnostic checklist for the 'nothing arrives' case.

## Index updates

docs/{explanations,howtos,tutorials}/README.md each gain a row
linking to the new page.

## Followups in scope of the original offer

- docs/howtos/manage-mailing-lists.md exists as a stub today;
  drafting that one is the second piece of the offer and will
  land as a separate commit on the same branch.
- docs/references/http-api.md is a stub overall (not just for
  webhooks). The webhook-specific endpoints could be sketched
  there in a later pass; deferred so this commit stays focused
  on the conceptual + tutorial layer.

## Verification

The four facts that are easiest to get wrong (and that I had
ground truth on from the chorus-side webhook receiver):

- HMAC inputs: 'timestamp.raw_body' not 'raw_body.timestamp'.
- X-MAIL-Timestamp value: Unix seconds as a STRING, used in both
  the HMAC and the header so receivers can recompute from the
  header alone.
- Signature header format: 'sha256=<hex>'.
- Body bytes: payload.model_dump_json() (Pydantic's canonical
  JSON), posted as-is — re-encoded JSON has different bytes and
  breaks verification.

All four match what _webhook_delivered_post does in
src/mail/server/src/mail_server/backends/base.py:653.
@addisonkline
addisonkline merged commit 2ff26ea into kline/v2-docs Jun 23, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants