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.

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