{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/node-sdk-webhook-signing","markdown_url":"https://test.abhinandan.one/node-sdk-webhook-signing.md","article":{"id":"e5aaa54e-6f73-4952-b96a-e3f383da2693","article_slug":"node-sdk-webhook-signing","parent_article_slug":null,"parent_article_title":null,"kind":"guide","published_at":"2026-08-11T18:54:51.370053+00:00","keywords":["verifyWebhookSignature","Primitive-Signature header","WebhookVerificationError","toleranceSeconds","@primitivedotdev/sdk/webhook","MyMX-Signature"],"meta_description":"Verify the Primitive-Signature HMAC-SHA256 header manually with verifyWebhookSignature when your framework doesn't expose a standard Request object.","og_image_url":null,"source_file_paths":["sdk-node/README.md"],"recording_id":null,"replayable":false,"task_name":"Webhook Signature Verification","category":"Node.js SDK","summary":null,"description":"Verify the Primitive-Signature HMAC header by hand with verifyWebhookSignature when your framework doesn't give you a standard Request object to pass to primitive.receive.","content_kind":"repo_page","content_markdown":"Use `verifyWebhookSignature` when you need to check a webhook delivery's authenticity yourself: a non-standard framework, a language bridge proxying through Node, or a one-off audit of a captured request. Most app code never needs this, because [`primitive.receive(...)`](node-sdk-receiving-email) already verifies the signature for you in one call.\n\n<Tip>\n\nIf your framework hands you a standard `Request` object (Next.js route handlers, Cloudflare Workers, Deno), use `primitive.receive(req, { secret })` instead. It extracts the raw body, verifies the signature, and returns a normalized [ReceivedEmail](email-model). Reach for `verifyWebhookSignature` only when you have already pulled the raw body and header value yourself.\n\n</Tip>\n\n## The wire format\n\nEvery webhook delivery carries a `Primitive-Signature` header holding a unix-seconds timestamp and a hex HMAC-SHA256 signature over `${timestamp}.${rawBody}`.\n\n```http\nPrimitive-Signature: t=<unix-seconds>,v1=<hex>\n```\n\n- **Signed string**: `${timestamp}.${rawBody}`, where `rawBody` is the exact request bytes before any JSON decoding.\n- **Signature**: HMAC-SHA256 of the signed string, hex-encoded.\n- **Secret**: returned by `GET /account/webhook-secret`. Use it as a UTF-8 string; do not base64-decode it, even though it looks base64-shaped.\n- **Tolerance**: reject deliveries whose `t=` timestamp is more than 5 minutes (300 seconds) off your wall clock. `verifyWebhookSignature` enforces this by default.\n\nA legacy `MyMX-Signature` header carries the same value for back-compat with integrations written before the rename. New code should read `Primitive-Signature`.\n\n<Note>\n\nThe signature is computed over the **raw** request body. If your framework re-serializes JSON before you see it (`JSON.parse` then `JSON.stringify`), the signature check fails even though the payload \"looks\" identical, because whitespace and key order differ from what was signed.\n\n</Note>\n\n## Verify a delivery manually\n\nCapture the raw body and the `Primitive-Signature` header value, pass both plus your webhook secret to `verifyWebhookSignature`, and treat a thrown `WebhookVerificationError` as a rejected delivery.\n\n<Steps>\n\n<Step title=\"Capture the raw body and signature header\">\n\nRead the request body as a string or `Buffer`, not as parsed JSON, and pull the `Primitive-Signature` header value verbatim.\n\n```ts\n// Your framework's raw-body accessor; must return the exact bytes.\nconst rawBody = await getRawRequestBody(req); // string | Buffer\nconst signatureHeader = req.headers[\"primitive-signature\"] as string;\n```\n\n</Step>\n\n<Step title=\"Call verifyWebhookSignature\">\n\nImport the helper from the `@primitivedotdev/sdk/webhook` subpath and pass the raw body, the header value, and your webhook secret.\n\n```ts\nimport { verifyWebhookSignature } from \"@primitivedotdev/sdk/webhook\";\n\nverifyWebhookSignature({\n  rawBody,\n  signatureHeader,\n  secret: process.env.PRIMITIVE_WEBHOOK_SECRET!,\n});\n```\n\n`rawBody` must be the exact bytes of the HTTP body (string or `Buffer`) before any JSON parsing. `signatureHeader` is the `Primitive-Signature` header value verbatim.\n\n</Step>\n\n<Step title=\"Handle the result\">\n\n`verifyWebhookSignature` returns nothing on success and throws `WebhookVerificationError` on mismatch, expired timestamp, or malformed input.\n\n```ts\nimport {\n  verifyWebhookSignature,\n  WebhookVerificationError,\n} from \"@primitivedotdev/sdk/webhook\";\n\ntry {\n  verifyWebhookSignature({\n    rawBody,\n    signatureHeader,\n    secret: process.env.PRIMITIVE_WEBHOOK_SECRET!,\n  });\n} catch (err) {\n  if (err instanceof WebhookVerificationError) {\n    // signature mismatch, expired timestamp, or malformed header\n    return new Response(\"invalid signature\", { status: 400 });\n  }\n  throw err;\n}\n```\n\n**Verification signal**: no exception thrown means the delivery is authentic and within the replay window. Proceed to parse the JSON body.\n\n</Step>\n\n</Steps>\n\n## Override the replay tolerance\n\nPass `toleranceSeconds` to change the replay window, which defaults to 300 seconds (5 minutes). Widen it only when your infrastructure adds queueing delay before the handler runs.\n\n```ts\nimport { verifyWebhookSignature } from \"@primitivedotdev/sdk/webhook\";\n\nverifyWebhookSignature({\n  rawBody,\n  signatureHeader,\n  secret: process.env.PRIMITIVE_WEBHOOK_SECRET!,\n  toleranceSeconds: 600,\n});\n```\n\n<Warning>\n\nWidening the tolerance window increases your exposure to replay attacks: a captured, valid request can be resent successfully for the full window. Only widen it if you accept that tradeoff.</br>\n\n</Warning>\n\n## Debugging a signature mismatch\n\nFour causes account for nearly every false mismatch: a re-serialized body, a base64-decoded secret, a stripped or altered header, and clock skew. Check them in that order.\n\nIf `verifyWebhookSignature` throws on a delivery you believe is genuine:\n\n1. **Re-serialized body.** Confirm you're verifying the exact raw bytes, not a value that passed through `JSON.parse`/`JSON.stringify` anywhere in your stack (a logging middleware is a common culprit).\n2. **Base64-decoded secret.** The secret returned by `GET /account/webhook-secret` looks base64-shaped but is not base64. Use it as-is, as a UTF-8 string.\n3. **Wrong header.** Confirm you're reading `primitive-signature` (case-insensitive) and not stripping or altering it in a proxy layer.\n4. **Clock skew.** If the timestamp is more than 5 minutes off your server's wall clock, verification fails even with a correct signature. Check NTP sync.\n\nFor the full wire-level reference (response codes, replay protection details), see the \"Webhook signing\" section of the [OpenAPI spec](https://api.primitive.dev/v1/openapi).\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Receiving Inbound Email\" href=\"node-sdk-receiving-email\">\n\nUse primitive.receive to verify and normalize a webhook delivery in one call: the recommended path for most app code.\n\n</Card>\n\n<Card title=\"Handling Payment and Interaction Webhook Events\" href=\"node-sdk-webhook-events\">\n\nUse handleWebhookEvent to verify and branch on email.*, payment.*, and interaction.x402.* deliveries from a single endpoint.\n\n</Card>\n\n<Card title=\"Standard Webhooks Signature Support\" href=\"node-sdk-standard-webhooks\">\n\nVerify deliveries using the webhook-id/webhook-timestamp/webhook-signature convention instead of the Primitive-Signature HMAC header.\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":"https://github.com/abhi-browzer/primitive-sdks/edit/main/sdk-node/README.md","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Webhook+Signature+Verification&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fnode-sdk-webhook-signing","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}