{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/x402-payments-overview-04a296ff/python-x402-email-payments","markdown_url":"https://test.abhinandan.one/x402-payments-overview-04a296ff/python-x402-email-payments.md","article":{"id":"ab9cb747-7a7e-412f-a09b-9ede7aad8ab9","article_slug":"python-x402-email-payments","parent_article_slug":"x402-payments-overview-04a296ff","parent_article_title":"x402 Payments Overview","kind":"guide","published_at":"2026-08-11T18:55:06.607242+00:00","keywords":["create_email_challenge","extract_email_challenge","pay_email_challenge","interaction.json","x402 email-native payment","X402Client python"],"meta_description":"Issue and pay an x402 payment challenge over an email thread in Python with create_email_challenge, extract_email_challenge, and pay_email_challenge.","og_image_url":null,"source_file_paths":["sdk-python/src/primitive/x402/client.py","sdk-python/README.md","sdk-python/tests/test_x402_client.py"],"recording_id":null,"replayable":false,"task_name":"Email-Native Payments (Python SDK)","category":"Python SDK","summary":null,"description":"Issue an x402 payment challenge over a real email thread and pay it with a signed interaction.json attachment, using create_email_challenge, extract_email_challenge, and pay_email_challenge.","content_kind":"repo_page","content_markdown":"Use email-native x402 payments when the payment challenge should ride a real email thread instead of an out-of-band challenge id. The payee issues the challenge as an email; the payer signs it locally into an `interaction.json` payment step and replies with that file attached.\n\nThis is the Python SDK's version of the shared [email-native x402 payment](x402-payments-overview) flow. For the synthetic-challenge alternative (create a challenge, hand the id to the payer over any channel), see [Creating and Paying Challenges](python-x402-charge-and-pay).\n\n<Note>\n\nEvery method on `X402Client` raises `primitive.X402Error` on a client-side, transport, or non-2xx server error. `status` is the HTTP status, or `0` for a request that never reached the server or failed local validation before any network call. See the [Python SDK Error Reference](python-errors-reference) for the full catalog.\n\n</Note>\n\n## Prerequisites\n\nYou need two wallet keys, a Primitive API key, a registered payout address, and a way to send and reply to mail.\n\n- A payee wallet key in `PAYEE_KEY` and a payer wallet key in `PAYER_KEY` (0x-prefixed hex private keys). Keys never leave the process that holds them.\n- A Primitive API key in `PRIMITIVE_API_KEY`, set on the payee's `X402Client`.\n- The payee's payout address already registered for the network you're using, see [Registering Payout Addresses and Spend Policy](python-x402-payout-and-policy).\n- A way for the payee to send outbound mail and the payer to receive and reply to it (the SDK's `client.send` / `client.reply`, covered in [Sending Email](python-send-email) and [Replying and Forwarding](python-reply-forward)).\n\n## The flow\n\nThe payee issues a challenge email, the payer extracts and signs it locally, and the payer replies with the signed `interaction.json` attached so the platform can settle.\n\n```mermaid\nsequenceDiagram\n    participant Payee\n    participant Email as Email thread\n    participant Payer\n\n    Payee->>Payee: x402.create_email_challenge(from_, to, amount_usdc)\n    Payee->>Email: sends challenge email (interaction_id bound)\n    Email->>Payer: inbound email with interaction.json attachment\n    Payer->>Payer: extract_email_challenge(interaction_part)\n    Payer->>Payer: x402.pay_email_challenge(challenge, signer=payer)\n    Payer->>Email: reply with signed interaction.json attached\n    Email->>Payee: platform reads envelope, re-derives nonce, settles on chain\n```\n\n<Steps>\n\n<Step title=\"Issue the challenge as an email (payee)\">\n\nCall `create_email_challenge` with the payee's sending address (`from_`), the payer's address (`to`), and the amount. The `pay_to` payout wallet and token asset are resolved server-side from the payee's registered payout address, you only supply the addresses, amount, and network.\n\n```python\nimport os\nimport primitive\n\nx402 = primitive.create_x402_client(api_key=os.environ[\"PRIMITIVE_API_KEY\"])\n\nissued = x402.create_email_challenge(\n    from_=\"payee@your-domain.example\",  # your sending address (funds receiver)\n    to=\"payer@their-domain.example\",    # the payer's address\n    amount_usdc=\"0.01\",\n    network=\"base-sepolia\",\n)\n# issued.interaction_id is the email thread the payment is bound to;\n# issued.challenge carries the payment_requirements + nonce_binding to sign.\n```\n\n`create_email_challenge` sends the challenge email itself; you don't call `client.send` separately. Provide exactly one of `amount_usdc` (human USDC, e.g. `\"0.01\"`) or `amount` (base units, e.g. `\"10000\"`); passing both raises `X402Error`.\n\n<Tip>\n\nPass `idempotency_key` to `create_email_challenge` to make retries safe: retrying with the same key returns the original challenge instead of sending a second email.\n\n</Tip>\n\n</Step>\n\n<Step title=\"Extract the challenge from the inbound email (payer)\">\n\nThe payer receives the challenge as an `interaction.json` MIME part on an inbound email. Pass the part's bytes to `extract_email_challenge` rather than hand-parsing the envelope: it validates the wire shape and rebuilds the nonce binding for you.\n\n```python\nfrom primitive import extract_email_challenge\n\n# `interaction_part` is the body of the inbound email's `interaction.json`\n# attachment (str, bytes, or an already-parsed dict).\nissued = extract_email_challenge(interaction_part)\n```\n\n`extract_email_challenge` raises `primitive.X402Error` (status `0`) on any malformed or non-challenge part: bad JSON, wrong protocol, wrong step, or a payment-requirements shape that fails validation. The resulting object's `challenge_id` is always empty because the platform's private challenge id is never carried on the wire; `pay_email_challenge` doesn't need it, since it binds to `interaction_id` and the challenge step id instead.\n\n</Step>\n\n<Step title=\"Sign the payment locally (payer)\">\n\nBuild the signed payment step with `pay_email_challenge`. This does not send anything: it returns the signed envelope and its canonical JSON bytes, ready to attach to a reply.\n\n```python\nimport base64\nimport os\nimport primitive\n\nx402 = primitive.create_x402_client(api_key=os.environ[\"PRIMITIVE_API_KEY\"])\npayer = primitive.PrivateKeySigner(os.environ[\"PAYER_KEY\"])\nbuilt = x402.pay_email_challenge(issued, signer=payer)\n\n# `built.json` is the interaction.json body.\nattachment: primitive.SendAttachment = {\n    \"filename\": \"interaction.json\",\n    \"content_type\": \"application/json\",\n    \"content_base64\": base64.b64encode(built.json.encode(\"utf-8\")).decode(),\n}\n```\n\nThe validity window (`valid_after` / `valid_before`) is computed and clamped into the platform's accepted band automatically, so you never hand-set `valid_before`. The band keeps at least 60 seconds of settlement headroom and caps the total window at 24 hours; see [Low-Level Payment Signing](python-x402-signing-primitives) for how that window is derived.\n\n</Step>\n\n<Step title=\"Reply with the signed envelope attached (payer)\">\n\nAttach the built `interaction.json` to a reply on the same thread using the client's `reply` method. The platform reads the envelope, re-derives the interaction-bound nonce, and settles on chain.\n\n```python\nimport os\nimport primitive\n\nclient = primitive.client(api_key=os.environ[\"PRIMITIVE_API_KEY\"])\n\nclient.reply(\n    challenge_email,\n    {\"text\": \"Payment attached.\", \"attachments\": [attachment]},\n)\n```\n\n`challenge_email` is the `ReceivedEmail` object from normalizing the inbound challenge; see [Receiving and Parsing Inbound Email](python-receive-email).\n\n</Step>\n\n</Steps>\n\n## Verifying settlement\n\nConfirm settlement from webhook events, not from the return value of `pay_email_challenge`: settlement happens asynchronously after the platform reads the reply. Listen for the `payment.settled` / `payment.failed` events, or the `interaction.x402.*` lifecycle events, as described in [Handling Webhook Events](python-webhook-events).\n\n<Warning>\n\n`pay_email_challenge` only signs; it does not confirm delivery. Treat a successful call as \"the payment step is ready to send,\" not \"the payment settled.\" Confirm settlement from the webhook event, not from the return value of `pay_email_challenge`.\n\n</Warning>\n\n## Common failure modes\n\nMost failures come from hand-building a challenge object or attaching the signed envelope with the wrong filename or content type.\n\n| Symptom | Cause | Fix |\n| --- | --- | --- |\n| `email challenge is missing or malformed: interaction_id (mismatch with challenge.nonce_binding.interaction_id)` | The envelope's `interaction_id` disagrees with the nested `nonce_binding.interaction_id` | Re-extract with `extract_email_challenge`; don't hand-construct the challenge object |\n| `email challenge is missing or malformed: challenge.expires_at` | Challenge object built manually and missing required fields | Always build challenges via `create_email_challenge` / `extract_email_challenge`, never by hand |\n| Payment step signed but never settles | Reply sent without the `interaction.json` attachment, or attached under the wrong filename/content type | Use exactly `filename=\"interaction.json\"`, `content_type=\"application/json\"` |\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Creating and Paying Challenges\" href=\"python-x402-charge-and-pay\">\n\nThe synthetic-challenge flow: create a challenge with charge() and settle it with pay() over any out-of-band channel.\n\n</Card>\n\n<Card title=\"Low-Level Payment Signing\" href=\"python-x402-signing-primitives\">\n\nDrive nonce derivation, validity windows, and EIP-712 signing directly when pay_email_challenge doesn't fit your flow.\n\n</Card>\n\n<Card title=\"Handling Webhook Events\" href=\"python-webhook-events\">\n\nParse payment.settled, payment.failed, and interaction.x402.* events to confirm a payment's outcome.\n\n</Card>\n\n<Card title=\"Registering Payout Addresses and Spend Policy\" href=\"python-x402-payout-and-policy\">\n\nRegister the payee's payout address and configure spend caps before issuing challenges.\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-python/src/primitive/x402/client.py","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Email-Native+Payments+%28Python+SDK%29&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fpython-x402-email-payments","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}