---
title: "Email-Native x402 Payments"
canonical: "https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-email"
markdown_url: "https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-email.md"
publisher: "Primitive SDKs"
kind: "guide"
content_type: "reference"
category: "Node.js SDK"
parent: "x402-payments-overview-04a296ff"
description: "Issue an x402 payment challenge over an email thread with createEmailChallenge and settle it with payEmailChallenge in the Node.js SDK."
keywords: ["createEmailChallenge", "parseEmailChallengeFromPart", "payEmailChallenge", "interaction.json", "x402 email-native payment", "X402EmailChallenge"]
last_modified: "2026-08-11T18:54:56.743506+00:00"
published_at: "2026-08-11T18:54:56.574939+00:00"
source_files:
  - "sdk-node/README.md"
  - "sdk-node/src/x402/client.ts"
  - "sdk-go/x402.go"
  - "sdk-go/README.md"
sections:
  - {anchor: "what-you-need-before-starting", title: "What you need before starting"}
  - {anchor: "issue-the-challenge-as-an-email-payee", title: "Issue the challenge as an email (payee)"}
  - {anchor: "step-construct-the-x402-client", title: "Construct the x402 client"}
  - {anchor: "step-call-createemailchallenge", title: "Call createEmailChallenge"}
  - {anchor: "parse-the-challenge-on-the-payers-side", title: "Parse the challenge on the payer's side"}
  - {anchor: "sign-and-reply-with-the-payment-payer", title: "Sign and reply with the payment (payer)"}
  - {anchor: "step-sign-the-challenge-with-payemailchallenge", title: "Sign the challenge with payEmailChallenge"}
  - {anchor: "step-reply-to-the-challenge-email-with-the-signed-payment-attached", title: "Reply to the challenge email with the signed payment attached"}
  - {anchor: "how-this-differs-from-the-go-sdk", title: "How this differs from the Go SDK"}
  - {anchor: "next-steps", title: "Next steps"}
---

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

# Email-Native x402 Payments

Issue and pay x402 payment challenges that ride a real email thread using createEmailChallenge, parseEmailChallengeFromPart, and payEmailChallenge, for when the payment needs to travel over email instead of an out-of-band challenge id.

Use email-native x402 payments when the payment challenge needs to travel inside a real email thread instead of being exchanged out-of-band (API call, dashboard link). The payee issues the challenge as an email; the payer signs it locally and replies with the signed payment attached as `interaction.json`. Primitive reads that attachment, re-derives the interaction-bound nonce, and settles on chain.

For the synthetic-challenge flow (`charge()` / `pay()` with an out-of-band challenge id), see [Charging and Registering Payout Addresses](https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-charging.md) and [Paying a Challenge](https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-paying.md). The overall non-custodial payment model (payout registration, spend policy, networks, amount units) is explained on [x402 Payments Overview](https://test.abhinandan.one/x402-payments-overview.md).

> **Note:** The payer's signing key never leaves their machine. `payEmailChallenge` signs locally and returns bytes to attach; it does not submit anything over the network itself.

## What you need before starting

- A payee sending address you control (verified outbound domain), used as the `from` of the challenge email.
- The payer's email address, to send the challenge `to`.
- The payer's wallet private key (`PAYER_KEY`), held only on the payer's side.
- `@primitivedotdev/sdk` installed and `PRIMITIVE_API_KEY` set. See the [Node.js SDK Quickstart](https://test.abhinandan.one/node-sdk-quickstart.md).

## Issue the challenge as an email (payee)

### 1. Construct the x402 client

```ts
import { createX402Client } from "@primitivedotdev/sdk/x402";

const x402 = createX402Client({ apiKey: process.env.PRIMITIVE_API_KEY! });
```

### 2. Call createEmailChallenge

The `pay_to` payout wallet and the token asset are resolved server-side from your registered [payout address](https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-charging.md); you only supply the addresses, amount, and network. Pass exactly one of `amountUsdc` (human USDC, the recommended path) or `amount` (token base units):

```ts
const issued = await x402.createEmailChallenge({
  from: "payee@your-domain.example", // your sending address (the funds receiver)
  to: "payer@their-domain.example", // the payer's address
  amountUsdc: "0.01",
  network: "base-sepolia",
});

// issued.interaction_id is the email thread the payment is bound to;
// issued.challenge carries the payment_requirements + nonce_binding the payer signs.
```

This sends the challenge email from `from` to `to` and returns the `X402EmailChallenge`, including the real `interaction_id` (the thread id, `uuid@domain`) the payment binds to. `X402EmailChargeInput` also accepts `description`, `resource`, `expiresIn` (seconds, default 300 seconds / 5 minutes), and `idempotencyKey`; retrying with the same key returns the original challenge without sending a second email.

> **Tip:** `createEmailChallenge` takes exactly one of `amount` or `amountUsdc`. Passing both, or neither, throws an `X402Error` before any network call.

## Parse the challenge on the payer's side

The payer receives the challenge as an `interaction.json` MIME part on an inbound email (filename `interaction.json`, content type `application/json`). Don't hand-parse it: `parseEmailChallengeFromPart` validates the envelope shape (protocol, step, nonce binding, payment requirements) and returns the typed `X402EmailChallenge`.

```ts
import { parseEmailChallengeFromPart } from "@primitivedotdev/sdk/x402";

// `interactionPart` is the body of the inbound email's `interaction.json`
// attachment: a string, a Buffer/Uint8Array, or an already-parsed object.
const issued = parseEmailChallengeFromPart(interactionPart);
```

`parseEmailChallengeFromPart` throws an `X402Error` with `status` `0` on any malformed or non-challenge part: a wrong `interaction_version`, a wrong `protocol`, a `step` other than `"challenge"`, a malformed `challenge_nonce`, or missing `payment_requirements` fields. Treat that error as "this attachment isn't a valid x402 challenge," not a network failure.

## Sign and reply with the payment (payer)

### 1. Sign the challenge with payEmailChallenge

`payEmailChallenge` derives the interaction-bound authorization, signs it locally with your signer, and returns the signed payment-step envelope plus its canonical JSON bytes. It does **not** send anything over the network; you attach the result yourself.

```ts
import { privateKeyToAccount } from "viem/accounts";

const payer = privateKeyToAccount(process.env.PAYER_KEY as `0x${string}`);
const built = await x402.payEmailChallenge(issued, { signer: payer });

// `built.json` is the interaction.json body to attach to the reply.
```

The validity window (`validAfter` / `validBefore`) is computed and clamped into the platform's accepted band automatically, so you never hand-set `validBefore`. See [Low-Level x402 Signing Primitives](https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-signing-primitives.md) if you need to control that window directly.

### 2. Reply to the challenge email with the signed payment attached

Attach `built.json` as an `interaction.json` file on a reply to the challenge email, using the [normal reply flow](https://test.abhinandan.one/node-sdk-sending-email.md):

```ts
import { Buffer } from "node:buffer";
import primitive from "@primitivedotdev/sdk";

const mail = primitive.client({ apiKey: process.env.PRIMITIVE_API_KEY! });

await mail.reply(challengeEmail, {
  text: "Payment attached.",
  attachments: [
    {
      filename: "interaction.json",
      content_type: "application/json",
      content_base64: Buffer.from(built.json, "utf8").toString("base64"),
    },
  ],
});
```

`challengeEmail` is the [`ReceivedEmail`](https://test.abhinandan.one/node-sdk-receiving-email.md) normalized from the inbound challenge webhook. Primitive reads the envelope from the attachment, re-derives the interaction-bound nonce, and settles on chain.

**Expected result**: the reply send succeeds and Primitive settles the payment on chain. Confirm the outcome asynchronously via the `payment.settled` / `payment.failed` webhook or the `interaction.x402.*` events; see [Handling Payment and Interaction Webhook Events](https://test.abhinandan.one/node-sdk-webhook-events.md).

> **Warning:** The signed authorization stays settleable only inside its validity window, which is clamped to at most 24 hours with at least 60 seconds of settlement headroom. Attach and send `built.json` promptly; an envelope whose window has lapsed is rejected, and you must issue a new challenge.

## How this differs from the Go SDK

The Go SDK exposes the identical flow with different function names: `client.CreateEmailChallenge`, `primitive.ExtractEmailChallenge`, and `client.PayEmailChallenge`, using a `*primitive.PrivateKeySigner` in place of a viem `LocalAccount`.

```go
// main.go
import (
	"encoding/base64"

	primitive "github.com/primitivedotdev/sdks/sdk-go"
)

issued, err := client.CreateEmailChallenge(ctx, primitive.X402EmailChargeInput{
	From:       "payee@your-domain.example",
	To:         "payer@their-domain.example",
	AmountUsdc: "0.01",
	Network:    "base-sepolia",
})

// On the payer side, after extracting the challenge from the inbound
// interaction.json part:
built, err := client.PayEmailChallenge(issued, payer)

_, err = client.Reply(ctx, challengeEmail, primitive.ReplyParams{
	BodyText: "Payment attached.",
	Attachments: []primitive.SendAttachment{
		{Filename: "interaction.json", ContentBase64: base64.StdEncoding.EncodeToString([]byte(built.JSON))},
	},
})
```

The Python SDK mirrors this with `create_email_challenge`, `extract_email_challenge`, and `pay_email_challenge`; see [Email-Native Payments (Python)](https://test.abhinandan.one/x402-payments-overview-04a296ff/python-x402-email-payments.md).
