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"])
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",
)
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#
import primitive client = primitive.client(api_key="prim_test") - 2
Clone it with new defaults#
fast = client.with_options(timeout=5.0)timeoutis 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=Noneto clear any timeout (httpx's "no timeout" behavior). - Pass a
floatto set a request timeout in seconds.
- 3
Merge in default headers#
traced = client.with_options(extra_headers={"X-Trace-Id": "batch-42"})extra_headersmerges on top of the clone's current defaults. The merge-only model means there's no way to remove a header an earlierwith_optionscall added, so construct a fresh client if you need a clean baseline. - 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_clientandapi_send_client(and their httpx clients) with the parent; only the default options differ.
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. |
# 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",
)
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#
Use client.send with idempotency and wait-for-delivery semantics.
Replying and ForwardingContinue a thread or redirect a message to a new recipient.
Install and Configure the Python SDKInstall primitivedotdev and send your first email end to end.
Inbound and Outbound Email ModelLearn the dual-host client and wait-mode delivery statuses shared across SDKs.
Was this page helpful?