Ramp.Webhooks (ramp v1.0.0)

Copy Markdown View Source

Verifies Ramp webhook signatures and decodes webhook payloads into Ramp.Webhooks.Event structs.

Ramp signs each webhook delivery with HMAC-SHA256 over the raw request body, sent in the X-Webhook-Signature header, alongside a timestamp in X-Webhook-Timestamp that lets you additionally guard against replay of captured deliveries.

Always verify against the raw, unparsed request body -- reserializing JSON can change byte-for-byte formatting and break the signature check. In a Phoenix app, read the raw body before your JSON parser plug runs (e.g. with a custom Plug.Parsers body reader), or read it directly with Plug.Conn.read_body/2 in a plug placed ahead of Plug.Parsers.

Example (Plug/Phoenix controller)

def webhook(conn, _params) do
  {:ok, raw_body, conn} = Plug.Conn.read_body(conn)
  signature = conn |> Plug.Conn.get_req_header("x-webhook-signature") |> List.first()
  timestamp = conn |> Plug.Conn.get_req_header("x-webhook-timestamp") |> List.first()

  case Ramp.Webhooks.construct_event(raw_body, signature, timestamp, @webhook_secret) do
    {:ok, event} ->
      MyApp.WebhookProcessor.handle(event)
      send_resp(conn, 200, "ok")

    {:error, %Ramp.Error{}} ->
      send_resp(conn, 400, "invalid signature")
  end
end

Summary

Functions

Verifies the signature (and optional timestamp), then decodes raw_body into a Ramp.Webhooks.Event on success.

Verifies an HMAC-SHA256 webhook signature using a constant-time comparison (:crypto.hash_equals/2).

Verifies both the signature and, if timestamp is given, that the delivery falls within tolerance_seconds (default 300) of now -- protecting against replay of a captured payload + signature.

Functions

construct_event(raw_body, signature, timestamp \\ nil, secret, opts \\ [])

@spec construct_event(binary(), String.t(), String.t() | nil, String.t(), keyword()) ::
  {:ok, Ramp.Webhooks.Event.t()} | {:error, Ramp.Error.t()}

Verifies the signature (and optional timestamp), then decodes raw_body into a Ramp.Webhooks.Event on success.

Options

  • :tolerance_seconds - overrides the default replay-protection window

verify(raw_body, signature, secret)

@spec verify(binary(), String.t(), String.t()) :: :ok | {:error, Ramp.Error.t()}

Verifies an HMAC-SHA256 webhook signature using a constant-time comparison (:crypto.hash_equals/2).

signature is the raw value of the X-Webhook-Signature header (either the bare hex digest, or one prefixed with sha256= -- both are accepted).

verify(raw_body, signature, timestamp, secret, tolerance_seconds \\ 300)

@spec verify(binary(), String.t(), String.t() | nil, String.t(), non_neg_integer()) ::
  :ok | {:error, Ramp.Error.t()}

Verifies both the signature and, if timestamp is given, that the delivery falls within tolerance_seconds (default 300) of now -- protecting against replay of a captured payload + signature.

timestamp is the raw value of the X-Webhook-Timestamp header (Unix seconds, as a string). Pass nil to skip the freshness check.