{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/python-send-email","markdown_url":"https://test.abhinandan.one/python-send-email.md","article":{"id":"a4d9b079-13dd-44f5-88ac-5206ef4fe6ad","article_slug":"python-send-email","parent_article_slug":null,"parent_article_title":null,"kind":"guide","published_at":"2026-08-11T18:55:00.50459+00:00","keywords":["client.send python","primitive.client(api_key)","idempotency_key python","wait_timeout_ms","delivery_status","SendResult"],"meta_description":"client.send() in the Primitive Python SDK sends outbound mail, returning as soon as Primitive accepts the message unless you pass wait=True.","og_image_url":null,"source_file_paths":["sdk-python/src/primitive/client.py","sdk-python/README.md","sdk-python/tests/test_client.py"],"recording_id":null,"replayable":false,"task_name":"Sending Email","category":"Python SDK","summary":null,"description":"Send outbound email synchronously with client.send, control idempotent retries and wait-mode delivery confirmation, and read the resulting SendResult.","content_kind":"repo_page","content_markdown":"Use `client.send` to send a new outbound email from the Python SDK (`primitivedotdev` on PyPI, imported as `primitive`). Reach for it whenever you're composing a fresh message rather than continuing a thread. For replies and forwards, see [Replying and Forwarding](python-reply-forward).\n\nBy default `send` returns as soon as Primitive accepts the message for delivery. Pass `wait=True` when you need to know the first downstream SMTP outcome before your code moves on.\n\n## Send a message\n\nCall `client.send` with `from_email`, `to`, `subject`, and one of `body_text` or `body_html`; it returns a `SendResult` once Primitive accepts the message.\n\n<Steps>\n\n<Step title=\"Create a client\">\n\n```python\nimport primitive\n\nclient = primitive.client(api_key=\"prim_test\")\n```\n\n`primitive.client(...)` builds a `PrimitiveClient`. Configuring the API key, timeouts, and dual-host base URLs is covered in [Install and Configure the Python SDK](python-sdk-quickstart) and [Client and Request Options](python-client-options).\n\n</Step>\n\n<Step title=\"Call client.send with required fields\">\n\n```python\nresult = client.send(\n    from_email=\"Support <support@example.com>\",\n    to=\"alice@example.com\",\n    subject=\"Hello\",\n    body_text=\"Hi there\",\n)\n\nprint(result.id, result.status, result.queue_id, result.delivery_status)\n```\n\n`from_email`, `to`, and `subject` are required. You must supply at least one of `body_text` or `body_html`; passing neither raises `ValueError(\"one of body_text or body_html is required\")` before any request is sent.\n\n</Step>\n\n<Step title=\"Read the result\">\n\n`client.send` returns a `SendResult` dataclass:\n\n| Field | Type | Meaning |\n|---|---|---|\n| `id` | `str` | The sent-email id |\n| `status` | `str` | Server-side send status (e.g. `submitted_to_agent`) |\n| `accepted` | `list[str]` | Recipients accepted for delivery |\n| `rejected` | `list[str]` | Recipients rejected |\n| `client_idempotency_key` | `str` | The idempotency key recorded for this send |\n| `request_id` | `str` | Server request id, useful for support |\n| `content_hash` | `str` | Hash of the canonical send payload |\n| `queue_id` | `str \\| None` | Queue id for the outbound message, if any |\n| `idempotent_replay` | `bool` | `True` when this response replays an earlier send with the same idempotency key |\n| `delivery_status` | `str \\| None` | Set only in [wait mode](#wait-mode-and-delivery-status) |\n| `smtp_response_code` | `int \\| None` | Set only in wait mode |\n| `smtp_response_text` | `str \\| None` | Set only in wait mode |\n\n</Step>\n\n</Steps>\n\n## Validation before the request fires\n\n`send` validates its arguments locally before making a network call, so malformed input raises `ValueError` immediately instead of burning a request:\n\n- `from_email` and `to` must be non-empty header-safe strings (3–998 chars for `from`, 3–320 for `to`).\n- `to` must be a valid email address or a `\"Display Name <addr@example.com>\"` form. An invalid address raises `ValueError(\"to must be a valid email address\")`.\n- `subject` must be non-empty.\n- One of `body_text` / `body_html` is required.\n- `wait_timeout_ms`, if given, must be between `1000` and `30000`.\n\n## Idempotent retries\n\nPass `idempotency_key` to `send` to make retries safe; it is sent as the `Idempotency-Key` request header. Reusing the same key returns the original response instead of sending a duplicate message:\n\n```python\nclient.send(\n    from_email=\"support@example.com\",\n    to=\"alice@example.com\",\n    subject=\"Hello\",\n    body_text=\"Hi there\",\n    idempotency_key=\"customer-key-abc123\",\n)\n```\n\nUse one key per logical send, for example derived from your own outbox row id, not a value that changes on every retry. Check `result.idempotent_replay` to tell a fresh send from a replayed one.\n\n<Tip>\n\nFor per-call `timeout` and `extra_headers` in addition to `idempotency_key`, see [Client and Request Options](python-client-options). `with_options(...)` lets you set client-wide defaults that these per-call kwargs still override.\n\n</Tip>\n\n## Wait mode and delivery status\n\nPass `wait=True` to hold the HTTP response open until the first downstream SMTP delivery outcome, or until `wait_timeout_ms` (default 30000 ms) elapses. The wait-mode delivery model (statuses `delivered`, `bounced`, `deferred`, `wait_timeout`) is identical across every SDK; see [Inbound and Outbound Email Model](email-model) for the full contract. In the Python SDK, opt in with `wait=True`:\n\n```python\nresult = client.send(\n    from_email=\"support@example.com\",\n    to=\"alice@example.com\",\n    subject=\"Hello\",\n    body_text=\"Hi there\",\n    wait=True,\n    wait_timeout_ms=5000,\n)\n\nprint(result.delivery_status)       # \"delivered\", \"bounced\", \"deferred\", or \"wait_timeout\"\nprint(result.smtp_response_code)    # e.g. 250\nprint(result.smtp_response_text)    # e.g. \"250 OK\"\n```\n\n`wait_timeout_ms` must be between `1000` and `30000` and defaults to `30000` when omitted. When you set `wait=True`, configure the client with a request timeout long enough for SMTP delivery, typically 30-60 seconds:\n\n```python\nclient = primitive.client(api_key=\"prim_test\", timeout=60.0)\n```\n\n<Warning>\n\n`wait_timeout` means \"no outcome observed in time,\" not \"the send failed.\" The message may still be delivered after your call returns. Do not treat `wait_timeout` as a permanent failure.\n\n</Warning>\n\n## Threading a send manually\n\n`send` accepts an optional `thread` argument (a `SendThread` with `in_reply_to` and `references`) when you need to place a new message into an existing thread yourself. Most reply flows should use `client.reply` instead; see [Replying and Forwarding](python-reply-forward).\n\n```python\nfrom primitive.client import SendThread\n\nclient.send(\n    from_email=\"support@example.com\",\n    to=\"alice@example.com\",\n    subject=\"Re: Hello\",\n    body_text=\"Following up\",\n    thread=SendThread(\n        in_reply_to=\"<parent@example.com>\",\n        references=[\"<root@example.com>\", \"<parent@example.com>\"],\n    ),\n)\n```\n\n## Error handling\n\nA non-2xx response raises `primitive.client.PrimitiveAPIError`, the SDK's API-error exception, which carries `status_code`, `code`, `gates`, `request_id`, `retry_after`, and `details`:\n\n```python\nfrom primitive.client import PrimitiveAPIError\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, err.request_id)\n```\n\nFor the full error catalog and what triggers each code, see [Python SDK Error Reference](python-errors-reference).\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Replying and Forwarding\" href=\"python-reply-forward\">\n\nContinue an inbound thread with client.reply or forward it with client.forward.\n\n</Card>\n\n<Card title=\"Client and Request Options\" href=\"python-client-options\">\n\nSet per-call timeouts, extra headers, and client-wide defaults with with_options.\n\n</Card>\n\n<Card title=\"Inbound and Outbound Email Model\" href=\"email-model\">\n\nUnderstand the wait-mode delivery statuses shared across every SDK.\n\n</Card>\n\n<Card title=\"Python SDK Error Reference\" href=\"python-errors-reference\">\n\nLook up every PrimitiveAPIError code and the fix for each.\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+Sending+Email&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fpython-send-email","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}