{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/python-reply-forward","markdown_url":"https://test.abhinandan.one/python-reply-forward.md","article":{"id":"fc56f8ad-47ea-40e9-8c6b-f1df919e3206","article_slug":"python-reply-forward","parent_article_slug":null,"parent_article_title":null,"kind":"guide","published_at":"2026-08-11T18:55:01.061383+00:00","keywords":["client.reply","client.forward","PrimitiveClient reply","inbound_not_repliable","reply threading Python SDK","forward inbound email"],"meta_description":"client.reply(email, text) threads a reply server-side using the parent's Message-Id, while client.forward(email, to=...) sends a new message with a synthesized body.","og_image_url":null,"source_file_paths":["sdk-python/src/primitive/client.py","sdk-python/src/primitive/received_email.py","sdk-python/README.md","sdk-python/tests/test_client.py"],"recording_id":null,"replayable":false,"task_name":"Replying and Forwarding","category":"Python SDK","summary":null,"description":"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.","content_kind":"repo_page","content_markdown":"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](python-receive-email), both methods take that object directly.\n\nBoth calls go through the [`PrimitiveClient`](python-sdk-quickstart) you already constructed with your API key:\n\n```python\nimport primitive\n\nclient = primitive.client(api_key=\"prim_test\")\n```\n\n## Reply to an inbound email\n\n`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.\n\n<Steps>\n\n<Step title=\"Call reply with the received email and a body\">\n\n```python\nimport primitive\n\nclient = primitive.client(api_key=\"prim_test\")\n\n\ndef webhook_handler(body: bytes, headers: dict[str, str]) -> dict[str, object]:\n    email = primitive.receive(\n        body=body,\n        headers=headers,\n        secret=\"whsec_...\",\n    )\n\n    client.reply(email, \"Thank you for your email.\")\n    return {\"ok\": True}\n```\n\nA bare string is treated as the reply's `text`.\n\n</Step>\n\n<Step title=\"Verify the result\">\n\n`client.reply` returns a `SendResult` with the same shape as `client.send`:\n\n```python\nresult = client.reply(email, \"Thank you for your email.\")\nprint(result.id, result.status, result.accepted)\n```\n\n`result.accepted` lists the recipient the server resolved from the inbound row; you never had to supply it.\n\n</Step>\n\n</Steps>\n\n### Reply with HTML, attachments, or a wait\n\nPass a dict instead of a bare string to set `html` alongside `text`, attach files, or wait for delivery:\n\n```python\nattachment: primitive.SendAttachment = {\n    \"filename\": \"report.txt\",\n    \"content_base64\": \"aGVsbG8=\",\n}\n\nclient.reply(\n    email,\n    {\n        \"text\": \"Thanks for your email.\",\n        \"html\": \"<p>Thanks for your email.</p>\",\n        \"attachments\": [attachment],\n        \"wait\": True,\n    },\n)\n```\n\n`wait` mirrors `client.send`'s [wait mode](email-model): 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.\n\n### Reply from a different address\n\n`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:\n\n```python\nclient.reply(\n    email,\n    \"Thanks for your email.\",\n    from_email=\"notifications@outbound.example.com\",\n)\n```\n\nThe server still validates that the from-domain is a verified outbound domain for your org, so this override carries no extra privilege.\n\n<Tip>\n\nWhen you pass a dict, `reply()` reads `text`, `html`, `from`, `attachments`, and `wait` from it. A `subject` key raises a `ValueError`, see below.\n\n</Tip>\n\n### Why `subject` is rejected\n\n`reply()` intentionally does not accept a subject override:\n\n```python\nclient.reply(email, {\"text\": \"Thanks\", \"subject\": \"Custom subject\"})\n# ValueError: subject overrides are not supported on reply: a custom subject\n# breaks Gmail's threading. Use client.send() if you need full control.\n```\n\nGmail'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`](python-send-email) instead of `reply`.\n\n### When reply fails\n\nIf 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`:\n\n```python\nimport primitive\nfrom primitive.client import PrimitiveAPIError\n\nclient = primitive.client(api_key=\"prim_test\")\n\ntry:\n    client.reply(email, \"Thanks!\")\nexcept PrimitiveAPIError as err:\n    if err.code == \"inbound_not_repliable\":\n        # Fall back to client.send() with an explicit `to`.\n        ...\n```\n\nSee [Python SDK Error Reference](python-errors-reference) for the full error catalog.\n\n## Forward an inbound email to a new recipient\n\n`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.\n\n```python\nclient.forward(\n    email,\n    to=\"ops@example.com\",\n    body_text=\"Can you take this one?\",\n)\n```\n\nUnlike `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.\n\nThe forwarded body always includes:\n\n- your optional intro text (`body_text`), if given\n- a `---------- Forwarded message ----------` marker\n- `From`, `To`, `Subject`, and (when present) `Date` and `Message-ID` lines copied from the original\n- the original email's plain-text body\n\n<Warning>\n\nForward 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`.\n\n</Warning>\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Receiving and Parsing Inbound Email\" href=\"python-receive-email\">\n\nGet the ReceivedEmail object that reply and forward both take as their first argument.\n\n</Card>\n\n<Card title=\"Sending Email\" href=\"python-send-email\">\n\nUse client.send directly when you need a custom subject or a brand-new thread.\n\n</Card>\n\n<Card title=\"Inbound and Outbound Email Model\" href=\"email-model\">\n\nUnderstand wait mode and the delivery statuses shared by send, reply, and forward.\n\n</Card>\n\n<Card title=\"Python SDK Error Reference\" href=\"python-errors-reference\">\n\nLook up inbound_not_repliable and every other PrimitiveAPIError code.\n\n</Card>\n\n</CardGroup>","canonical_base_url":"https://test.abhinandan.one","seo_indexing_enabled":true,"last_modified":"2026-08-21T18:22:43.359885+00:00","video_url":null,"voiceover_url":null,"tools_used":[],"demonstrated_by":[],"steps":[],"related_links":[],"intro":null,"prerequisites":[],"verification":[],"troubleshooting":[],"suggest_edit_url":"https://github.com/abhi-browzer/primitive-sdks/edit/main/sdk-python/src/primitive/client.py","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Replying+and+Forwarding&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fpython-reply-forward","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}