Verifies Lettermint webhook deliveries.
A delivery is genuine when its HMAC-SHA256 signature over "<t>." <> raw body, keyed with the endpoint's signing secret (whsec_..., used as is),
matches, and its timestamp is within the tolerance:
webhook = Lettermint.Webhook.new(System.fetch_env!("LETTERMINT_WEBHOOK_SECRET"))
case Lettermint.Webhook.verify(webhook, raw_body, conn.req_headers) do
{:ok, %Lettermint.WebhookPayload{event: event, data: data}} -> handle(event, data)
{:error, %Lettermint.WebhookVerificationError{}} -> send_resp(conn, 400, "")
endPass the raw request body: the signature covers the exact bytes, so
decoding and re-encoding the JSON breaks it. In Phoenix or Plug, keep the
raw body with a :body_reader (see the README).
inspect/1 and Jason.encode/1 show only the tolerance, never the secret.
Summary
Types
Request headers: a map (any case; a value or a list of values), a list of
{name, value} (such as conn.req_headers), or a Plug.Conn.
Functions
Creates a verifier for a signing secret.
Verifies a delivery from its raw body and request headers, and returns the decoded payload.
Verifies the raw body against an X-Lettermint-Signature value, for setups
where the headers are not at hand.
Types
Request headers: a map (any case; a value or a list of values), a list of
{name, value} (such as conn.req_headers), or a Plug.Conn.
@type t() :: %Lettermint.Webhook{ secret: (-> String.t()), tolerance: non_neg_integer() }
Functions
@spec new(String.t(), [{:tolerance, non_neg_integer()}]) :: t()
Creates a verifier for a signing secret.
Options: :tolerance, the maximum difference between the signed timestamp
and now, in seconds, in either direction (default 300; 0 accepts only the
current second). An empty secret or a negative tolerance raises
Lettermint.ConfigError.
@spec verify(t(), binary(), headers(), [{:now, integer()}]) :: {:ok, Lettermint.WebhookPayload.t()} | {:error, Lettermint.WebhookVerificationError.t()}
Verifies a delivery from its raw body and request headers, and returns the decoded payload.
Requires X-Lettermint-Signature (t=<unix>,v1=<hex>; any matching v1
is accepted) and X-Lettermint-Delivery, which must equal the signed
timestamp. Header names are case-insensitive.
Options: :now, the current time in Unix seconds (for tests).
@spec verify_signature(t(), binary(), String.t(), timestamp: String.t() | integer(), now: integer() ) :: {:ok, Lettermint.WebhookPayload.t()} | {:error, Lettermint.WebhookVerificationError.t()}
Verifies the raw body against an X-Lettermint-Signature value, for setups
where the headers are not at hand.
Options: :timestamp, the X-Lettermint-Delivery value, which must equal
the signed timestamp when given; :now, the current time in Unix seconds.