---
title: "Handling Payment and Interaction Webhook Events"
canonical: "https://test.abhinandan.one/node-sdk-webhook-events"
markdown_url: "https://test.abhinandan.one/node-sdk-webhook-events.md"
publisher: "Primitive SDKs"
kind: "guide"
content_type: "reference"
category: "Node.js SDK"
description: "handleWebhookEvent verifies the Primitive-Signature HMAC once and returns a typed union of email.*, payment.*, and interaction.x402.* webhook events."
keywords: ["handleWebhookEvent", "isPaymentSettledEvent", "isInteractionX402Event", "X-Webhook-Event header", "WEBHOOK_EVENT_TYPES", "UnknownEvent"]
last_modified: "2026-08-11T18:54:50.989585+00:00"
published_at: "2026-08-11T18:54:50.636551+00:00"
source_files:
  - "sdk-node/src/webhook/index.ts"
sections:
  - {anchor: "why-the-header-matters", title: "Why the header matters"}
  - {anchor: "verify-and-classify-in-one-call", title: "Verify and classify in one call"}
  - {anchor: "step-import-the-handler-and-type-guards", title: "Import the handler and type guards"}
  - {anchor: "step-call-handlewebhookevent-with-the-raw-body-headers-and-secret", title: "Call handleWebhookEvent with the raw body, headers, and secret"}
  - {anchor: "step-branch-on-the-typed-event", title: "Branch on the typed event"}
  - {anchor: "the-event-catalog", title: "The event catalog"}
  - {anchor: "typed-shapes-for-payment-and-interaction-events", title: "Typed shapes for payment and interaction events"}
  - {anchor: "handlewebhookevent-vs-handlewebhook", title: "handleWebhookEvent vs. handleWebhook"}
  - {anchor: "next-steps", title: "Next steps"}
---

> Documentation index: https://test.abhinandan.one/llms.txt

# Handling Payment and Interaction Webhook Events

Verify and branch on every webhook family, email.*, payment.*, and interaction.x402.*, from a single endpoint using handleWebhookEvent and its typed event guards.

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.

> **Note:** If your integration only ever needs inbound mail, use `primitive.receive(...)` or `handleWebhook(...)` instead, see [Receiving Inbound Email](https://test.abhinandan.one/node-sdk-receiving-email.md). Reach for `handleWebhookEvent` the moment you also care about payments or interactions.

## Why the header matters

The `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:

| Family | Discriminator location | Example |
| --- | --- | --- |
| `email.*` | body field `event` | `{ "event": "email.received", ... }` |
| `payment.*` | body field `type` | `{ "type": "payment.settled", ... }` |
| `interaction.*` | none, body has no event/type field | `{ "interaction": { ... } }` |

Because `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.

## Verify and classify in one call

`handleWebhookEvent` verifies the `Primitive-Signature` HMAC over the raw body first (this step is identical to [webhook signature verification](https://test.abhinandan.one/node-sdk-webhook-signing.md) and independent of the event family), then parses the JSON and classifies it on the `X-Webhook-Event` header.

### 1. Import the handler and type guards

```typescript
import {
  handleWebhookEvent,
  isPaymentSettledEvent,
  isInteractionX402Event,
} from "@primitivedotdev/sdk/webhook";
```

### 2. Call handleWebhookEvent with the raw body, headers, and secret

```typescript
const event = handleWebhookEvent({
  body: rawBodyString,
  headers: req.headers,
  secret: process.env.PRIMITIVE_WEBHOOK_SECRET!,
});
```

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

### 3. Branch on the typed event

```typescript
if (isPaymentSettledEvent(event)) {
  // typed PaymentSettledEvent: flat fields, amount in token base units
  console.log("settled", event.challenge_id, event.amount, event.settle_tx);
} else if (isInteractionX402Event(event)) {
  // typed interaction.x402.* event (challenge/payment/settled/...)
  console.log(event.event, event.interaction);
} else if (event.event === "email.received") {
  // fully typed EmailReceivedEvent — see the email-model docs
  console.log(event.email.headers.subject);
} else {
  // UnknownEvent: a future event type this SDK version doesn't know about yet
  console.log("unhandled event:", event.event);
}
```

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

## The event catalog

The SDK classifies 19 header values across four families, exported as the `WEBHOOK_EVENT_TYPES` array and the `WebhookEventType` union:

```typescript
import { WEBHOOK_EVENT_TYPES } from "@primitivedotdev/sdk/webhook";
```

| Family | Event names |
| --- | --- |
| Email | `email.received`, `email.bounced`, `email.tls_report`, `email.dmarc_report`, `email.dmarc_failure` |
| Payment | `payment.settled`, `payment.failed` |
| Interaction (x402) | `interaction.x402.challenge`, `interaction.x402.payment`, `interaction.x402.settled`, `interaction.x402.rejected`, `interaction.x402.declined`, `interaction.x402.expired`, `interaction.x402.verify_timeout` |
| Interaction (ack) | `interaction.ack.received`, `interaction.ack.requested`, `interaction.ack.acked`, `interaction.ack.canceled`, `interaction.ack.expired` |

Only `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](https://test.abhinandan.one/webhook-events.md) for the cross-SDK contract and forward-compatibility guarantees this catalog is part of.

## Typed shapes for payment and interaction events

`PaymentEvent` (and its narrower `PaymentSettledEvent` / `PaymentFailedEvent` variants) carries flat fields, no nested payment object:

```typescript
import type { PaymentEvent, PaymentSettledEvent, PaymentFailedEvent } from "@primitivedotdev/sdk/webhook";
```

- `challenge_id`, the [x402 payment challenge](https://test.abhinandan.one/x402-payments-overview.md) this event settles or fails
- `amount`, token base units (USDC has 6 decimals, so `"10000"` is 0.01 USDC)
- `settle_tx`, present on `payment.settled`, the on-chain settlement transaction hash
- `failure_reason`, present on `payment.failed`

`InteractionX402Event` wraps the raw `{ interaction: {...} }` body plus the canonical `event` name from the header:

```typescript
import type { InteractionX402Event } from "@primitivedotdev/sdk/webhook";
```

Use `isPaymentEvent`, `isPaymentSettledEvent`, `isPaymentFailedEvent`, and `isInteractionX402Event` as type guards to narrow `WebhookEvent` without manually checking `event` strings.

## handleWebhookEvent vs. handleWebhook

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

> **Tip:** If 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.

> **Warning:** Don'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.
