# Webhooks
For the canonical lifecycle glossary behind webhook-driven local projection
updates, see [Lifecycle Semantics](lifecycle_semantics.md). Use that guide for
the meaning of `active`, `canceling`, `paused`, `past_due`, and ended states;
use this guide for delivery, verification, and convergence wiring.
Keep webhook setup on the public host boundary. The recommended shape is:
- mount `/webhooks/stripe` in a dedicated raw-body pipeline
- mount `/webhooks/braintree` in a standard params pipeline that accepts
`bt_signature` and `bt_payload`
- implement a host handler with `use Accrue.Webhook.Handler`
- configure the signing secret in `config/runtime.exs`
- use replay through the supported admin and task surfaces
## Route and raw body
Stripe signatures are checked against the original request body, so the webhook
scope must use a parser pipeline with a raw body reader:
```elixir
pipeline :accrue_webhook_raw_body do
plug Plug.Parsers,
parsers: [:json],
pass: ["*/*"],
json_decoder: Jason,
body_reader: {Accrue.Webhook.CachingBodyReader, :read_body, []}
end
scope "/webhooks" do
pipe_through :accrue_webhook_raw_body
accrue_webhook "/stripe", :stripe
end
```
Braintree does not require Stripe's raw-body reader. The handler still belongs
at the same host boundary, but the plug path reads `bt_signature` and
`bt_payload`, then passes them through `Signature.parse_braintree!/2` before
Accrue persists the normalized event.
```elixir
scope "/webhooks" do
pipe_through :browser
accrue_webhook "/braintree", :braintree
end
```
## Host handler boundary
Use `use Accrue.Webhook.Handler` in a host-owned module:
```elixir
defmodule MyApp.BillingHandler do
use Accrue.Webhook.Handler
@impl Accrue.Webhook.Handler
def handle_event(type, event, ctx) do
MyApp.Billing.handle_webhook(type, event, ctx)
end
end
```
## Signature failures and generic HTTP failures
Invalid signatures should return a generic `400`. Host misconfiguration should
surface as a generic server failure, with the actionable detail carried by the
stable diagnostic code and linked fix path in the troubleshooting guide.
Raw-body ordering and parser placement issues map to **`ACCRUE-DX-WEBHOOK-RAW-BODY`** — see [Troubleshooting — `ACCRUE-DX-WEBHOOK-RAW-BODY`](troubleshooting.md#accrue-dx-webhook-raw-body) for the fix matrix row.
For Braintree, the support-visible failure shape is different: wrong
`portal_base_url` or `portal_mount_path` does not break an upstream hosted
redirect because there is none. It breaks the local checkout or billing-portal
URL generation that the mounted portal returns to the host.
## Processor-aware event handling
Stripe and Braintree share the same Accrue ingest boundary, but they do not
prove the same thing:
- Stripe webhook truth comes from upstream event delivery and hosted URLs.
- Braintree webhook truth is normalized into Accrue event shapes and may finish
local portal work by persisting `accrue.portal.checkout.completed`.
That local checkout-completion event is projection-backed. On Braintree, the
authoritative completion story is not an upstream hosted redirect callback; it
is the persisted local event that `Accrue.Webhook.DefaultHandler` reduces after
the mounted checkout flow succeeds. The same reducer also normalizes Braintree
subscription webhook kinds through `normalize_braintree_type/1` so replay and
recovery stay inside the existing invoice and subscription reducers.
## Replay
Replay is for reprocessing persisted webhook events after you fix host setup or
handler code. Use it when the host boundary is fixed, when the async dispatcher
dead-lettered a row, or when Braintree local checkout completion or metered
renewal recovery needs the persisted event stream to converge again.
Core entry points:
- `mix accrue.webhooks.replay` for the bounded CLI path
- `Accrue.Webhooks.DLQ.requeue/1` and `Accrue.Webhooks.DLQ.requeue_where/2`
for explicit replay of persisted dead or failed rows
- the admin replay surface in the example host for operator-driven recovery
Verify the end-to-end proof path with:
```bash
mix test test/accrue_host_web/webhook_ingest_test.exs
```
Webhook delivery is the convergence path, not a separate lifecycle glossary.
After lifecycle actions run, local projection truth may briefly lead or lag
external provider state; the lifecycle guide defines what the resulting states
mean, and webhook replay helps those local projections converge cleanly.
When triaging Braintree-specific failures, keep the support contract provider
honest: replay fixes the persisted Accrue event path, not an upstream hosted
checkout session. If `portal_base_url`, `portal_mount_path`, auth continuity,
or Hosted Fields readiness is wrong, fix the host setup first, then replay the
stored event so the local projection and telemetry catch back up.