Documentation Index: Fetch llms.txt first to discover every published page. This page is also available as Markdown at /node-sdk-webhook-events.md.
Verified · 8/11/2026

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

FamilyDiscriminator locationExample
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 and independent of the event family), then parses the JSON and classifies it on the X-Webhook-Event header.

  1. 1

    Import the handler and type guards#

    import {
      handleWebhookEvent,
      isPaymentSettledEvent,
      isInteractionX402Event,
    } from "@primitivedotdev/sdk/webhook";
    
  2. 2

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

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

    Branch on the typed event#

    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:

import { WEBHOOK_EVENT_TYPES } from "@primitivedotdev/sdk/webhook";
FamilyEvent names
Emailemail.received, email.bounced, email.tls_report, email.dmarc_report, email.dmarc_failure
Paymentpayment.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 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:

import type { PaymentEvent, PaymentSettledEvent, PaymentFailedEvent } from "@primitivedotdev/sdk/webhook";
  • challenge_id, the x402 payment challenge 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:

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.

Next steps#

Was this page helpful?

© Primitive SDKs

Powered by Browzer