> ## Documentation Index
> Fetch the complete documentation index at: https://docs.elapse.finance/llms.txt
> Use this file to discover all available pages before exploring further.

# Signatures

> How every delivery is signed, how the SDK verifies it, and a vector to check by hand.

Every delivery carries an `X-Elapse-Signature` header:

```text theme={"system"}
X-Elapse-Signature: t=1757160000,v1=5f59b985…
```

* `t` is the Unix time the delivery was signed.
* `v1` is `HMAC-SHA256(secret, "{t}.{raw_body}")`, hex encoded. There may be more than one `v1` during a secret roll; any one matching is enough.
* The secret is the endpoint's `whsec_…`, shown once when you create the endpoint in the dashboard, or printed by `elapse listen`.

Verification rejects a header older than **300 seconds**, and compares in constant time. Sign the **raw body bytes**: a body re-serialised by a JSON middleware will not match.

## With the SDK

<CodeGroup>
  ```ts TypeScript theme={"system"}
  import { constructEvent } from "@elapse/sdk";

  // Node types a header as `string | string[] | undefined`, so narrow it first.
  const sent = req.headers["x-elapse-signature"];
  const signature = Array.isArray(sent) ? sent[0] : sent;

  const event = constructEvent(rawBody, signature, process.env.ELAPSE_WEBHOOK_SECRET);
  // Throws ElapseSignatureVerificationError on a missing, malformed, expired or mismatched signature.
  ```

  ```bash cURL theme={"system"}
  # Recompute v1 from the same inputs and compare with the header (see the vector below).
  printf '%s.%s' "$T" "$RAW_BODY" | openssl dgst -sha256 -hmac "$ELAPSE_WEBHOOK_SECRET"
  ```
</CodeGroup>

`constructEvent` accepts a string or a byte buffer, one secret or an array of them for a roll, and returns the parsed event only after the signature passes. Two frameworks that eat the raw body by default:

<CodeGroup>
  ```ts Express theme={"system"}
  app.post("/webhooks", express.raw({ type: "*/*" }), (req, res) => {
    const sent = req.headers["x-elapse-signature"];
    const event = constructEvent(req.body, Array.isArray(sent) ? sent[0] : sent, secret);
    res.sendStatus(200);
  });
  ```

  ```ts Hono theme={"system"}
  app.post("/webhooks", async (c) => {
    // Hono's `header()` already returns `string | undefined`, so there is nothing to narrow.
    const event = constructEvent(await c.req.text(), c.req.header("x-elapse-signature"), secret);
    return c.json({ received: true });
  });
  ```

  ```bash cURL theme={"system"}
  # Frameworks differ; the rule is the same everywhere: hand constructEvent the bytes you received.
  ```
</CodeGroup>

## Verify by hand

This vector is checked by the SDK's own test suite, so it cannot go stale.

|        |                                                                                    |
| ------ | ---------------------------------------------------------------------------------- |
| Secret | `whsec_docs_vector_0000000000000000`                                               |
| `t`    | `1757160000`                                                                       |
| Header | `t=1757160000,v1=5f59b98549936d4e1ef661a3ef806225f1737731c2a0ea2bd55250a8b04e4deb` |

```json raw body, exactly these bytes theme={"system"}
{"id":"evt_test_vector","object":"event","type":"subscription.canceled","created":1757160000,"livemode":false,"pending_webhooks":1,"data":{"object":{"id":"sub_test00000000000","object":"subscription","status":"canceled","seconds_elapsed":83,"amount_settled":"0.332"}}}
```

<CodeGroup>
  ```ts Node theme={"system"}
  import { createHmac } from "node:crypto";
  const v1 = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  ```

  ```bash openssl theme={"system"}
  printf '%s.%s' "1757160000" "$RAW_BODY" \
    | openssl dgst -sha256 -hmac "whsec_docs_vector_0000000000000000"
  ```
</CodeGroup>

Both print the `v1` above.

## Rolling a secret

Roll from the dashboard with a grace period of 0, 1 or 24 hours. During the grace period deliveries carry two `v1` values, one per secret, so pass both secrets to `constructEvent` and swap the old one out when the window closes.
