Python SDK Error Reference
Look up every error the Python SDK raises, PrimitiveAPIError fields, webhook verification/payload/validation codes, and X402Error status conditions, with the fix for each.
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.
PrimitiveAPIError#
Raised 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.
from primitive.client import PrimitiveAPIError, PrimitiveClient
client = PrimitiveClient(api_key="prim_test")
try:
client.send(
from_email="support@example.com",
to="alice@example.com",
subject="Hello",
body_text="Hi there",
)
except PrimitiveAPIError as err:
print(err.status_code, err.code, str(err))
| Field | Type | Description |
|---|---|---|
status_code | int | None | HTTP status code. None when no response was ever parsed. |
code | str | None | Machine-readable error code from the API's error.code field (e.g. validation_error, recipient_not_allowed, rate_limit_exceeded). |
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. |
request_id | str | None | The API's request id, useful when filing a support ticket. |
retry_after | int | None | Seconds to wait before retrying, parsed from the Retry-After header. Present on 429 responses. |
details | dict | None | Additional structured context (e.g. sent_email_id, required_entitlements). |
payload | Any | The raw parsed error response, or the fallback payload when parsing failed. |
str(err) returns the human-readable message.
Common code values and fixes#
code | What triggered it | Fix |
|---|---|---|
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. |
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. |
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. |
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. |
Webhook errors#
Raised by primitive.receive, primitive.handle_webhook, and primitive.handle_webhook_event. Full webhook verification mechanics are documented on Verifying Webhook Signatures and Handling Webhook Events; this section is the error lookup.
WebhookVerificationError#
Raised when the HMAC (or Standard Webhooks) signature check fails.
code | Cause | Fix |
|---|---|---|
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. |
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. |
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. |
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. |
WebhookPayloadError#
Raised when the request body isn't parseable as the expected shape.
code | Cause | Fix |
|---|---|---|
PAYLOAD_EMPTY_BODY | The request body is empty. | Check your web framework passes the raw body through. |
JSON_PARSE_FAILED | The body isn't valid JSON. | Check for truncation or double-encoding before it reaches the SDK. |
INVALID_ENCODING | The body contains invalid UTF-8 bytes. | If the data is binary, base64-encode it first. |
PAYLOAD_UNDEFINED | Nothing was passed to parse_webhook_event(...). | Pass the parsed body explicitly. |
PAYLOAD_NULL | The payload is None. | Check that your request-body variable is defined before parsing. |
PAYLOAD_IS_ARRAY | The payload is a JSON array, not an object. | Webhook payloads are always objects; check upstream framing. |
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. |
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. |
WebhookValidationError#
Raised when a known event type (most commonly email.received) fails JSON Schema validation.
The 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.
RawEmailDecodeError#
Raised by decode_raw_email and verify_raw_email_download.
code | Cause | Fix |
|---|---|---|
NOT_INCLUDED | Raw content wasn't included inline; the error message includes the download URL. | Fetch email.content.download.url instead of decoding inline. |
INVALID_BASE64 | email.content.raw.data isn't valid base64. | Treat as a corrupted payload; do not retry decoding the same bytes. |
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. |
X402Error#
Raised 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.
from primitive import X402Error
try:
x402.pay(challenge, signer=payer)
except X402Error as err:
print(err.status, err.retry_after, err.body)
| Field | Type | Description |
|---|---|---|
status | int | HTTP status, or 0 for a client-side/transport error that never reached the server. |
body | Any | The parsed error envelope when present, or a truncated raw response body. |
retry_after | str | None | The Retry-After response header, when the server sent one. |
On 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.
Status-0 (client-side) conditions#
These never reach the server. All are raised before any HTTP request is made.
| Message contains | Cause | Fix |
|---|---|---|
no API key configured | Neither api_key nor PRIMITIVE_API_KEY was set. | Pass api_key= explicitly or export PRIMITIVE_API_KEY. |
unknown charge() option "..." | A typo'd keyword argument to charge(). | Check the argument name against the documented charge() signature on Creating and Paying Challenges. |
exactly one of \amount` ... or `amount_usdc`` | Both amount and amount_usdc were passed. | Pass exactly one. |
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). |
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". |
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. |
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. |
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. |
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. |
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. |
invalid expires_at | The challenge's expires_at isn't a parseable timestamp. | Re-fetch the challenge rather than editing the field by hand. |
network mismatch | The challenge's top-level network disagrees with payment_requirements.network. | Treat as a malformed challenge; re-fetch it. |
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. |
requires \from_`/requires `to`` | create_email_challenge() was called without one of the required email addresses. | Pass both from_ and to. |
| 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. |
| 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. |
Server-side (non-zero status) conditions#
status | Typical cause |
|---|---|
422 | Payment declined at settlement (e.g. payment_declined in the error message). |
429 | Rate limited; retry_after names the backoff window. |
| Other 4xx/5xx | See err.body for the server's error.message. |
Next steps#
Full walkthrough of HMAC and Standard Webhooks verification that raises these errors.
Creating and Paying ChallengesThe charge()/pay() flow that raises X402Error on failure.
Registering Payout Addresses and Spend PolicyPayout registration and spend-policy calls covered by the same X402Error contract.
Email-Native Paymentscreate_email_challenge / extract_email_challenge / pay_email_challenge error conditions in context.
Was this page helpful?