Two ways to send a reply — inside a handler, or explicitly with the client.
# Inside a handler — the SDK resolves the active client via the current async context.
@client.on_event()
async def handle(event):
if event.type == "email.received":
await event.reply(text="Got it. We're on it.")
# Anywhere you have an event + a client.
await client.reply(event, text="Got it.")
Both return a ReplyReceipt:
receipt = await event.reply(text="hi")
receipt.reply_id # str — server-side UUID
receipt.accepted_at # datetime — server's accept time
Email params
Email replies accept these in addition to text:
await event.reply(
text="Plain-text body (required).",
html="<p>Optional rich body.</p>",
subject="Optional subject override (defaults to 'Re: <original>').",
cc=["ops@acme.com"],
bcc=["archive@acme.com"],
)
Per-channel param validation
The SDK carries an allowlist per channel type. Passing an email param on a non-email event raises RequestError before any network call — no silent drops:
# event.type == "webhook.received"
await event.reply(text="hi", subject="oops")
# ↑ RequestError: subject is not supported for webhook replies.
Channels that don’t support replies at all raise UnsupportedChannelError:
# event.type == "webhook.received"
await event.reply(text="hi")
# ↑ UnsupportedChannelError: replies are not supported for 'webhook' channels yet.
Today the only channel that accepts replies is email. More soon.
Threading
You don’t need to do anything. The backend reads the original email’s Message-ID, stamps In-Reply-To + References on the reply, and builds the subject with a Re: prefix when you don’t override it. The reply lands in the same conversation in Gmail, Outlook, Apple Mail.
Errors
| Exception | When | Retry? |
|---|---|---|
UnsupportedChannelError | Channel doesn’t support replies. | No. |
RequestError | Client-side validation or server 4xx. | No. |
ServerError | Server 5xx after retries. | Yes (later). |
NetworkError | Network failure after retries. | Yes (later). |
AuthError | Token rejected. | No. Fix key. |
All inherit from ReplyError. Check .is_transient if you need to gate retry logic:
try:
await event.reply(text="hi")
except ReplyError as exc:
if exc.is_transient:
# queue for a retry later
...
else:
raise
See Errors for the full hierarchy.