Stripe webhook signature verification and event construction.
LatticeStripe.Webhook provides pure-functional HMAC-SHA256 signature verification for incoming Stripe webhook payloads. It is designed to be used in a Plug pipeline or any web framework — it has no Plug dependency itself.
Usage
# In a Plug or controller action, after reading the raw body:
raw_body = conn.assigns[:raw_body]
sig_header = Plug.Conn.get_req_header(conn, "stripe-signature") |> List.first()
secret = Application.fetch_env!(:my_app, :stripe_webhook_secret)
case LatticeStripe.Webhook.construct_event(raw_body, sig_header, secret) do
{:ok, event} ->
handle_event(event)
send_resp(conn, 200, "ok")
{:error, :missing_header} ->
send_resp(conn, 400, "Missing Stripe-Signature header")
{:error, :timestamp_expired} ->
send_resp(conn, 400, "Webhook timestamp too old")
{:error, reason} ->
send_resp(conn, 400, "Signature verification failed: #{reason}")
endImportant: Raw Body Requirement
Stripe signs the raw, unmodified request body. Most web frameworks parse the body and discard the original bytes. You must configure your framework to preserve the raw body before calling these functions. See the LatticeStripe Plug documentation for a ready-made solution.
Replay Attack Protection
By default, verify_signature/3 rejects webhooks with a timestamp older than
300 seconds (5 minutes). Override with tolerance: seconds in opts.
Multiple Secrets (Secret Rotation)
Pass a list of secrets to verify against any of them. Useful during Stripe webhook secret rotation — the new and old secret both work until rotation completes.
Webhook.verify_signature(payload, header, [old_secret, new_secret])Stripe API Reference
See the Stripe Webhooks documentation for the full webhook reference, event catalog, and delivery guarantees.
Summary
Functions
Verifies a Stripe webhook signature and, if valid, constructs a typed %Event{}.
Like construct_event/4 but raises SignatureVerificationError on failure.
Generates a Stripe-compatible webhook signature header for testing.
Verifies a Stripe webhook signature header against a payload and secret.
Like verify_signature/4 but raises SignatureVerificationError on failure.
Types
Functions
@spec construct_event(String.t(), String.t() | nil, secret(), keyword()) :: {:ok, LatticeStripe.Event.t()} | {:error, verify_error()}
Verifies a Stripe webhook signature and, if valid, constructs a typed %Event{}.
This is the primary function for handling incoming webhooks. It:
- Verifies the
Stripe-Signatureheader using HMAC-SHA256 - Checks the timestamp is within the tolerance window (replay attack protection)
- Decodes the JSON payload into a
%LatticeStripe.Event{}struct
Parameters
payload- The raw, unmodified request body stringsig_header- The value of theStripe-Signatureheader (e.g.,"t=1234,v1=abc...")secret- Your webhook signing secret (string or list of strings for rotation)opts- Options::tolerance- max age in seconds (default: 300). Set0to disable staleness check.
Returns
{:ok, %Event{}}on success{:error, verify_error()}on failure — seeverify_error/0
@spec construct_event!(String.t(), String.t() | nil, secret(), keyword()) :: LatticeStripe.Event.t()
Like construct_event/4 but raises SignatureVerificationError on failure.
Returns
%Event{}on success- Raises
LatticeStripe.Webhook.SignatureVerificationErroron failure
Generates a Stripe-compatible webhook signature header for testing.
Use this in tests to produce a Stripe-Signature header that passes
verify_signature/3. This avoids hard-coding computed HMAC values in tests
and correctly simulates what Stripe's servers send.
Parameters
payload- The JSON-encoded payload stringsecret- The webhook signing secretopts- Options::timestamp- Unix timestamp integer to embed (default: current time)
Returns
A Stripe-Signature header value string, e.g. "t=1680000000,v1=abc123...".
Example
header = LatticeStripe.Webhook.generate_test_signature(payload, secret)
{:ok, event} = LatticeStripe.Webhook.construct_event(payload, header, secret)
@spec verify_signature(String.t(), String.t() | nil, secret(), keyword()) :: {:ok, integer()} | {:error, verify_error()}
Verifies a Stripe webhook signature header against a payload and secret.
Performs timing-safe HMAC-SHA256 comparison via Plug.Crypto.secure_compare/2.
Returns the parsed timestamp integer on success (useful for logging).
Parameters
payload- The raw request body stringsig_header- TheStripe-Signatureheader valuesecret- Signing secret or list of secrets (for rotation)opts- Options::tolerance- max timestamp age in seconds (default: 300)
Returns
{:ok, timestamp}wheretimestampis a Unix integer on success{:error, :missing_header}— no header provided{:error, :invalid_header}— header is present but malformed{:error, :timestamp_expired}— timestamp older than tolerance{:error, :no_matching_signature}— HMAC doesn't match any provided secret
Like verify_signature/4 but raises SignatureVerificationError on failure.
Returns
timestamp(integer) on success- Raises
LatticeStripe.Webhook.SignatureVerificationErroron failure