{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/node-sdk-webhook-events","markdown_url":"https://test.abhinandan.one/node-sdk-webhook-events.md","article":{"id":"468fae7c-a6fa-4cf8-aea3-0296c67a273c","article_slug":"node-sdk-webhook-events","parent_article_slug":null,"parent_article_title":null,"kind":"guide","published_at":"2026-08-11T18:54:50.636551+00:00","keywords":["handleWebhookEvent","isPaymentSettledEvent","isInteractionX402Event","X-Webhook-Event header","WEBHOOK_EVENT_TYPES","UnknownEvent"],"meta_description":"handleWebhookEvent verifies the Primitive-Signature HMAC once and returns a typed union of email.*, payment.*, and interaction.x402.* webhook events.","og_image_url":null,"source_file_paths":["sdk-node/src/webhook/index.ts"],"recording_id":null,"replayable":false,"task_name":"Handling Payment and Interaction Webhook Events","category":"Node.js SDK","summary":null,"description":"Verify and branch on every webhook family, email.*, payment.*, and interaction.x402.*, from a single endpoint using handleWebhookEvent and its typed event guards.","content_kind":"repo_page","content_markdown":"Use `handleWebhookEvent`, the verify-then-classify entry point in `@primitivedotdev/sdk/webhook`, to handle `email.*`, `payment.*`, and `interaction.x402.*` deliveries from a single endpoint. Reach for it when the same webhook URL you registered for inbound mail also receives x402 settlement and interaction events.\n\n<Note>\n\nIf your integration only ever needs inbound mail, use `primitive.receive(...)` or `handleWebhook(...)` instead, see [Receiving Inbound Email](node-sdk-receiving-email). Reach for `handleWebhookEvent` the moment you also care about payments or interactions.\n\n</Note>\n\n## Why the header matters\n\nThe `X-Webhook-Event` header is the only discriminator present on every webhook family, so it is what the SDK keys on. Primitive names every delivery in that header, not in the body. The stored body is sent verbatim with no shared envelope, so its shape differs by family:\n\n| Family | Discriminator location | Example |\n| --- | --- | --- |\n| `email.*` | body field `event` | `{ \"event\": \"email.received\", ... }` |\n| `payment.*` | body field `type` | `{ \"type\": \"payment.settled\", ... }` |\n| `interaction.*` | none, body has no event/type field | `{ \"interaction\": { ... } }` |\n\nBecause `interaction.*` bodies carry no discriminator at all, the header is the only reliable signal across every family. `handleWebhookEvent` reads it for you and overlays a canonical `event` field onto the parsed result so your code always branches on one field, regardless of family.\n\n## Verify and classify in one call\n\n`handleWebhookEvent` verifies the `Primitive-Signature` HMAC over the raw body first (this step is identical to [webhook signature verification](node-sdk-webhook-signing) and independent of the event family), then parses the JSON and classifies it on the `X-Webhook-Event` header.\n\n<Steps>\n\n<Step title=\"Import the handler and type guards\">\n\n```typescript\nimport {\n  handleWebhookEvent,\n  isPaymentSettledEvent,\n  isInteractionX402Event,\n} from \"@primitivedotdev/sdk/webhook\";\n```\n\n</Step>\n\n<Step title=\"Call handleWebhookEvent with the raw body, headers, and secret\">\n\n```typescript\nconst event = handleWebhookEvent({\n  body: rawBodyString,\n  headers: req.headers,\n  secret: process.env.PRIMITIVE_WEBHOOK_SECRET!,\n});\n```\n\n`body` must be the exact request bytes before any JSON parsing. `headers` accepts a plain object (Express `req.headers`) or a Fetch API `Headers` instance. `secret` is your account's webhook secret, returned by `GET /account/webhook-secret`.\n\n</Step>\n\n<Step title=\"Branch on the typed event\">\n\n```typescript\nif (isPaymentSettledEvent(event)) {\n  // typed PaymentSettledEvent: flat fields, amount in token base units\n  console.log(\"settled\", event.challenge_id, event.amount, event.settle_tx);\n} else if (isInteractionX402Event(event)) {\n  // typed interaction.x402.* event (challenge/payment/settled/...)\n  console.log(event.event, event.interaction);\n} else if (event.event === \"email.received\") {\n  // fully typed EmailReceivedEvent — see the email-model docs\n  console.log(event.email.headers.subject);\n} else {\n  // UnknownEvent: a future event type this SDK version doesn't know about yet\n  console.log(\"unhandled event:\", event.event);\n}\n```\n\n</Step>\n\n</Steps>\n\n**Expected result**: for a known event type, `event` is a typed value matching that family's shape; for anything the current SDK doesn't recognize, `event` is an `UnknownEvent` with `event` and the raw payload preserved, the call never throws for an unrecognized (but well-signed) event type.\n\n## The event catalog\n\nThe SDK classifies 19 header values across four families, exported as the `WEBHOOK_EVENT_TYPES` array and the `WebhookEventType` union:\n\n```typescript\nimport { WEBHOOK_EVENT_TYPES } from \"@primitivedotdev/sdk/webhook\";\n```\n\n| Family | Event names |\n| --- | --- |\n| Email | `email.received`, `email.bounced`, `email.tls_report`, `email.dmarc_report`, `email.dmarc_failure` |\n| Payment | `payment.settled`, `payment.failed` |\n| Interaction (x402) | `interaction.x402.challenge`, `interaction.x402.payment`, `interaction.x402.settled`, `interaction.x402.rejected`, `interaction.x402.declined`, `interaction.x402.expired`, `interaction.x402.verify_timeout` |\n| Interaction (ack) | `interaction.ack.received`, `interaction.ack.requested`, `interaction.ack.acked`, `interaction.ack.canceled`, `interaction.ack.expired` |\n\nOnly `email.received` is validated against the canonical JSON Schema and returned as a fully typed `EmailReceivedEvent`. Every other known type is returned as its body plus a canonical `event` field overlaid from the header. See [Webhook Events Overview](webhook-events) for the cross-SDK contract and forward-compatibility guarantees this catalog is part of.\n\n## Typed shapes for payment and interaction events\n\n`PaymentEvent` (and its narrower `PaymentSettledEvent` / `PaymentFailedEvent` variants) carries flat fields, no nested payment object:\n\n```typescript\nimport type { PaymentEvent, PaymentSettledEvent, PaymentFailedEvent } from \"@primitivedotdev/sdk/webhook\";\n```\n\n- `challenge_id`, the [x402 payment challenge](x402-payments-overview) this event settles or fails\n- `amount`, token base units (USDC has 6 decimals, so `\"10000\"` is 0.01 USDC)\n- `settle_tx`, present on `payment.settled`, the on-chain settlement transaction hash\n- `failure_reason`, present on `payment.failed`\n\n`InteractionX402Event` wraps the raw `{ interaction: {...} }` body plus the canonical `event` name from the header:\n\n```typescript\nimport type { InteractionX402Event } from \"@primitivedotdev/sdk/webhook\";\n```\n\nUse `isPaymentEvent`, `isPaymentSettledEvent`, `isPaymentFailedEvent`, and `isInteractionX402Event` as type guards to narrow `WebhookEvent` without manually checking `event` strings.\n\n## handleWebhookEvent vs. handleWebhook\n\n`handleWebhook` remains hard-typed to `email.received` for backward compatibility, it throws on anything else. `handleWebhookEvent` is the superset: same signature verification, but it returns the full `WebhookEvent` union instead of throwing on a payment or interaction delivery.\n\n<Tip>\n\nIf your handler is already committed to `email.received` only and never expects payment or interaction traffic on that endpoint, `handleWebhook` (used by `primitive.receive(...)`) is simpler and still correct. Switch to `handleWebhookEvent` the moment you register the same endpoint for x402 payments.\n\n</Tip>\n\n<Warning>\n\nDon't try to discriminate on a body field yourself. `interaction.*` bodies have no `event` or `type` field at all, code that reads `body.event` will silently misclassify or crash on those deliveries. Always let `handleWebhookEvent` read the `X-Webhook-Event` header for you.\n\n</Warning>\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"x402 Payments Overview\" href=\"x402-payments-overview\">\n\nUnderstand the challenge/pay/settle model that produces payment.* and interaction.x402.* events.\n\n</Card>\n\n<Card title=\"Webhook Signature Verification\" href=\"node-sdk-webhook-signing\">\n\nVerify the Primitive-Signature HMAC header manually when you don't have a standard Request object.\n\n</Card>\n\n<Card title=\"Node.js SDK Errors\" href=\"node-sdk-errors\">\n\nLook up WebhookVerificationError, WebhookValidationError, and WebhookPayloadError codes.\n\n</Card>\n\n<Card title=\"Webhook Events Overview\" href=\"webhook-events\">\n\nSee the shared event catalog and forward-compatibility contract across every SDK.\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/src/webhook/index.ts","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Handling+Payment+and+Interaction+Webhook+Events&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fnode-sdk-webhook-events","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}