{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/python-client-options","markdown_url":"https://test.abhinandan.one/python-client-options.md","article":{"id":"4c5a7bfc-74d8-4789-9da3-4fbd7629737a","article_slug":"python-client-options","parent_article_slug":null,"parent_article_title":null,"kind":"guide","published_at":"2026-08-11T18:54:59.79607+00:00","keywords":["PrimitiveClient","with_options","api_base_url_1","api_base_url_2","extra_headers","idempotency_key"],"meta_description":"Configure PrimitiveClient defaults with with_options and override timeout, extra_headers, or idempotency_key per call in the Python SDK.","og_image_url":null,"source_file_paths":["sdk-python/src/primitive/client.py","sdk-python/README.md"],"recording_id":null,"replayable":false,"task_name":"Client and Request Options","category":"Python SDK","summary":null,"description":"Configure PrimitiveClient's API key, dual-host base URLs, and default timeout, then override timeout, headers, or idempotency key on any individual send/reply/forward call.","content_kind":"repo_page","content_markdown":"Every `PrimitiveClient` call accepts per-call `timeout`, `extra_headers`, and `idempotency_key` overrides, and `with_options` lets you set new defaults for a cloned client. Reach for this page when you need a shorter timeout on one call, a header on every call from a background worker, or a client that never retries the same send twice.\n\n## Construct a client\n\n`primitive.client(api_key=...)` builds a `PrimitiveClient`, the high-level send/reply/forward client, and requires an API key. Read the key from the environment rather than hardcoding it.\n\n```python\nimport os\nimport primitive\n\nclient = primitive.client(api_key=os.environ[\"PRIMITIVE_API_KEY\"])\n```\n\n<Tip>\n\nFor the canonical receive-and-reply flow this client feeds into, see the [Python SDK Quickstart](python-sdk-quickstart).\n\n</Tip>\n\n### Dual-host base URLs\n\n`PrimitiveClient` talks to two API hosts: `api_base_url_1` for most operations, and `api_base_url_2` (a larger body cap) for `/send-mail` and `/emails/{id}/reply`. This dual-host split is shared across every SDK and transparent in normal use; see [Inbound and Outbound Email Model](email-model).\n\nOverride the hosts only for internal staging or local testing:\n\n```python\nclient = primitive.client(\n    api_key=os.environ[\"PRIMITIVE_API_KEY\"],\n    api_base_url_1=\"https://staging.primitive.dev/api/v1\",\n    api_base_url_2=\"https://staging-send.primitive.dev/v1\",\n)\n```\n\n<Warning>\n\n`PrimitiveClient` no longer accepts a `base_url` keyword. Passing it raises `TypeError` naming the rename explicitly: `api_base_url_1` is the primary host, `api_base_url_2` is the attachments-supporting message host. Update old code to pass `api_base_url_1=...` instead.\n\n</Warning>\n\n## Set client-level defaults with `with_options`\n\n`with_options` returns a clone of the client carrying new default request options, leaving the original untouched. Per-call keyword arguments on `send`, `reply`, and `forward` still win over whatever defaults it sets.\n\n<Steps>\n\n<Step title=\"Start from a configured client\">\n\n```python\nimport primitive\n\nclient = primitive.client(api_key=\"prim_test\")\n```\n\n</Step>\n\n<Step title=\"Clone it with new defaults\">\n\n```python\nfast = client.with_options(timeout=5.0)\n```\n\n`timeout` is three-state, which matters if you ever need to reset it:\n\n- Omit the argument entirely and the clone keeps the parent's current timeout unchanged.\n- Pass `timeout=None` to clear any timeout (httpx's \"no timeout\" behavior).\n- Pass a `float` to set a request timeout in seconds.\n\n</Step>\n\n<Step title=\"Merge in default headers\">\n\n```python\ntraced = client.with_options(extra_headers={\"X-Trace-Id\": \"batch-42\"})\n```\n\n`extra_headers` merges on top of the clone's current defaults. The merge-only model means there's no way to remove a header an earlier `with_options` call added, so construct a fresh client if you need a clean baseline.\n\n</Step>\n\n<Step title=\"Use the clone like any other client\">\n\n```python\nfast.send(\n    from_email=\"Support <support@example.com>\",\n    to=\"alice@example.com\",\n    subject=\"Hello\",\n    body_text=\"Hi there\",\n)\n```\n\nThe clone shares the same underlying `api_client` and `api_send_client` (and their httpx clients) with the parent; only the default options differ.\n\n</Step>\n\n</Steps>\n\n<Warning>\n\n`with_options` accepts `timeout` and `extra_headers` only. Passing `idempotency_key` is rejected because idempotency is a per-call concern: reusing one key across unrelated sends would deduplicate emails you actually want delivered.\n\n</Warning>\n\n## Override options for a single call\n\n`send`, `reply`, `forward`, and their async `a*` variants accept three per-call keyword arguments, layered on top of whatever `with_options` set:\n\n| Keyword | Type | Effect |\n|---|---|---|\n| `timeout` | `float` (seconds) | Overrides the client-level timeout for this call only. |\n| `extra_headers` | `dict[str, str]` | Merged on top of client headers for this call only. |\n| `idempotency_key` | `str` | Sent as the `Idempotency-Key` request header. |\n\n```python\n# Per-call timeout: this send fails fast at 15 seconds instead of\n# waiting on the client's default.\nclient.send(\n    from_email=\"support@example.com\",\n    to=\"alice@example.com\",\n    subject=\"Hello\",\n    body_text=\"Hi there\",\n    timeout=15.0,\n)\n\n# Per-call idempotency key: retrying with the same key returns the\n# original response instead of sending a second email.\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\n<Tip>\n\nUse a fresh, unique `idempotency_key` per logical send. Reusing a key on purpose is exactly how you make retries safe: a retried network call with the same key replays the original response instead of creating a duplicate send. See [Sending Email](python-send-email) for the full `send`/`reply`/`forward` reference.\n\n</Tip>\n\n### Timeout for `wait: true` sends\n\nIf you pass `wait=True` to `send` or `reply`, the call holds the HTTP response open until the first downstream SMTP delivery outcome, or `wait_timeout_ms` (default 30000). Set the client (or per-call) `timeout` long enough to cover that, typically 30-60 seconds:\n\n```python\nclient = primitive.client(api_key=\"prim_test\", timeout=60.0)\n```\n\nThe full `wait`-mode delivery-status contract (`delivered`, `bounced`, `deferred`, `wait_timeout`) is documented once, on [Inbound and Outbound Email Model](email-model).\n\n## How options compose\n\nPer-call keyword arguments beat `with_options` defaults, which beat the client's construction-time settings. Setting the same option at multiple levels is not an error; the narrowest scope wins.\n\n```python\nbase = primitive.client(api_key=\"prim_test\", timeout=30.0)\nscoped = base.with_options(timeout=10.0, extra_headers={\"X-Source\": \"webhook-worker\"})\n\n# Uses timeout=5.0 (per-call) and merges the X-Source header from with_options.\nscoped.send(\n    from_email=\"support@example.com\",\n    to=\"alice@example.com\",\n    subject=\"Hello\",\n    body_text=\"Hi there\",\n    timeout=5.0,\n)\n```\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Sending Email\" href=\"python-send-email\">\n\nUse client.send with idempotency and wait-for-delivery semantics.\n\n</Card>\n\n<Card title=\"Replying and Forwarding\" href=\"python-reply-forward\">\n\nContinue a thread or redirect a message to a new recipient.\n\n</Card>\n\n<Card title=\"Install and Configure the Python SDK\" href=\"python-sdk-quickstart\">\n\nInstall primitivedotdev and send your first email end to end.\n\n</Card>\n\n<Card title=\"Inbound and Outbound Email Model\" href=\"email-model\">\n\nLearn the dual-host client and wait-mode delivery statuses shared across SDKs.\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+Client+and+Request+Options&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fpython-client-options","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}