Documentation Index: Fetch llms.txt first to discover every published page. This page is also available as Markdown at /python-client-options.md.
Verified · 8/11/2026

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.

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.

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.

Override the hosts only for internal staging or local testing:

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

    Start from a configured client#

    import primitive
    
    client = primitive.client(api_key="prim_test")
    
  2. 2

    Clone it with new defaults#

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

    Merge in default headers#

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

    Use the clone like any other client#

    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:

KeywordTypeEffect
timeoutfloat (seconds)Overrides the client-level timeout for this call only.
extra_headersdict[str, str]Merged on top of client headers for this call only.
idempotency_keystrSent as the Idempotency-Key request header.
# 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 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:

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.

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.

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,
)

Next steps#

Was this page helpful?

© Primitive SDKs

Powered by Browzer