{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/webhook-events","markdown_url":"https://test.abhinandan.one/webhook-events.md","article":{"id":"a6646c7d-9421-40e0-86bc-f2cd02b48b3a","article_slug":"webhook-events","parent_article_slug":null,"parent_article_title":null,"kind":"concept","published_at":"2026-08-11T18:18:13.025192+00:00","keywords":["X-Webhook-Event header","Primitive-Signature","handleWebhookEvent","WEBHOOK_EVENT_TYPES","UnknownEvent","interaction.x402.settled"],"meta_description":"The X-Webhook-Event header names every webhook delivery's event type, and one HMAC signature scheme verifies email, payment, and interaction bodies alike.","og_image_url":null,"source_file_paths":["docs/architecture.md","openapi/primitive-api.yaml"],"recording_id":null,"replayable":false,"task_name":"Webhook Events Overview","category":"Core Concepts","summary":null,"description":"Every Primitive webhook delivery shares one signature scheme and one event-type discriminator across email, payment, and interaction families, and every SDK exposes the same typed union with built-in forward compatibility.","content_kind":"repo_page","content_markdown":"A Primitive webhook delivery is an HTTP POST to your endpoint, signed with one HMAC scheme and labeled with one header, regardless of whether it carries an inbound email, a payment settlement, or an x402-over-email interaction step. Every SDK (Node, Python, Go) parses that delivery into the same typed event union, so the contract in this page is identical no matter which language you're integrating.\n\n## The event name lives in a header, not the body\n\nEvery webhook family sends its body **verbatim, with no shared envelope**. That means the field carrying the event name is different per family:\n\n| Family | Body shape | Event name field |\n|---|---|---|\n| `email.*` | Full `EmailReceivedEvent`-style object | top-level `event` field |\n| `payment.*` | Flat object | `type` field |\n| `interaction.*` | `{ \"interaction\": { ... } }` | no field at all |\n\nBecause the body alone can't reliably discriminate every family, Primitive sends the canonical event name on the **`X-Webhook-Event` header** on every delivery, for every family. This header is the primary discriminator every SDK's parser keys on. A top-level `event` string in an `email.*` body is only a backward-compat fallback for senders that don't set the header.\n\n```http\nX-Webhook-Event: payment.settled\n```\n\nIf a delivery has neither the header nor a body `event` field, the SDKs raise a payload error (`PAYLOAD_MISSING_EVENT`) rather than guessing.\n\n## Signature verification is one scheme for every family\n\nEvery delivery, `email.*`, `payment.*`, `interaction.*` alike, is signed the same way, over the **raw request body**, independent of event type. The default scheme:\n\n```http\nPrimitive-Signature: t=<unix-seconds>,v1=<hex>\n```\n\n- **Signed string**: `${timestamp}.${rawBody}`, where `rawBody` is the exact HTTP body bytes before any JSON parsing.\n- **Signature**: HMAC-SHA256 of the signed string, hex-encoded, keyed with your account's webhook secret (from `GET /account/webhook-secret`) used as a UTF-8 string, do not base64-decode it despite its base64-shaped appearance.\n- **Tolerance**: reject any delivery whose `t=` is more than 5 minutes off your wall clock; each SDK's verifier enforces this by default.\n- **Legacy headers**: each delivery is signed once, and the same `t=...,v1=...` value is sent on `X-Primitive-Signature` and the legacy `X-Webhook-Signature` header alongside the primary `Primitive-Signature`. A legacy `MyMX-Signature` header carries the same value too, for integrations written before the rename.\n\nBecause verification runs on the raw body independent of event type, the exact same verifier code path handles `email.received`, `payment.settled`, and `interaction.x402.payment` deliveries, there's no per-family signature variant.\n\n<Note>\n\nNeed Standard Webhooks (`webhook-id`/`webhook-timestamp`/`webhook-signature`, `whsec_`-prefixed secret) instead of the Primitive-Signature HMAC? That's an alternative scheme offered alongside this default, see [Standard Webhooks Signature Support](node-sdk-standard-webhooks) (Node), [Python](python-standard-webhooks), or [Go](go-standard-webhooks).\n\n</Note>\n\n## The full event catalog\n\nEvery current webhook event type, grouped by family:\n\n**Email** (subject = an email):\n`email.received`, `email.bounced`, `email.tls_report`, `email.dmarc_report`, `email.dmarc_failure`\n\n**Payment** (subject = a payment; see [x402 Payments Overview](x402-payments-overview)):\n`payment.settled`, `payment.failed`\n\n**Interaction** (subject = an interaction step in the x402-over-email or ack protocols):\n`interaction.x402.challenge`, `interaction.x402.payment`, `interaction.x402.settled`, `interaction.x402.rejected`, `interaction.x402.declined`, `interaction.x402.expired`, `interaction.x402.verify_timeout`, `interaction.ack.received`, `interaction.ack.requested`, `interaction.ack.acked`, `interaction.ack.canceled`, `interaction.ack.expired`\n\nEach SDK exports this catalog as a language-native constant:\n\n| SDK | Export |\n|---|---|\n| Node | `WEBHOOK_EVENT_TYPES` (array), `WebhookEventType` (union type) |\n| Python | `WEBHOOK_EVENT_TYPES` (tuple) |\n| Go | `WebhookEventTypes` (slice) |\n\nThe normalized `ReceivedEmail` object and the full `EmailReceivedEvent` schema, the two representations of an `email.received` delivery, are explained once in [Inbound and Outbound Email Model](email-model); this page only covers the webhook transport contract shared by every event family.\n\n## Parsing a delivery: verify, then classify\n\nEvery SDK follows the same two-step flow for any event family:\n\n```mermaid\nsequenceDiagram\n    participant P as Primitive\n    participant H as Your handler\n    P->>H: POST body + Primitive-Signature + X-Webhook-Event\n    H->>H: 1. Verify HMAC over raw body\n    H->>H: 2. Classify on X-Webhook-Event header\n    H->>H: Known type -> typed event\n    H->>H: Unknown type -> UnknownEvent\n```\n\nThe high-level entry point that does both steps in one call is `handleWebhookEvent` (Node/Go: `HandleWebhookEvent`, Python: `handle_webhook_event`). It:\n\n1. Verifies the signature over the raw body (works identically for every family).\n2. Parses the JSON body.\n3. Classifies on the `X-Webhook-Event` header, returning a typed event for known types and an `UnknownEvent` for anything else, it does not throw on an unrecognized type.\n\n<Tabs>\n\n<Tab title=\"Node.js\">\n\n```typescript\nimport {\n  handleWebhookEvent,\n  isPaymentSettledEvent,\n  isInteractionX402Event,\n} from \"@primitivedotdev/sdk/webhook\";\n\nconst event = handleWebhookEvent({\n  body: rawBodyString,\n  headers: req.headers,\n  secret: process.env.PRIMITIVE_WEBHOOK_SECRET!,\n});\n\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  console.log(event.event, event.interaction);\n}\n```\n\n</Tab>\n\n<Tab title=\"Python\">\n\n```python\nfrom primitive import (\n    handle_webhook_event,\n    is_payment_settled_event,\n    is_interaction_x402_event,\n)\n\nevent = handle_webhook_event(\n    body=raw_body,\n    headers=request.headers,\n    secret=os.environ[\"PRIMITIVE_WEBHOOK_SECRET\"],\n)\n\nif is_payment_settled_event(event):\n    print(event[\"challenge_id\"], event[\"amount\"], event[\"settle_tx\"])\nelif is_interaction_x402_event(event):\n    ...\n```\n\n</Tab>\n\n<Tab title=\"Go\">\n\n```go\nevent, err := primitive.HandleWebhookEvent(primitive.HandleWebhookOptions{\n\tBody:    rawBody,\n\tHeaders: req.Header,\n\tSecret:  os.Getenv(\"PRIMITIVE_WEBHOOK_SECRET\"),\n})\nif err != nil {\n\t// signature/verification failure\n}\n\nswitch {\ncase primitive.IsPaymentSettledEvent(event):\n\tsettled := event.(primitive.PaymentEvent)\n\tlog.Println(settled.ChallengeID, settled.Amount, settled.SettleTx)\ncase primitive.IsInteractionX402Event(event):\n\tx402 := event.(primitive.InteractionEvent)\n\t_ = x402\n}\n```\n\n</Tab>\n\n</Tabs>\n\n`payment.*` events carry flat fields with amounts in token base units (USDC has 6 decimals). `interaction.*` events carry the full `{ interaction: {...} }` payload with the canonical `event` name overlaid by the parser (since the raw body has no event/type field of its own).\n\n## Forward compatibility is a hard guarantee\n\nNew event types ship before SDK releases catch up to them, so every SDK treats an unrecognized `X-Webhook-Event` value as data, not an error:\n\n- **Known types** (matching the current catalog) validate strictly, `email.received` bodies are checked against the full JSON Schema; malformed known-type bodies raise a validation error.\n- **Unknown types** are returned as-is, wrapped in an `UnknownEvent` carrying the event name and the raw payload, instead of being rejected.\n\nThis behavior is enforced by the shared compatibility test suite (`test-fixtures/`) across all three SDKs, see [Monorepo Structure and Release Process](monorepo-and-releases) for how that shared fixture contract is maintained. Practically, it means your handler code doesn't break the day Primitive adds `payment.refunded` or a new `interaction.ack.*` suffix; you keep receiving deliveries and can add a case for the new type on your own schedule.\n\n## Legacy hard-typed entry point\n\nEvery SDK keeps a legacy `handleWebhook` entry point (Go: `HandleWebhook`, Python: `handle_webhook`) that is hard-typed to `email.received` only, kept for callers written before payment and interaction events existed. It performs the same verify-then-parse flow but raises if the delivery isn't an `email.received` event. New integrations should use `handleWebhookEvent` unless they are intentionally scoped to email-only webhooks.\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Inbound and Outbound Email Model\" href=\"email-model\">\n\nLearn the normalized ReceivedEmail object and wait-mode delivery statuses that email.received events feed into.\n\n</Card>\n\n<Card title=\"x402 Payments Overview\" href=\"x402-payments-overview\">\n\nSee how payment.* and interaction.x402.* events fit into the non-custodial payment flow.\n\n</Card>\n\n<Card title=\"Handling Payment and Interaction Webhook Events (Node)\" href=\"node-sdk-webhook-events\">\n\nFull handleWebhookEvent reference and typed event guards for the Node.js SDK.\n\n</Card>\n\n<Card title=\"Standard Webhooks Signature Support\" href=\"node-sdk-standard-webhooks\">\n\nUse the webhook-id/webhook-timestamp/webhook-signature scheme instead of Primitive-Signature.\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/docs/architecture.md","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Webhook+Events+Overview&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fwebhook-events","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}