---
title: "Email-Native Payments (Python SDK)"
canonical: "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"
publisher: "Primitive SDKs"
kind: "guide"
content_type: "reference"
category: "Python SDK"
parent: "x402-payments-overview-04a296ff"
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."
keywords: ["create_email_challenge", "extract_email_challenge", "pay_email_challenge", "interaction.json", "x402 email-native payment", "X402Client python"]
last_modified: "2026-08-21T18:22:43.359885+00:00"
published_at: "2026-08-11T18:55:06.607242+00:00"
source_files:
  - "sdk-python/src/primitive/x402/client.py"
  - "sdk-python/README.md"
  - "sdk-python/tests/test_x402_client.py"
sections:
  - {anchor: "prerequisites", title: "Prerequisites"}
  - {anchor: "the-flow", title: "The flow"}
  - {anchor: "step-issue-the-challenge-as-an-email-payee", title: "Issue the challenge as an email (payee)"}
  - {anchor: "step-extract-the-challenge-from-the-inbound-email-payer", title: "Extract the challenge from the inbound email (payer)"}
  - {anchor: "step-sign-the-payment-locally-payer", title: "Sign the payment locally (payer)"}
  - {anchor: "step-reply-with-the-signed-envelope-attached-payer", title: "Reply with the signed envelope attached (payer)"}
  - {anchor: "verifying-settlement", title: "Verifying settlement"}
  - {anchor: "common-failure-modes", title: "Common failure modes"}
  - {anchor: "next-steps", title: "Next steps"}
---

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

# Email-Native Payments (Python SDK)

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.

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.

This is the Python SDK's version of the shared [email-native x402 payment](https://test.abhinandan.one/x402-payments-overview.md) flow. For the synthetic-challenge alternative (create a challenge, hand the id to the payer over any channel), see [Creating and Paying Challenges](https://test.abhinandan.one/x402-payments-overview-04a296ff/python-x402-charge-and-pay.md).

> **Note:** Every 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](https://test.abhinandan.one/python-errors-reference.md) for the full catalog.

## Prerequisites

You need two wallet keys, a Primitive API key, a registered payout address, and a way to send and reply to mail.

- 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.
- A Primitive API key in `PRIMITIVE_API_KEY`, set on the payee's `X402Client`.
- The payee's payout address already registered for the network you're using, see [Registering Payout Addresses and Spend Policy](https://test.abhinandan.one/x402-payments-overview-04a296ff/python-x402-payout-and-policy.md).
- 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](https://test.abhinandan.one/python-send-email.md) and [Replying and Forwarding](https://test.abhinandan.one/python-reply-forward.md)).

## The flow

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

```mermaid
sequenceDiagram
    participant Payee
    participant Email as Email thread
    participant Payer

    Payee->>Payee: x402.create_email_challenge(from_, to, amount_usdc)
    Payee->>Email: sends challenge email (interaction_id bound)
    Email->>Payer: inbound email with interaction.json attachment
    Payer->>Payer: extract_email_challenge(interaction_part)
    Payer->>Payer: x402.pay_email_challenge(challenge, signer=payer)
    Payer->>Email: reply with signed interaction.json attached
    Email->>Payee: platform reads envelope, re-derives nonce, settles on chain
```

### 1. Issue the challenge as an email (payee)

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

```python
import os
import primitive

x402 = primitive.create_x402_client(api_key=os.environ["PRIMITIVE_API_KEY"])

issued = x402.create_email_challenge(
    from_="payee@your-domain.example",  # your sending address (funds receiver)
    to="payer@their-domain.example",    # the payer's address
    amount_usdc="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 to sign.
```

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

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

### 2. Extract the challenge from the inbound email (payer)

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

```python
from primitive import extract_email_challenge

# `interaction_part` is the body of the inbound email's `interaction.json`
# attachment (str, bytes, or an already-parsed dict).
issued = extract_email_challenge(interaction_part)
```

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

### 3. Sign the payment locally (payer)

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

```python
import base64
import os
import primitive

x402 = primitive.create_x402_client(api_key=os.environ["PRIMITIVE_API_KEY"])
payer = primitive.PrivateKeySigner(os.environ["PAYER_KEY"])
built = x402.pay_email_challenge(issued, signer=payer)

# `built.json` is the interaction.json body.
attachment: primitive.SendAttachment = {
    "filename": "interaction.json",
    "content_type": "application/json",
    "content_base64": base64.b64encode(built.json.encode("utf-8")).decode(),
}
```

The 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](https://test.abhinandan.one/x402-payments-overview-04a296ff/python-x402-signing-primitives.md) for how that window is derived.

### 4. Reply with the signed envelope attached (payer)

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

```python
import os
import primitive

client = primitive.client(api_key=os.environ["PRIMITIVE_API_KEY"])

client.reply(
    challenge_email,
    {"text": "Payment attached.", "attachments": [attachment]},
)
```

`challenge_email` is the `ReceivedEmail` object from normalizing the inbound challenge; see [Receiving and Parsing Inbound Email](https://test.abhinandan.one/python-receive-email.md).

## Verifying settlement

Confirm 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](https://test.abhinandan.one/python-webhook-events.md).

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

## Common failure modes

Most failures come from hand-building a challenge object or attaching the signed envelope with the wrong filename or content type.

| Symptom | Cause | Fix |
| --- | --- | --- |
| `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 |
| `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 |
| 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"` |
