PinStripe.WebhookController (PinStripe v0.3.4)
View SourceBase controller for handling Stripe webhook events.
This module provides a use macro that injects webhook handling functionality
into your Phoenix controller. It automatically verifies webhook signatures and
dispatches events to handler functions defined using the handle/2 macro.
Usage
Create a controller in your Phoenix app and define event handlers:
defmodule MyAppWeb.StripeWebhookController do
use PinStripe.WebhookController
handle "customer.created", fn event ->
# Process customer.created event
customer = event["data"]["object"]
IO.inspect(customer, label: "New customer")
:ok
end
handle "invoice.paid", MyApp.InvoicePaidHandler
endThen add it to your router:
scope "/webhooks" do
pipe_through [:api]
post "/stripe", StripeWebhookController, :create
endConfiguration
Configure your webhook secret:
config :pin_stripe,
stripe_webhook_secret: "whsec_..."Security
This controller automatically verifies webhook signatures using the
stripe-signature header. Invalid signatures are rejected with a 400 response.
The raw request body must be available in conn.assigns.raw_body for signature
verification to work. Use PinStripe.ParsersWithRawBody in your endpoint.
Handler Functions
Handlers can be either:
- Anonymous functions that take the event as an argument
- Module names that implement a
handle_event/1function
Function Handler Example
handle "customer.created", fn event ->
# Process customer.created event
:ok
endModule Handler Example
handle "invoice.paid", MyApp.InvoicePaidHandlerThen create the handler module:
defmodule MyApp.InvoicePaidHandler do
def handle_event(event) do
invoice = event["data"]["object"]
# Process the paid invoice
:ok
end
endError Handling
The controller returns HTTP 200 for any handler that completes normally (regardless of return value). Stripe considers a 200 response as successful delivery and will not retry the event.
If a handler raises an exception, it propagates through Phoenix, which returns a 500 response. Stripe treats any non-2xx response as a failure and retries with exponential backoff (5 min, 30 min, 2 hours, 5 hours, 10 hours, then every 12 hours).
Warning: After 3 days of consecutive failures, Stripe disables the endpoint
entirely. Once disabled, all events stop being delivered — not just the ones that
failed. Use raise only for transient failures (e.g., database timeouts) where a
retry is likely to succeed.
Examples
# Successful handling — returns 200
handle "invoice.paid", fn event ->
MyApp.Billing.process_invoice(event["data"]["object"])
:ok
end
# Permanent failure — returns 200, handle the error yourself
handle "checkout.session.completed", fn event ->
case MyApp.Billing.provision(event) do
:ok -> :ok
{:error, reason} -> MyApp.ErrorReporter.report(reason, event)
end
end
# Transient failure — raises, returns 500, Stripe retries
handle "customer.subscription.updated", fn event ->
MyApp.Billing.sync_subscription!(event["data"]["object"])
end