Documentation Index: Fetch llms.txt first to discover every published page. This page is also available as Markdown at /node-sdk-webhook-signing/node-sdk-standard-webhooks.md.
Verified · 8/11/2026

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 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, 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.

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. 1

    Import the helper#

    import { verifyStandardWebhooksSignature } from "@primitivedotdev/sdk/webhook";
    
  2. 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.

    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. 3

    Verify against your whsec_ secret#

    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.

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:

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 handlingused verbatim as a UTF-8 stringbase64-decoded, optional whsec_ prefix stripped first
Verified automatically byprimitive.receive(...), handleWebhook(...), handleWebhookEvent(...)verifyStandardWebhooksSignature (call it yourself)
When to useEverything by defaultInterop with tooling that expects the Standard Webhooks convention

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

Next steps#

Was this page helpful?

© Primitive SDKs

Powered by Browzer