{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/python-errors-reference","markdown_url":"https://test.abhinandan.one/python-errors-reference.md","article":{"id":"ccfba16a-2a41-415f-9cc4-39e3e4910145","article_slug":"python-errors-reference","parent_article_slug":null,"parent_article_title":null,"kind":"reference","published_at":"2026-08-11T18:55:07.905324+00:00","keywords":["PrimitiveAPIError","WebhookVerificationError","WebhookPayloadError","WebhookValidationError","X402Error","RawEmailDecodeError"],"meta_description":"Lists every error code PrimitiveAPIError, WebhookVerificationError, WebhookPayloadError, and X402Error can raise in the Python SDK, with the fix for each.","og_image_url":null,"source_file_paths":["sdk-python/src/primitive/client.py","sdk-python/src/primitive/x402/client.py"],"recording_id":null,"replayable":false,"task_name":"Python SDK Error Reference","category":"Python SDK","summary":null,"description":"Look up every error the Python SDK raises, PrimitiveAPIError fields, webhook verification/payload/validation codes, and X402Error status conditions, with the fix for each.","content_kind":"repo_page","content_markdown":"The Python SDK raises three error families: `PrimitiveAPIError` from the email client, the webhook error classes (`WebhookVerificationError`, `WebhookPayloadError`, `WebhookValidationError`, `RawEmailDecodeError`), and `X402Error` from every x402 method. All are plain Python exceptions you catch with `try`/`except`.\n\n## PrimitiveAPIError\n\nRaised by `client.send`, `client.reply`, `client.forward`, and `client.semantic_search` (and their `a*` async variants) whenever the API responds with a non-2xx status, or the response envelope is missing its `data` field.\n\n```python\nfrom primitive.client import PrimitiveAPIError, PrimitiveClient\n\nclient = PrimitiveClient(api_key=\"prim_test\")\n\ntry:\n    client.send(\n        from_email=\"support@example.com\",\n        to=\"alice@example.com\",\n        subject=\"Hello\",\n        body_text=\"Hi there\",\n    )\nexcept PrimitiveAPIError as err:\n    print(err.status_code, err.code, str(err))\n```\n\n| Field | Type | Description |\n|---|---|---|\n| `status_code` | `int \\| None` | HTTP status code. `None` when no response was ever parsed. |\n| `code` | `str \\| None` | Machine-readable error code from the API's `error.code` field (e.g. `validation_error`, `recipient_not_allowed`, `rate_limit_exceeded`). |\n| `gates` | `list[dict] \\| None` | Gate-denial details when a send is blocked by an authorization gate. Each entry carries `name`, `reason`, `subject`, `message`, and an optional `fix`. |\n| `request_id` | `str \\| None` | The API's request id, useful when filing a support ticket. |\n| `retry_after` | `int \\| None` | Seconds to wait before retrying, parsed from the `Retry-After` header. Present on `429` responses. |\n| `details` | `dict \\| None` | Additional structured context (e.g. `sent_email_id`, `required_entitlements`). |\n| `payload` | `Any` | The raw parsed error response, or the fallback payload when parsing failed. |\n\n`str(err)` returns the human-readable `message`.\n\n### Common `code` values and fixes\n\n| `code` | What triggered it | Fix |\n|---|---|---|\n| `validation_error` | Request body failed server-side validation (e.g. sending to an address that hasn't sent authenticated mail yet on the `agent` plan). | Check the error message for the specific field; adjust the request. |\n| `recipient_not_allowed` | A gate denied the send (see `gates` for detail). | Inspect `err.gates[0][\"reason\"]` and `err.gates[0][\"fix\"]` for the exact remediation, e.g. wait for an inbound email from that address first. |\n| `rate_limit_exceeded` | Exceeded the sliding-window rate limit (120 requests per 60 seconds per organization). | Back off for `err.retry_after` seconds before retrying. |\n| `inbound_not_repliable` (HTTP 422) | `client.reply(...)` targeted an inbound row that cannot be replied to: it was rejected at ingestion, its content was discarded, or it has no recipient recorded. A missing `Message-Id` does not trigger this error; it only omits the threading headers. | Don't retry the reply; the message is not repliable. |\n\n## Webhook errors\n\nRaised by `primitive.receive`, `primitive.handle_webhook`, and `primitive.handle_webhook_event`. Full webhook verification mechanics are documented on [Verifying Webhook Signatures](python-webhook-verification) and [Handling Webhook Events](python-webhook-events); this section is the error lookup.\n\n### `WebhookVerificationError`\n\nRaised when the HMAC (or Standard Webhooks) signature check fails.\n\n| `code` | Cause | Fix |\n|---|---|---|\n| `MISSING_SECRET` | `secret` was `None`, empty string, or empty bytes; or (Standard Webhooks) the secret is not valid base64 (with or without the `whsec_` prefix), or decodes to zero bytes. | Pass the real webhook secret from `GET /account/webhook-secret`, as a UTF-8 string, unmodified. |\n| `INVALID_SIGNATURE_HEADER` | The `Primitive-Signature` header is missing the `t=`/`v1=` format, or (Standard Webhooks) `webhook-signature` is present but empty, or `webhook-id`/`webhook-timestamp` is missing, or `webhook-timestamp` is not a unix-seconds integer. | Confirm the header is forwarded to your handler unmodified. |\n| `TIMESTAMP_OUT_OF_RANGE` | The signature timestamp is more than `tolerance_seconds` (default 300s) old, or more than 60 seconds in the future. | Check server clock sync; pass a larger `tolerance_seconds` only if you understand the replay-window tradeoff. |\n| `SIGNATURE_MISMATCH` | The computed HMAC does not match any signature in the header. | Verify the secret matches your account, and that you're signing the raw request body, not a re-serialized `json.dumps()` of it. |\n\n### `WebhookPayloadError`\n\nRaised when the request body isn't parseable as the expected shape.\n\n| `code` | Cause | Fix |\n|---|---|---|\n| `PAYLOAD_EMPTY_BODY` | The request body is empty. | Check your web framework passes the raw body through. |\n| `JSON_PARSE_FAILED` | The body isn't valid JSON. | Check for truncation or double-encoding before it reaches the SDK. |\n| `INVALID_ENCODING` | The body contains invalid UTF-8 bytes. | If the data is binary, base64-encode it first. |\n| `PAYLOAD_UNDEFINED` | Nothing was passed to `parse_webhook_event(...)`. | Pass the parsed body explicitly. |\n| `PAYLOAD_NULL` | The payload is `None`. | Check that your request-body variable is defined before parsing. |\n| `PAYLOAD_IS_ARRAY` | The payload is a JSON array, not an object. | Webhook payloads are always objects; check upstream framing. |\n| `PAYLOAD_WRONG_TYPE` | The payload (or a required nested field, e.g. `email.content.download.expires_at`) is missing or the wrong type. | Confirm the path named in the error message is present in the raw payload. |\n| `PAYLOAD_MISSING_EVENT` | Neither the `X-Webhook-Event` header nor a top-level `event` field in the body identifies the event type. | Pass the `X-Webhook-Event` header through, or call `handle_webhook_event`, which reads it for you. |\n\n### `WebhookValidationError`\n\nRaised when a known event type (most commonly `email.received`) fails JSON Schema validation.\n\nThe error code is `SCHEMA_VALIDATION_FAILED`, and the message describes the schema violation. Fix: compare the payload against `json-schema/email-received-event.schema.json`, the canonical schema source referenced on [Webhook Events Overview](webhook-events).\n\n### `RawEmailDecodeError`\n\nRaised by `decode_raw_email` and `verify_raw_email_download`.\n\n| `code` | Cause | Fix |\n|---|---|---|\n| `NOT_INCLUDED` | Raw content wasn't included inline; the error message includes the download URL. | Fetch `email.content.download.url` instead of decoding inline. |\n| `INVALID_BASE64` | `email.content.raw.data` isn't valid base64. | Treat as a corrupted payload; do not retry decoding the same bytes. |\n| `HASH_MISMATCH` | The decoded (or downloaded) bytes' SHA-256 doesn't match `email.content.raw.sha256`. | The content may be corrupted in transit; re-fetch or re-request delivery. |\n\n## X402Error\n\nRaised by every method on `primitive.x402.X402Client` (`charge`, `create_email_challenge`, `pay`, `pay_email_challenge`, `register_payout_address`, `get_challenge`, `get_spend_policy`, `set_spend_policy`, `list_payout_addresses`, `list_declined_payments`) on a client-side, transport, or non-2xx server error.\n\n```python\nfrom primitive import X402Error\n\ntry:\n    x402.pay(challenge, signer=payer)\nexcept X402Error as err:\n    print(err.status, err.retry_after, err.body)\n```\n\n| Field | Type | Description |\n|---|---|---|\n| `status` | `int` | HTTP status, or `0` for a client-side/transport error that never reached the server. |\n| `body` | `Any` | The parsed error envelope when present, or a truncated raw response body. |\n| `retry_after` | `str \\| None` | The `Retry-After` response header, when the server sent one. |\n\n<Warning>\n\nOn `pay()`, a `status == 0` error means the request may never have reached the server, the payment outcome is **indeterminate**. Do not blindly retry; check `get_challenge(id)` or the settlement webhook before resubmitting, since resubmitting a payment that actually landed risks a duplicate authorization attempt.\n\n</Warning>\n\n### Status-0 (client-side) conditions\n\nThese never reach the server. All are raised before any HTTP request is made.\n\n| Message contains | Cause | Fix |\n|---|---|---|\n| `no API key configured` | Neither `api_key` nor `PRIMITIVE_API_KEY` was set. | Pass `api_key=` explicitly or export `PRIMITIVE_API_KEY`. |\n| `unknown charge() option \"...\"` | A typo'd keyword argument to `charge()`. | Check the argument name against the documented `charge()` signature on [Creating and Paying Challenges](python-x402-charge-and-pay). |\n| `exactly one of \\`amount\\` ... or \\`amount_usdc\\`` | Both `amount` and `amount_usdc` were passed. | Pass exactly one. |\n| `requires \\`amount\\` as a positive integer string ... or \\`amount_usdc\\`` | Neither `amount` nor `amount_usdc` was passed, or the value given failed validation. | Pass `amount_usdc=\"0.01\"` (human USDC) or `amount=\"10000\"` (base units). |\n| `at most 6 decimals` | `amount_usdc` had more than 6 decimal places, was non-positive, or was malformed. | USDC has 6 decimals; use a value like `\"0.01\"`. |\n| `requires a signer` | `pay()` or `pay_email_challenge()` was called with `signer=None` or a signer missing the required methods. | Pass a `PrivateKeySigner` or an object implementing `sign_typed_data`/`sign_message`. |\n| `challenge is missing or malformed: <field>` | The challenge object passed to `pay()` is missing a required field (`id`, `network`, `expires_at`, `nonce_binding`, or a `payment_requirements` field). | Re-fetch the challenge with `get_challenge(id)` rather than hand-constructing one. |\n| `email challenge is missing or malformed: <field>` | The challenge passed to `pay_email_challenge()` is missing a required field, or its `interaction_id` disagrees with `challenge.nonce_binding.interaction_id`. | Use `extract_email_challenge(...)` to build the challenge from the raw `interaction.json` part instead of constructing it manually. |\n| `interaction.json part is not a valid x402 challenge: <field>` | `extract_email_challenge(...)` received a malformed or non-challenge envelope. | Confirm the attachment is the unmodified `interaction.json` part from the challenge email. |\n| `already expired` | The challenge's `expires_at` (plus settlement margin) is in the past. | Request a fresh challenge; an expired challenge cannot be signed into a valid authorization. |\n| `invalid expires_at` | The challenge's `expires_at` isn't a parseable timestamp. | Re-fetch the challenge rather than editing the field by hand. |\n| `network mismatch` | The challenge's top-level `network` disagrees with `payment_requirements.network`. | Treat as a malformed challenge; re-fetch it. |\n| `could not resolve your organization id` | `register_payout_address()` was called without `org=` and the account lookup returned no id. | Pass `org=` explicitly, or confirm your API key resolves to a valid organization. |\n| `requires \\`from_\\`` / `requires \\`to\\`` | `create_email_challenge()` was called without one of the required email addresses. | Pass both `from_` and `to`. |\n| request timed out / request failed | A DNS, connection, or TLS-level failure, or the request exceeded the client's timeout. | Retry with backoff; check network connectivity to `api.primitive.dev`. |\n| non-JSON response / missing success/data envelope | The server returned something other than the expected `{\"success\": ..., \"data\": ...}` envelope. | Usually transient; retry. Persisting failures indicate an API-side issue. |\n\n### Server-side (non-zero status) conditions\n\n| `status` | Typical cause |\n|---|---|\n| `422` | Payment declined at settlement (e.g. `payment_declined` in the error message). |\n| `429` | Rate limited; `retry_after` names the backoff window. |\n| Other 4xx/5xx | See `err.body` for the server's `error.message`. |\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Verifying Webhook Signatures\" href=\"python-webhook-verification\">\n\nFull walkthrough of HMAC and Standard Webhooks verification that raises these errors.\n\n</Card>\n\n<Card title=\"Creating and Paying Challenges\" href=\"python-x402-charge-and-pay\">\n\nThe charge()/pay() flow that raises X402Error on failure.\n\n</Card>\n\n<Card title=\"Registering Payout Addresses and Spend Policy\" href=\"python-x402-payout-and-policy\">\n\nPayout registration and spend-policy calls covered by the same X402Error contract.\n\n</Card>\n\n<Card title=\"Email-Native Payments\" href=\"python-x402-email-payments\">\n\ncreate_email_challenge / extract_email_challenge / pay_email_challenge error conditions in context.\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/client.py","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Python+SDK+Error+Reference&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fpython-errors-reference","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}