---
title: "Replying and Forwarding"
canonical: "https://test.abhinandan.one/python-reply-forward"
markdown_url: "https://test.abhinandan.one/python-reply-forward.md"
publisher: "Primitive SDKs"
kind: "guide"
content_type: "reference"
category: "Python SDK"
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."
keywords: ["client.reply", "client.forward", "PrimitiveClient reply", "inbound_not_repliable", "reply threading Python SDK", "forward inbound email"]
last_modified: "2026-08-21T18:22:43.359885+00:00"
published_at: "2026-08-11T18:55:01.061383+00:00"
source_files:
  - "sdk-python/src/primitive/client.py"
  - "sdk-python/src/primitive/received_email.py"
  - "sdk-python/README.md"
  - "sdk-python/tests/test_client.py"
sections:
  - {anchor: "reply-to-an-inbound-email", title: "Reply to an inbound email"}
  - {anchor: "step-call-reply-with-the-received-email-and-a-body", title: "Call reply with the received email and a body"}
  - {anchor: "step-verify-the-result", title: "Verify the result"}
  - {anchor: "reply-with-html-attachments-or-a-wait", title: "Reply with HTML, attachments, or a wait"}
  - {anchor: "reply-from-a-different-address", title: "Reply from a different address"}
  - {anchor: "why-subject-is-rejected", title: "Why `subject` is rejected"}
  - {anchor: "when-reply-fails", title: "When reply fails"}
  - {anchor: "forward-an-inbound-email-to-a-new-recipient", title: "Forward an inbound email to a new recipient"}
  - {anchor: "next-steps", title: "Next steps"}
---

> Documentation index: https://test.abhinandan.one/llms.txt

# 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](https://test.abhinandan.one/python-receive-email.md), both methods take that object directly.

Both calls go through the [`PrimitiveClient`](https://test.abhinandan.one/python-sdk-quickstart.md) you already constructed with your API key:

```python
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

```python
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.reply` returns a `SendResult` with the same shape as `client.send`:

```python
result = client.reply(email, "Thank you for your email.")
print(result.id, result.status, result.accepted)
```

`result.accepted` lists 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:

```python
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](https://test.abhinandan.one/email-model.md): 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:

```python
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.

> **Tip:** 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:

```python
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`](https://test.abhinandan.one/python-send-email.md) 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`:

```python
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](https://test.abhinandan.one/python-errors-reference.md) 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.

```python
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) `Date` and `Message-ID` lines copied from the original
- the original email's plain-text body

> **Warning:** 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`.
