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"],
)
Slack params
Slack replies only accept text. CC, BCC, subject, and HTML are rejected as invalid for Slack.
await event.reply(text="👀 on it. will report back.")
The reply is posted via chat.postMessage back into the channel the event came from, threaded under the originating message (thread_ts set to the event’s thread_ts if present, otherwise the message’s ts — i.e. replies always attach to the conversation, not the channel’s main feed).
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, email and slack accept replies. webhook and cron do not — there’s no inbound conversation to reply into.
Limits
Email replies are capped to protect customer domain reputation:
| Field | Limit |
|---|---|
cc + bcc | 5 addresses combined. |
subject | 512 characters. |
text | 256 KB. |
Exceeding any cap raises RequestError with one of the codes
too_many_recipients, subject_too_long, body_too_large.
Threading
Email: no action needed. Nevo stamps the right In-Reply-To and References headers on the reply and adds a Re: prefix when you don’t override the subject. The reply lands in the same conversation in Gmail, Outlook, Apple Mail.
Slack: also automatic. A reply to a top-level @mention starts a new thread under that mention; a reply to a message already in a thread posts into that same thread.
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.