Replying and Forwarding
Reply to an inbound email with server-derived threading using client.reply, or forward it to a new recipient with client.forward, both without hand-building headers.
Use client.reply to answer an inbound email in the same thread, and client.forward to hand it off to a new recipient. Reach for these once you have a ReceivedEmail from Receiving and Parsing Inbound Email, both methods take that object directly.
Both calls go through the PrimitiveClient you already constructed with your API key:
import primitive
client = primitive.client(api_key="prim_test")
Reply to an inbound email#
client.reply(email, text), the high-level reply call on PrimitiveClient, posts to the server's /emails/{id}/reply endpoint and returns a SendResult. The server derives the recipient, the Re: <parent> subject, and the threading headers (In-Reply-To, References) from the inbound row identified by email.id, you only control the body and a small set of overrides.
- 1
Call reply with the received email and a body#
import primitive client = primitive.client(api_key="prim_test") def webhook_handler(body: bytes, headers: dict[str, str]) -> dict[str, object]: email = primitive.receive( body=body, headers=headers, secret="whsec_...", ) client.reply(email, "Thank you for your email.") return {"ok": True}A bare string is treated as the reply's
text. - 2
Verify the result#
client.replyreturns aSendResultwith the same shape asclient.send:result = client.reply(email, "Thank you for your email.") print(result.id, result.status, result.accepted)result.acceptedlists the recipient the server resolved from the inbound row; you never had to supply it.
Reply with HTML, attachments, or a wait#
Pass a dict instead of a bare string to set html alongside text, attach files, or wait for delivery:
attachment: primitive.SendAttachment = {
"filename": "report.txt",
"content_base64": "aGVsbG8=",
}
client.reply(
email,
{
"text": "Thanks for your email.",
"html": "<p>Thanks for your email.</p>",
"attachments": [attachment],
"wait": True,
},
)
wait mirrors client.send's wait mode: pass wait=True to hold the response open until the first downstream SMTP delivery outcome, or until wait_timeout_ms elapses (default 30000 ms), instead of returning as soon as Primitive accepts the message.
Reply from a different address#
reply() defaults the From address to the inbound recipient, the address that received the original email. If your verified outbound domain differs from your inbound domain, pass from_email explicitly:
client.reply(
email,
"Thanks for your email.",
from_email="notifications@outbound.example.com",
)
The server still validates that the from-domain is a verified outbound domain for your org, so this override carries no extra privilege.
When you pass a dict, reply() reads text, html, from, attachments, and wait from it. A subject key raises a ValueError, see below.
Why subject is rejected#
reply() intentionally does not accept a subject override:
client.reply(email, {"text": "Thanks", "subject": "Custom subject"})
# ValueError: subject overrides are not supported on reply: a custom subject
# breaks Gmail's threading. Use client.send() if you need full control.
Gmail's Conversation View requires both a References match and a normalized-subject match to thread a message. A custom subject silently breaks threading for a portion of your recipients. If you need full subject control, use client.send instead of reply.
When reply fails#
If the inbound row is not in a state Primitive can reply to, no Message-Id was recorded, or the content was discarded, the API returns inbound_not_repliable (HTTP 422) and the SDK raises PrimitiveAPIError:
import primitive
from primitive.client import PrimitiveAPIError
client = primitive.client(api_key="prim_test")
try:
client.reply(email, "Thanks!")
except PrimitiveAPIError as err:
if err.code == "inbound_not_repliable":
# Fall back to client.send() with an explicit `to`.
...
See Python SDK Error Reference for the full error catalog.
Forward an inbound email to a new recipient#
client.forward(email, to=..., body_text=...) builds a brand-new outbound message addressed to to, quoting the original sender, recipient, subject, and body in a synthesized "Forwarded message" block.
client.forward(
email,
to="ops@example.com",
body_text="Can you take this one?",
)
Unlike reply, forward is not threaded to the original message, it is a fresh client.send call under the hood. from_ defaults to the address that received the inbound email; subject defaults to email.forward_subject (Fwd: <original subject>), and both can be overridden.
The forwarded body always includes:
- your optional intro text (
body_text), if given - a
---------- Forwarded message ----------marker From,To,Subject, and (when present)DateandMessage-IDlines copied from the original- the original email's plain-text body
Forward does not carry over the original email's attachments. If you need the recipient to receive the original attachments, extract them from email.raw and pass them explicitly via attachments on client.send.
Next steps#
Get the ReceivedEmail object that reply and forward both take as their first argument.
Sending EmailUse client.send directly when you need a custom subject or a brand-new thread.
Inbound and Outbound Email ModelUnderstand wait mode and the delivery statuses shared by send, reply, and forward.
Python SDK Error ReferenceLook up inbound_not_repliable and every other PrimitiveAPIError code.
Was this page helpful?