---
title: "Client and Request Options"
canonical: "https://test.abhinandan.one/python-client-options"
markdown_url: "https://test.abhinandan.one/python-client-options.md"
publisher: "Primitive SDKs"
kind: "guide"
content_type: "reference"
category: "Python SDK"
description: "Configure PrimitiveClient defaults with with_options and override timeout, extra_headers, or idempotency_key per call in the Python SDK."
keywords: ["PrimitiveClient", "with_options", "api_base_url_1", "api_base_url_2", "extra_headers", "idempotency_key"]
last_modified: "2026-08-21T18:22:43.359885+00:00"
published_at: "2026-08-11T18:54:59.79607+00:00"
source_files:
  - "sdk-python/src/primitive/client.py"
  - "sdk-python/README.md"
sections:
  - {anchor: "construct-a-client", title: "Construct a client"}
  - {anchor: "dual-host-base-urls", title: "Dual-host base URLs"}
  - {anchor: "set-client-level-defaults-with-with_options", title: "Set client-level defaults with `with_options`"}
  - {anchor: "step-start-from-a-configured-client", title: "Start from a configured client"}
  - {anchor: "step-clone-it-with-new-defaults", title: "Clone it with new defaults"}
  - {anchor: "step-merge-in-default-headers", title: "Merge in default headers"}
  - {anchor: "step-use-the-clone-like-any-other-client", title: "Use the clone like any other client"}
  - {anchor: "override-options-for-a-single-call", title: "Override options for a single call"}
  - {anchor: "timeout-for-wait-true-sends", title: "Timeout for `wait: true` sends"}
  - {anchor: "how-options-compose", title: "How options compose"}
  - {anchor: "next-steps", title: "Next steps"}
---

> Documentation index: https://test.abhinandan.one/llms.txt

# Client and Request Options

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.

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.

## Construct a client

`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.

```python
import os
import primitive

client = primitive.client(api_key=os.environ["PRIMITIVE_API_KEY"])
```

> **Tip:** For the canonical receive-and-reply flow this client feeds into, see the [Python SDK Quickstart](https://test.abhinandan.one/python-sdk-quickstart.md).

### Dual-host base URLs

`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](https://test.abhinandan.one/email-model.md).

Override the hosts only for internal staging or local testing:

```python
client = primitive.client(
    api_key=os.environ["PRIMITIVE_API_KEY"],
    api_base_url_1="https://staging.primitive.dev/api/v1",
    api_base_url_2="https://staging-send.primitive.dev/v1",
)
```

> **Warning:** `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.

## Set client-level defaults with `with_options`

`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.

### 1. Start from a configured client

```python
import primitive

client = primitive.client(api_key="prim_test")
```

### 2. Clone it with new defaults

```python
fast = client.with_options(timeout=5.0)
```

`timeout` is three-state, which matters if you ever need to reset it:

- Omit the argument entirely and the clone keeps the parent's current timeout unchanged.
- Pass `timeout=None` to clear any timeout (httpx's "no timeout" behavior).
- Pass a `float` to set a request timeout in seconds.

### 3. Merge in default headers

```python
traced = client.with_options(extra_headers={"X-Trace-Id": "batch-42"})
```

`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.

### 4. Use the clone like any other client

```python
fast.send(
    from_email="Support <support@example.com>",
    to="alice@example.com",
    subject="Hello",
    body_text="Hi there",
)
```

The clone shares the same underlying `api_client` and `api_send_client` (and their httpx clients) with the parent; only the default options differ.

> **Warning:** `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.

## Override options for a single call

`send`, `reply`, `forward`, and their async `a*` variants accept three per-call keyword arguments, layered on top of whatever `with_options` set:

| Keyword | Type | Effect |
|---|---|---|
| `timeout` | `float` (seconds) | Overrides the client-level timeout for this call only. |
| `extra_headers` | `dict[str, str]` | Merged on top of client headers for this call only. |
| `idempotency_key` | `str` | Sent as the `Idempotency-Key` request header. |

```python
# Per-call timeout: this send fails fast at 15 seconds instead of
# waiting on the client's default.
client.send(
    from_email="support@example.com",
    to="alice@example.com",
    subject="Hello",
    body_text="Hi there",
    timeout=15.0,
)

# Per-call idempotency key: retrying with the same key returns the
# original response instead of sending a second email.
client.send(
    from_email="support@example.com",
    to="alice@example.com",
    subject="Hello",
    body_text="Hi there",
    idempotency_key="customer-key-abc123",
)
```

> **Tip:** Use 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](https://test.abhinandan.one/python-send-email.md) for the full `send`/`reply`/`forward` reference.

### Timeout for `wait: true` sends

If 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:

```python
client = primitive.client(api_key="prim_test", timeout=60.0)
```

The full `wait`-mode delivery-status contract (`delivered`, `bounced`, `deferred`, `wait_timeout`) is documented once, on [Inbound and Outbound Email Model](https://test.abhinandan.one/email-model.md).

## How options compose

Per-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.

```python
base = primitive.client(api_key="prim_test", timeout=30.0)
scoped = base.with_options(timeout=10.0, extra_headers={"X-Source": "webhook-worker"})

# Uses timeout=5.0 (per-call) and merges the X-Source header from with_options.
scoped.send(
    from_email="support@example.com",
    to="alice@example.com",
    subject="Hello",
    body_text="Hi there",
    timeout=5.0,
)
```
