{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/x402-payments-overview-04a296ff/python-x402-charge-and-pay","markdown_url":"https://test.abhinandan.one/x402-payments-overview-04a296ff/python-x402-charge-and-pay.md","article":{"id":"428c3885-2994-4210-afbc-b667d0e2120c","article_slug":"python-x402-charge-and-pay","parent_article_slug":"x402-payments-overview-04a296ff","parent_article_title":"x402 Payments Overview","kind":"guide","published_at":"2026-08-11T18:55:05.570146+00:00","keywords":["X402Client.charge","X402Client.pay","x402 payment challenge","amount_usdc","PrivateKeySigner","get_challenge"],"meta_description":"Create a synthetic USDC payment challenge with X402Client.charge and settle it with X402Client.pay in the Primitive Python SDK.","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":"Creating and Paying Challenges","category":"Python SDK","summary":null,"description":"Create an x402 payment challenge as the payee with X402Client.charge, then sign and settle it as the payer with X402Client.pay, using non-custodial USDC payments on Base.","content_kind":"repo_page","content_markdown":"Use `X402Client.charge` to request a USDC payment as the payee, and `X402Client.pay` to sign and settle it as the payer. Reach for this out-of-band flow whenever the challenge id travels through your own channel, an API response, a dashboard, a database row, rather than riding a real email thread; for the email-carried variant, see [Email-Native Payments](python-x402-email-payments).\n\nThis page covers the synthetic-challenge flow end to end: creating the [x402 payment challenge](x402-payments-overview), handing it to a payer, and settling it. For the non-custodial payment model itself (registering a payout address, spend policy, how settlement works), see [x402 Payments Overview](x402-payments-overview).\n\n<Note>\n\nBoth `charge()` and `pay()` require a payout address already registered for the payee's org. See [Registering Payout Addresses and Spend Policy](python-x402-payout-and-policy) if you haven't done that yet.\n\n</Note>\n\n## Prerequisites\n\n- `primitivedotdev` installed (`pip install primitivedotdev`)\n- A Primitive API key (`prim_test` in examples below, or `PRIMITIVE_API_KEY` in the environment)\n- The payee's payout address already registered for the target network\n- A payer wallet private key, held only by the payer (`PAYER_KEY` in examples below)\n\n## Create the client\n\nBuild an `X402Client` with `primitive.create_x402_client`, the factory that returns the client for every x402 operation.\n\n```python\nimport os\nimport primitive\n\nx402 = primitive.create_x402_client(api_key=os.environ[\"PRIMITIVE_API_KEY\"])\n```\n\n`create_x402_client` and `X402Client` are both exported from `primitive` and `primitive.x402`. With no `api_key` argument it reads `PRIMITIVE_API_KEY` from the environment.\n\n<Steps>\n\n<Step title=\"Create the challenge (payee side)\">\n\nCall `charge()` with an amount and a network. Provide exactly one of `amount_usdc` (a human USDC decimal string) or `amount` (token base units), passing both raises `X402Error`.\n\n```python\nchallenge = x402.charge(\n    amount_usdc=\"0.01\",  # human USDC amount\n    network=\"base-sepolia\",\n    payer_org=os.environ.get(\"PAYER_ORG_ID\"),  # org allowed to pay\n    description=\"API call\",\n)\n\nprint(challenge.id, challenge.expires_at)\n```\n\n`amount_usdc=\"0.01\"` converts to base units `\"10000\"` internally (USDC has 6 decimals). Use `amount=\"10000\"` directly if you already have a base-unit value. `network` defaults to `base-sepolia` (testnet); use `base` for mainnet.\n\n`payer_org` is optional and binds the challenge to a specific paying org on-net. Other optional fields: `description`, `resource` (a URL identifying what's being paid for), `expires_in` (seconds until the challenge expires, defaulting to 3600 seconds / 1 hour), and `idempotency_key` (retrying `charge()` with the same key returns the original challenge instead of creating a duplicate).\n\nThe returned `challenge` carries `payment_requirements` and `nonce_binding`, the fields the payer signs over. Hand the whole object to the payer over any out-of-band channel (an API response, a message, a database row).\n\n</Step>\n\n<Step title=\"Sign and settle the challenge (payer side)\">\n\nThe payer builds a `PrivateKeySigner` from their own key and calls `pay()`. The key never leaves the process; `pay()` signs an EIP-3009 `transferWithAuthorization` locally and submits it.\n\n```python\npayer = primitive.PrivateKeySigner(os.environ[\"PAYER_KEY\"])\nreceipt = x402.pay(challenge, signer=payer)\n\nprint(receipt.status, receipt.settle_tx)  # settled, 0x-prefixed tx hash\n```\n\n`receipt.status` is `\"settled\"` on success; `receipt.settle_tx` is the on-chain settlement transaction hash.\n\n</Step>\n\n</Steps>\n\n## What each call returns\n\n`charge()` returns an `X402Challenge` dataclass with `id`, `network`, `amount` (base units), `pay_to`, `nonce_binding`, `payment_requirements`, and `expires_at`. `pay()` returns an `X402Receipt` with `id`, `status`, and `settle_tx`:\n\n```python\nprint(challenge.id, challenge.amount, challenge.pay_to)\n# 11111111-1111-4111-8111-111111111111 10000 0x1111111111111111111111111111111111111111\n\nprint(receipt.status, receipt.settle_tx)\n# settled 0xaaaa...\n```\n\n`settle_tx` is `None` when the platform has not yet recorded a settlement transaction.\n\n## Re-hydrating a challenge\n\nCall `get_challenge(id)` to fetch a challenge you already created, for example to retry `pay()` after a restart:\n\n```python\nimport os\nimport primitive\n\nx402 = primitive.create_x402_client(api_key=os.environ[\"PRIMITIVE_API_KEY\"])\nchallenge = x402.get_challenge(\"11111111-1111-4111-8111-111111111111\")\n```\n\n## Validation and failure modes\n\nBoth methods validate their inputs locally and raise `X402Error` with `status` `0` before any network call. `charge()` checks the amount:\n\n- Passing both `amount` and `amount_usdc`, or neither, raises `X402Error` with status `0`.\n- A non-positive, malformed, or over-6-decimal `amount_usdc` (e.g. `\"0\"`, `\"abc\"`, `\"1.1234567\"`) raises `X402Error` before any network call.\n- An unknown keyword argument (a typo like `payer_x`) raises `X402Error` naming the bad key rather than being silently dropped.\n\n`pay()` validates the challenge is fully hydrated and unexpired *before* signing, so a malformed challenge fails with a named `X402Error` instead of an opaque error mid-sign:\n\n- A missing or malformed `payment_requirements` (bad `maxAmountRequired`, `payTo`, `asset`, or `extra`) raises `X402Error`.\n- A challenge that already expired, or expires within the settlement margin, raises `X402Error` with `\"already expired\"` in the message, and never reaches the server.\n- Calling `pay()` without a signer, or with an object missing `sign_typed_data`, raises `X402Error`.\n\n<Warning>\n\nEvery method raises `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. On `pay()`, a `status == 0` error means the request may not have been sent, the payment outcome is **indeterminate**. Don't assume the payment failed; check `get_challenge()` or wait for a `payment.*` webhook before retrying. See the full [error reference](python-errors-reference) and [Handling Webhook Events](python-webhook-events).\n\n</Warning>\n\n<Tip>\n\n`amount_usdc` is the documented easy path for specifying an amount. Use raw `amount` in base units only when you've already computed the value yourself.\n\n</Tip>\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Email-Native Payments\" href=\"python-x402-email-payments\">\n\nIssue and pay an x402 challenge that rides a real email thread instead of an out-of-band channel.\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 and allowlists before charging.\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() doesn't fit your flow.\n\n</Card>\n\n<Card title=\"Python SDK Error Reference\" href=\"python-errors-reference\">\n\nLook up every X402Error condition and the suggested fix.\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+Creating+and+Paying+Challenges&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fpython-x402-charge-and-pay","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}