{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"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","article":{"id":"f32fb931-ccf7-4603-aed4-882718327ea4","article_slug":"node-sdk-x402-email","parent_article_slug":"x402-payments-overview-04a296ff","parent_article_title":"x402 Payments Overview","kind":"guide","published_at":"2026-08-11T18:54:56.574939+00:00","keywords":["createEmailChallenge","parseEmailChallengeFromPart","payEmailChallenge","interaction.json","x402 email-native payment","X402EmailChallenge"],"meta_description":"Issue an x402 payment challenge over an email thread with createEmailChallenge and settle it with payEmailChallenge in the Node.js SDK.","og_image_url":null,"source_file_paths":["sdk-node/README.md","sdk-node/src/x402/client.ts","sdk-go/x402.go","sdk-go/README.md"],"recording_id":null,"replayable":false,"task_name":"Email-Native x402 Payments","category":"Node.js SDK","summary":null,"description":"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.","content_kind":"repo_page","content_markdown":"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.\n\nFor the synthetic-challenge flow (`charge()` / `pay()` with an out-of-band challenge id), see [Charging and Registering Payout Addresses](node-sdk-x402-charging) and [Paying a Challenge](node-sdk-x402-paying). The overall non-custodial payment model (payout registration, spend policy, networks, amount units) is explained on [x402 Payments Overview](x402-payments-overview).\n\n<Note>\n\nThe 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.\n\n</Note>\n\n## What you need before starting\n\n- A payee sending address you control (verified outbound domain), used as the `from` of the challenge email.\n- The payer's email address, to send the challenge `to`.\n- The payer's wallet private key (`PAYER_KEY`), held only on the payer's side.\n- `@primitivedotdev/sdk` installed and `PRIMITIVE_API_KEY` set. See the [Node.js SDK Quickstart](node-sdk-quickstart).\n\n## Issue the challenge as an email (payee)\n\n<Steps>\n\n<Step title=\"Construct the x402 client\">\n\n```ts\nimport { createX402Client } from \"@primitivedotdev/sdk/x402\";\n\nconst x402 = createX402Client({ apiKey: process.env.PRIMITIVE_API_KEY! });\n```\n\n</Step>\n\n<Step title=\"Call createEmailChallenge\">\n\nThe `pay_to` payout wallet and the token asset are resolved server-side from your registered [payout address](node-sdk-x402-charging); you only supply the addresses, amount, and network. Pass exactly one of `amountUsdc` (human USDC, the recommended path) or `amount` (token base units):\n\n```ts\nconst issued = await x402.createEmailChallenge({\n  from: \"payee@your-domain.example\", // your sending address (the funds receiver)\n  to: \"payer@their-domain.example\", // the payer's address\n  amountUsdc: \"0.01\",\n  network: \"base-sepolia\",\n});\n\n// issued.interaction_id is the email thread the payment is bound to;\n// issued.challenge carries the payment_requirements + nonce_binding the payer signs.\n```\n\nThis 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.\n\n</Step>\n\n</Steps>\n\n<Tip>\n\n`createEmailChallenge` takes exactly one of `amount` or `amountUsdc`. Passing both, or neither, throws an `X402Error` before any network call.\n\n</Tip>\n\n## Parse the challenge on the payer's side\n\nThe 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`.\n\n```ts\nimport { parseEmailChallengeFromPart } from \"@primitivedotdev/sdk/x402\";\n\n// `interactionPart` is the body of the inbound email's `interaction.json`\n// attachment: a string, a Buffer/Uint8Array, or an already-parsed object.\nconst issued = parseEmailChallengeFromPart(interactionPart);\n```\n\n`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.\n\n## Sign and reply with the payment (payer)\n\n<Steps>\n\n<Step title=\"Sign the challenge with payEmailChallenge\">\n\n`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.\n\n```ts\nimport { privateKeyToAccount } from \"viem/accounts\";\n\nconst payer = privateKeyToAccount(process.env.PAYER_KEY as `0x${string}`);\nconst built = await x402.payEmailChallenge(issued, { signer: payer });\n\n// `built.json` is the interaction.json body to attach to the reply.\n```\n\nThe 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](node-sdk-x402-signing-primitives) if you need to control that window directly.\n\n</Step>\n\n<Step title=\"Reply to the challenge email with the signed payment attached\">\n\nAttach `built.json` as an `interaction.json` file on a reply to the challenge email, using the [normal reply flow](node-sdk-sending-email):\n\n```ts\nimport { Buffer } from \"node:buffer\";\nimport primitive from \"@primitivedotdev/sdk\";\n\nconst mail = primitive.client({ apiKey: process.env.PRIMITIVE_API_KEY! });\n\nawait mail.reply(challengeEmail, {\n  text: \"Payment attached.\",\n  attachments: [\n    {\n      filename: \"interaction.json\",\n      content_type: \"application/json\",\n      content_base64: Buffer.from(built.json, \"utf8\").toString(\"base64\"),\n    },\n  ],\n});\n```\n\n`challengeEmail` is the [`ReceivedEmail`](node-sdk-receiving-email) normalized from the inbound challenge webhook. Primitive reads the envelope from the attachment, re-derives the interaction-bound nonce, and settles on chain.\n\n</Step>\n\n</Steps>\n\n**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](node-sdk-webhook-events).\n\n<Warning>\n\nThe 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.\n\n</Warning>\n\n## How this differs from the Go SDK\n\nThe 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`.\n\n```go\n// main.go\nimport (\n\t\"encoding/base64\"\n\n\tprimitive \"github.com/primitivedotdev/sdks/sdk-go\"\n)\n\nissued, err := client.CreateEmailChallenge(ctx, primitive.X402EmailChargeInput{\n\tFrom:       \"payee@your-domain.example\",\n\tTo:         \"payer@their-domain.example\",\n\tAmountUsdc: \"0.01\",\n\tNetwork:    \"base-sepolia\",\n})\n\n// On the payer side, after extracting the challenge from the inbound\n// interaction.json part:\nbuilt, err := client.PayEmailChallenge(issued, payer)\n\n_, err = client.Reply(ctx, challengeEmail, primitive.ReplyParams{\n\tBodyText: \"Payment attached.\",\n\tAttachments: []primitive.SendAttachment{\n\t\t{Filename: \"interaction.json\", ContentBase64: base64.StdEncoding.EncodeToString([]byte(built.JSON))},\n\t},\n})\n```\n\nThe Python SDK mirrors this with `create_email_challenge`, `extract_email_challenge`, and `pay_email_challenge`; see [Email-Native Payments (Python)](python-x402-email-payments).\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Charging and Registering Payout Addresses\" href=\"node-sdk-x402-charging\">\n\nRegister a payout address and create synthetic challenges as the payee.\n\n</Card>\n\n<Card title=\"Paying a Challenge\" href=\"node-sdk-x402-paying\">\n\nSign and settle a synthetic (non-email) challenge with pay().\n\n</Card>\n\n<Card title=\"Low-Level x402 Signing Primitives\" href=\"node-sdk-x402-signing-primitives\">\n\nDrive nonce derivation and validity-window computation yourself when payEmailChallenge doesn't fit your flow.\n\n</Card>\n\n<Card title=\"Handling Payment and Interaction Webhook Events\" href=\"node-sdk-webhook-events\">\n\nBranch on payment.settled, payment.failed, and interaction.x402.* deliveries.\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/README.md","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Email-Native+x402+Payments&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fnode-sdk-x402-email","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}