LatticeStripe.Webhook (LatticeStripe v0.2.0)

Copy Markdown View Source

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}")
end

Important: 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

secret()

@type secret() :: String.t() | [String.t(), ...]

verify_error()

@type verify_error() ::
  :missing_header
  | :invalid_header
  | :no_matching_signature
  | :timestamp_expired

Functions

construct_event(payload, sig_header, secret, opts \\ [])

@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:

  1. Verifies the Stripe-Signature header using HMAC-SHA256
  2. Checks the timestamp is within the tolerance window (replay attack protection)
  3. Decodes the JSON payload into a %LatticeStripe.Event{} struct

Parameters

  • payload - The raw, unmodified request body string
  • sig_header - The value of the Stripe-Signature header (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). Set 0 to disable staleness check.

Returns

  • {:ok, %Event{}} on success
  • {:error, verify_error()} on failure — see verify_error/0

construct_event!(payload, sig_header, secret, opts \\ [])

@spec construct_event!(String.t(), String.t() | nil, secret(), keyword()) ::
  LatticeStripe.Event.t()

Like construct_event/4 but raises SignatureVerificationError on failure.

Returns

generate_test_signature(payload, secret, opts \\ [])

@spec generate_test_signature(String.t(), String.t(), keyword()) :: String.t()

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 string
  • secret - The webhook signing secret
  • opts - 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)

verify_signature(payload, sig_header, secret, opts \\ [])

@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 string
  • sig_header - The Stripe-Signature header value
  • secret - Signing secret or list of secrets (for rotation)
  • opts - Options:
    • :tolerance - max timestamp age in seconds (default: 300)

Returns

  • {:ok, timestamp} where timestamp is 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

verify_signature!(payload, sig_header, secret, opts \\ [])

@spec verify_signature!(String.t(), String.t() | nil, secret(), keyword()) ::
  integer()

Like verify_signature/4 but raises SignatureVerificationError on failure.

Returns