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

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

    Create a client#

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

    primitive.client(...) builds a PrimitiveClient. 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. 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, and subject are required. You must supply at least one of body_text or body_html; passing neither raises ValueError("one of body_text or body_html is required") before any request is sent.

  3. 3

    Read the result#

    client.send returns a SendResult dataclass:

    FieldTypeMeaning
    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_replayboolTrue when this response replays an earlier send with the same idempotency key
    delivery_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_email and to must be non-empty header-safe strings (3–998 chars for from, 3–320 for to).
  • to must be a valid email address or a "Display Name <addr@example.com>" form. An invalid address raises ValueError("to must be a valid email address").
  • subject must be non-empty.
  • One of body_text / body_html is required.
  • wait_timeout_ms, if given, must be between 1000 and 30000.

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.

Tip

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

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#

Was this page helpful?

© Primitive SDKs

Powered by Browzer