{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"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","article":{"id":"ddc0db75-28b3-493b-ac4d-84d4f1cbfbc9","article_slug":"node-sdk-standard-webhooks","parent_article_slug":"node-sdk-webhook-signing","parent_article_title":"Webhook Signature Verification","kind":"guide","published_at":"2026-08-11T18:55:00.392589+00:00","keywords":["verifyStandardWebhooksSignature","signStandardWebhooksPayload","webhook-signature header","whsec_ secret","webhook-id webhook-timestamp","Standard Webhooks Node SDK"],"meta_description":"verifyStandardWebhooksSignature checks the webhook-id, webhook-timestamp, and webhook-signature headers against a whsec_-prefixed secret in the Node.js SDK.","og_image_url":null,"source_file_paths":[],"recording_id":null,"replayable":false,"task_name":"Standard Webhooks Signature Support","category":"Node.js SDK","summary":null,"description":"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.","content_kind":"repo_page","content_markdown":"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](node-sdk-webhook-signing), and `primitive.receive(...)` / `handleWebhook(...)` verify that default automatically for you.\n\nReach 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](webhook-events).\n\n## Verify a Standard Webhooks delivery\n\n`verifyStandardWebhooksSignature`, exported from `@primitivedotdev/sdk/webhook`, checks a delivery's `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers against your secret.\n\n<Steps>\n\n<Step title=\"Import the helper\">\n\n```typescript\nimport { verifyStandardWebhooksSignature } from \"@primitivedotdev/sdk/webhook\";\n```\n\n</Step>\n\n<Step title=\"Read the raw body and the three headers\">\n\nThe 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.\n\n```typescript\nconst rawBody = await request.text(); // exact bytes, not a re-serialized parse\nconst msgId = request.headers.get(\"webhook-id\")!;\nconst timestamp = request.headers.get(\"webhook-timestamp\")!;\nconst signatureHeader = request.headers.get(\"webhook-signature\")!;\n```\n\n</Step>\n\n<Step title=\"Verify against your whsec_ secret\">\n\n```typescript\nverifyStandardWebhooksSignature({\n  rawBody,\n  msgId,\n  timestamp,\n  signatureHeader,\n  secret: process.env.PRIMITIVE_WEBHOOK_SECRET!, // whsec_...\n});\n```\n\nExpected result: the call returns without throwing when the signature is valid. Any mismatch, expired timestamp, or malformed header throws `WebhookVerificationError`.\n\n</Step>\n\n</Steps>\n\n```typescript\nimport { verifyStandardWebhooksSignature, WebhookVerificationError } from \"@primitivedotdev/sdk/webhook\";\n\nexport async function POST(req: Request) {\n  const rawBody = await req.text();\n\n  try {\n    verifyStandardWebhooksSignature({\n      rawBody,\n      msgId: req.headers.get(\"webhook-id\")!,\n      timestamp: req.headers.get(\"webhook-timestamp\")!,\n      signatureHeader: req.headers.get(\"webhook-signature\")!,\n      secret: process.env.PRIMITIVE_WEBHOOK_SECRET!,\n    });\n  } catch (err) {\n    if (err instanceof WebhookVerificationError) {\n      return new Response(err.code, { status: 400 });\n    }\n    throw err;\n  }\n\n  return Response.json({ ok: true });\n}\n```\n\n<Tip>\n\nPass `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`.\n\n</Tip>\n\n## Secret format\n\nA 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.\n\n<Warning>\n\nTreat 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.\n\n</Warning>\n\n## Sign a payload yourself\n\nIf you need to emit a Standard Webhooks-formatted signature (for example, replaying a fixture in tests), use `signStandardWebhooksPayload`:\n\n```typescript\nimport { signStandardWebhooksPayload } from \"@primitivedotdev/sdk/webhook\";\n\nconst { signature, msgId, timestamp } = signStandardWebhooksPayload(\n  rawBodyString,\n  process.env.PRIMITIVE_WEBHOOK_SECRET!, // whsec_...\n  \"msg_test_123\", // your own message id\n);\n```\n\nSend `signature` as the `webhook-signature` header, `msgId` as `webhook-id`, and `timestamp` as `webhook-timestamp` on the outbound request.\n\n## Choosing between the two schemes\n\n| | Primitive-Signature (default) | Standard Webhooks |\n|---|---|---|\n| Header(s) | `Primitive-Signature: t=<unix-seconds>,v1=<hex>` | `webhook-id`, `webhook-timestamp`, `webhook-signature` |\n| Secret handling | used verbatim as a UTF-8 string | base64-decoded, optional `whsec_` prefix stripped first |\n| Verified automatically by | `primitive.receive(...)`, `handleWebhook(...)`, `handleWebhookEvent(...)` | `verifyStandardWebhooksSignature` (call it yourself) |\n| When to use | Everything by default | Interop with tooling that expects the Standard Webhooks convention |\n\nBoth schemes verify the exact raw request bytes and default to a 300-second replay window.\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Webhook Signature Verification\" href=\"node-sdk-webhook-signing\">\n\nVerify the default Primitive-Signature HMAC header manually with verifyWebhookSignature.\n\n</Card>\n\n<Card title=\"Handling Payment and Interaction Webhook Events\" href=\"node-sdk-webhook-events\">\n\nUse handleWebhookEvent and typed event guards to branch on email.*, payment.*, and interaction.x402.* deliveries.\n\n</Card>\n\n<Card title=\"Webhook Events Overview\" href=\"webhook-events\">\n\nUnderstand the shared webhook contract: signature verification, event catalog, and forward-compatibility guarantees.\n\n</Card>\n\n<Card title=\"Node.js SDK Errors\" href=\"node-sdk-errors\">\n\nLook up WebhookVerificationError codes and what triggers each one.\n\n</Card>\n\n</CardGroup>","canonical_base_url":"https://test.abhinandan.one","seo_indexing_enabled":true,"last_modified":"2026-08-21T18:22:43.359885+00:00","video_url":null,"voiceover_url":null,"tools_used":[],"demonstrated_by":[],"steps":[],"related_links":[],"intro":null,"prerequisites":[],"verification":[],"troubleshooting":[],"suggest_edit_url":null,"raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Standard+Webhooks+Signature+Support&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fnode-sdk-standard-webhooks","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}