Keep webhook setup on the public host boundary. The recommended shape is:

  • mount /webhooks/stripe in a dedicated raw-body pipeline
  • 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
end

Host 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
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 for the fix matrix row.

Replay

Replay is for reprocessing persisted webhook events after you fix host setup or handler code. Verify the end-to-end proof path with:

mix test test/accrue_host_web/webhook_ingest_test.exs