# `PolarExpress.Webhook`
[🔗](https://github.com/jeffhuen/polar_express/blob/main/lib/polar_express/webhook.ex#L1)

Webhook signature verification and event construction.

Polar uses the [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks)
specification. Each webhook request includes three headers:

  * `webhook-id` — unique message identifier
  * `webhook-timestamp` — Unix epoch seconds
  * `webhook-signature` — `v1,<base64>` HMAC-SHA256 signature

## Usage

    headers = %{
      "webhook-id" => msg_id,
      "webhook-timestamp" => timestamp,
      "webhook-signature" => signature
    }

    case PolarExpress.Webhook.construct_event(payload, headers, secret) do
      {:ok, event} -> handle_event(event)
      {:error, error} -> send_resp(conn, 400, error.message)
    end

Most users should use `PolarExpress.WebhookPlug` instead, which handles all
of this automatically.

# `headers`

```elixir
@type headers() :: %{required(String.t()) =&gt; String.t()}
```

Standard Webhooks headers sent by Polar with every webhook request.

  * `"webhook-id"` — unique message identifier (used in signature and for deduplication)
  * `"webhook-timestamp"` — Unix epoch seconds as a string
  * `"webhook-signature"` — one or more `v1,<base64>` HMAC-SHA256 signatures (space-separated)

# `verify_opts`

```elixir
@type verify_opts() :: [{:tolerance, pos_integer()}]
```

Options for webhook verification.

# `compute_signature`

```elixir
@spec compute_signature(String.t(), integer(), binary(), String.t()) :: String.t()
```

Compute the expected Base64-encoded HMAC-SHA256 signature for a payload.

The signed content follows the Standard Webhooks format:

    "{msg_id}.{timestamp}.{payload}"

The secret is used as raw bytes (Polar-specific — they pass the full secret
string including the `whsec_` prefix as the HMAC key, rather than
Base64-decoding it as the Standard Webhooks spec suggests).

# `construct_event`

```elixir
@spec construct_event(binary(), headers(), String.t(), verify_opts()) ::
  {:ok, PolarExpress.Resources.Event.t()} | {:error, PolarExpress.Error.t()}
```

Verify a webhook signature and construct a typed event struct.

Parses the JSON payload, verifies the HMAC-SHA256 signature against the
Standard Webhooks headers, and deserializes the event data into the
appropriate schema struct (e.g. `PolarExpress.Schemas.Order` for `"order.paid"`).
Unknown event types are returned with raw map data.

## Parameters

  * `payload` - Raw request body (binary string, NOT parsed JSON)
  * `headers` - Standard Webhooks headers (see `t:headers/0`)
  * `secret` - Webhook endpoint signing secret (`whsec_...`)
  * `opts` - See `t:verify_opts/0`

## Examples

    headers = %{
      "webhook-id" => "msg_2Lml0nCjGr...",
      "webhook-timestamp" => "1714000000",
      "webhook-signature" => "v1,K7rRz..."
    }

    case PolarExpress.Webhook.construct_event(raw_body, headers, secret) do
      {:ok, %PolarExpress.Resources.Event{type: "order.paid", data: order}} ->
        fulfill_order(order)

      {:error, %PolarExpress.Error{message: msg}} ->
        Logger.warning("Webhook rejected: #{msg}")
    end

# `verify_headers`

```elixir
@spec verify_headers(binary(), headers(), String.t(), verify_opts()) ::
  :ok | {:error, PolarExpress.Error.t()}
```

Verify Standard Webhooks headers without constructing the event.

Checks the required headers are present, the timestamp is within the
tolerance window (bidirectional — rejects both stale and future-dated
events), and at least one `v1` signature matches the expected
HMAC-SHA256 using constant-time comparison.

## Parameters

  * `payload` - Raw request body (binary string)
  * `headers` - Standard Webhooks headers (see `t:headers/0`)
  * `secret` - Webhook endpoint signing secret (`whsec_...`)
  * `opts` - See `t:verify_opts/0`

---

*Consult [api-reference.md](api-reference.md) for complete listing*
