Sending Email
Send outbound email synchronously with client.send, control idempotent retries and wait-mode delivery confirmation, and read the resulting SendResult.
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.
By 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.
Send a message#
Call client.send with from_email, to, subject, and one of body_text or body_html; it returns a SendResult once Primitive accepts the message.
- 1
Create a client#
import primitive client = primitive.client(api_key="prim_test")primitive.client(...)builds aPrimitiveClient. Configuring the API key, timeouts, and dual-host base URLs is covered in Install and Configure the Python SDK and Client and Request Options. - 2
Call client.send with required fields#
result = client.send( from_email="Support <support@example.com>", to="alice@example.com", subject="Hello", body_text="Hi there", ) print(result.id, result.status, result.queue_id, result.delivery_status)from_email,to, andsubjectare required. You must supply at least one ofbody_textorbody_html; passing neither raisesValueError("one of body_text or body_html is required")before any request is sent. - 3
Read the result#
client.sendreturns aSendResultdataclass:Field Type Meaning idstrThe sent-email id statusstrServer-side send status (e.g. submitted_to_agent)acceptedlist[str]Recipients accepted for delivery rejectedlist[str]Recipients rejected client_idempotency_keystrThe idempotency key recorded for this send request_idstrServer request id, useful for support content_hashstrHash of the canonical send payload queue_idstr | NoneQueue id for the outbound message, if any idempotent_replayboolTruewhen this response replays an earlier send with the same idempotency keydelivery_statusstr | NoneSet only in wait mode smtp_response_codeint | NoneSet only in wait mode smtp_response_textstr | NoneSet only in wait mode
Validation before the request fires#
send validates its arguments locally before making a network call, so malformed input raises ValueError immediately instead of burning a request:
from_emailandtomust be non-empty header-safe strings (3–998 chars forfrom, 3–320 forto).tomust be a valid email address or a"Display Name <addr@example.com>"form. An invalid address raisesValueError("to must be a valid email address").subjectmust be non-empty.- One of
body_text/body_htmlis required. wait_timeout_ms, if given, must be between1000and30000.
Idempotent retries#
Pass 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:
client.send(
from_email="support@example.com",
to="alice@example.com",
subject="Hello",
body_text="Hi there",
idempotency_key="customer-key-abc123",
)
Use 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.
For per-call timeout and extra_headers in addition to idempotency_key, see Client and Request Options. with_options(...) lets you set client-wide defaults that these per-call kwargs still override.
Wait mode and delivery status#
Pass 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 for the full contract. In the Python SDK, opt in with wait=True:
result = client.send(
from_email="support@example.com",
to="alice@example.com",
subject="Hello",
body_text="Hi there",
wait=True,
wait_timeout_ms=5000,
)
print(result.delivery_status) # "delivered", "bounced", "deferred", or "wait_timeout"
print(result.smtp_response_code) # e.g. 250
print(result.smtp_response_text) # e.g. "250 OK"
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:
client = primitive.client(api_key="prim_test", timeout=60.0)
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.
Threading a send manually#
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.
from primitive.client import SendThread
client.send(
from_email="support@example.com",
to="alice@example.com",
subject="Re: Hello",
body_text="Following up",
thread=SendThread(
in_reply_to="<parent@example.com>",
references=["<root@example.com>", "<parent@example.com>"],
),
)
Error handling#
A 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:
from primitive.client import PrimitiveAPIError
try:
client.send(
from_email="support@example.com",
to="alice@example.com",
subject="Hello",
body_text="Hi there",
)
except PrimitiveAPIError as err:
print(err.status_code, err.code, err.request_id)
For the full error catalog and what triggers each code, see Python SDK Error Reference.
Next steps#
Continue an inbound thread with client.reply or forward it with client.forward.
Client and Request OptionsSet per-call timeouts, extra headers, and client-wide defaults with with_options.
Inbound and Outbound Email ModelUnderstand the wait-mode delivery statuses shared across every SDK.
Python SDK Error ReferenceLook up every PrimitiveAPIError code and the fix for each.
Was this page helpful?