---
title: "Standard Webhooks Signature Support"
canonical: "https://test.abhinandan.one/node-sdk-webhook-signing/node-sdk-standard-webhooks"
markdown_url: "https://test.abhinandan.one/node-sdk-webhook-signing/node-sdk-standard-webhooks.md"
publisher: "Primitive SDKs"
kind: "guide"
content_type: "reference"
category: "Node.js SDK"
parent: "node-sdk-webhook-signing"
description: "verifyStandardWebhooksSignature checks the webhook-id, webhook-timestamp, and webhook-signature headers against a whsec_-prefixed secret in the Node.js SDK."
keywords: ["verifyStandardWebhooksSignature", "signStandardWebhooksPayload", "webhook-signature header", "whsec_ secret", "webhook-id webhook-timestamp", "Standard Webhooks Node SDK"]
last_modified: "2026-08-11T18:55:00.563276+00:00"
published_at: "2026-08-11T18:55:00.392589+00:00"
sections:
  - {anchor: "verify-a-standard-webhooks-delivery", title: "Verify a Standard Webhooks delivery"}
  - {anchor: "step-import-the-helper", title: "Import the helper"}
  - {anchor: "step-read-the-raw-body-and-the-three-headers", title: "Read the raw body and the three headers"}
  - {anchor: "step-verify-against-your-whsec_-secret", title: "Verify against your whsec_ secret"}
  - {anchor: "secret-format", title: "Secret format"}
  - {anchor: "sign-a-payload-yourself", title: "Sign a payload yourself"}
  - {anchor: "choosing-between-the-two-schemes", title: "Choosing between the two schemes"}
  - {anchor: "next-steps", title: "Next steps"}
---

> Documentation index: https://test.abhinandan.one/llms.txt

# Standard Webhooks Signature Support

Verify or sign Primitive webhook deliveries using the Standard Webhooks convention (webhook-id/webhook-timestamp/webhook-signature) instead of the default Primitive-Signature HMAC header.

Use Standard Webhooks signature support when a receiving system already expects the [Standard Webhooks](https://www.standardwebhooks.com) convention (`webhook-id` / `webhook-timestamp` / `webhook-signature` headers, `whsec_`-prefixed secret) instead of Primitive's own scheme. This is the alternative path; the default is the `Primitive-Signature` HMAC header described in [Webhook Signature Verification](https://test.abhinandan.one/node-sdk-webhook-signing.md), and `primitive.receive(...)` / `handleWebhook(...)` verify that default automatically for you.

Reach for this page only if you're integrating with tooling built around Standard Webhooks, or you need to sign outbound deliveries in that format yourself. For the general webhook contract (event catalog, `X-Webhook-Event` header, forward compatibility), see [Webhook Events Overview](https://test.abhinandan.one/webhook-events.md).

## Verify a Standard Webhooks delivery

`verifyStandardWebhooksSignature`, exported from `@primitivedotdev/sdk/webhook`, checks a delivery's `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers against your secret.

### 1. Import the helper

```typescript
import { verifyStandardWebhooksSignature } from "@primitivedotdev/sdk/webhook";
```

### 2. Read the raw body and the three headers

The signature is computed over the exact request bytes, so read the raw body before any JSON parsing, the same requirement as the Primitive-Signature scheme.

```typescript
const rawBody = await request.text(); // exact bytes, not a re-serialized parse
const msgId = request.headers.get("webhook-id")!;
const timestamp = request.headers.get("webhook-timestamp")!;
const signatureHeader = request.headers.get("webhook-signature")!;
```

### 3. Verify against your whsec_ secret

```typescript
verifyStandardWebhooksSignature({
  rawBody,
  msgId,
  timestamp,
  signatureHeader,
  secret: process.env.PRIMITIVE_WEBHOOK_SECRET!, // whsec_...
});
```

Expected result: the call returns without throwing when the signature is valid. Any mismatch, expired timestamp, or malformed header throws `WebhookVerificationError`.

```typescript
import { verifyStandardWebhooksSignature, WebhookVerificationError } from "@primitivedotdev/sdk/webhook";

export async function POST(req: Request) {
  const rawBody = await req.text();

  try {
    verifyStandardWebhooksSignature({
      rawBody,
      msgId: req.headers.get("webhook-id")!,
      timestamp: req.headers.get("webhook-timestamp")!,
      signatureHeader: req.headers.get("webhook-signature")!,
      secret: process.env.PRIMITIVE_WEBHOOK_SECRET!,
    });
  } catch (err) {
    if (err instanceof WebhookVerificationError) {
      return new Response(err.code, { status: 400 });
    }
    throw err;
  }

  return Response.json({ ok: true });
}
```

> **Tip:** Pass `toleranceSeconds` to change the replay window; it defaults to 300 seconds (5 minutes), the same window the default `Primitive-Signature` scheme uses. A `webhook-timestamp` older than that throws `WebhookVerificationError` with code `TIMESTAMP_OUT_OF_RANGE`.

## Secret format

A Standard Webhooks secret is base64-encoded and may carry a `whsec_` prefix. `verifyStandardWebhooksSignature` accepts it with or without the prefix and does the base64 decode for you, so pass it exactly as issued and do not decode it yourself. A secret that isn't base64 after the prefix is stripped throws `WebhookVerificationError` with code `MISSING_SECRET`. This differs from the default `Primitive-Signature` scheme, whose secret (from `GET /account/webhook-secret`) is used verbatim as a UTF-8 string and must never be base64-decoded.

> **Warning:** Treat a delivery carrying `webhook-signature` but missing `webhook-id` or `webhook-timestamp` as a misconfiguration, not a fallback case. All three headers are required together; a partial set throws rather than silently degrading.

## Sign a payload yourself

If you need to emit a Standard Webhooks-formatted signature (for example, replaying a fixture in tests), use `signStandardWebhooksPayload`:

```typescript
import { signStandardWebhooksPayload } from "@primitivedotdev/sdk/webhook";

const { signature, msgId, timestamp } = signStandardWebhooksPayload(
  rawBodyString,
  process.env.PRIMITIVE_WEBHOOK_SECRET!, // whsec_...
  "msg_test_123", // your own message id
);
```

Send `signature` as the `webhook-signature` header, `msgId` as `webhook-id`, and `timestamp` as `webhook-timestamp` on the outbound request.

## Choosing between the two schemes

| | Primitive-Signature (default) | Standard Webhooks |
|---|---|---|
| Header(s) | `Primitive-Signature: t=<unix-seconds>,v1=<hex>` | `webhook-id`, `webhook-timestamp`, `webhook-signature` |
| Secret handling | used verbatim as a UTF-8 string | base64-decoded, optional `whsec_` prefix stripped first |
| Verified automatically by | `primitive.receive(...)`, `handleWebhook(...)`, `handleWebhookEvent(...)` | `verifyStandardWebhooksSignature` (call it yourself) |
| When to use | Everything by default | Interop with tooling that expects the Standard Webhooks convention |

Both schemes verify the exact raw request bytes and default to a 300-second replay window.
