For the canonical lifecycle glossary behind webhook-driven local projection
updates, see Lifecycle Semantics. 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/stripein a dedicated raw-body pipeline - mount
/webhooks/braintreein a standard params pipeline that acceptsbt_signatureandbt_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:
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
endBraintree 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.
scope "/webhooks" do
pipe_through :browser
accrue_webhook "/braintree", :braintree
endHost handler boundary
Use use Accrue.Webhook.Handler in a host-owned module:
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
endSignature 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 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.replayfor the bounded CLI pathAccrue.Webhooks.DLQ.requeue/1andAccrue.Webhooks.DLQ.requeue_where/2for 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:
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.