---
title: "Reply to one email"
method: POST
path: "/v1/mailboxes/{mailbox_id}/messages/{email_id}/reply"
tags: ["threads"]
---

# Reply to one email

`POST /v1/mailboxes/{mailbox_id}/messages/{email_id}/reply`

## Request

- Base URL: `https://mbag.ai/api`
- URL: `https://mbag.ai/api/v1/mailboxes/{mailbox_id}/messages/{email_id}/reply`
- Auth: HTTP bearer

Fetches the original message, then sends the reply through the
mailbox's operating-mode gate (`policy.mode`): Live sends
immediately; Supervised/Shadow park it as a draft instead (`held:
true`) — `success: true` is returned either way, so callers MUST
check `held` to know whether the reply actually left. A reply that
actually sends first consumes tier send-cap quota; a held reply
consumes nothing.

## Path parameters

- `mailbox_id` integer, required
- `email_id` string, required

## Request body

- ReplyRequest
  - `text_body` string, required

## Response `200`

Reply sent or parked — see `held`.

- ReplyResponse: `held`/`draft_id`/`mode` are omitted entirely (not even `null`) for a plain Live send — they only appear when the reply was parked instead of sent. `success: true` on its own does NOT mean the reply left; callers must check `held`.
  - `draft_id` integer
  - `held` boolean: True when the reply was parked (Supervised/Shadow mode) rather than sent.
  - `mode` 'supervised' | 'shadow': The mailbox's operating mode (`policy.mode`) that caused the hold, present when `held` is true.
  - `success` boolean, required

## Other responses

- `401`: Missing or invalid credential, raised by the `ApiKeyAuth` extractor (`backend/src/api_keys.rs`) before the handler runs: no `Authorization` header, a non-`Bearer` scheme, a key that doesn't hash-match any row, or a key whose status isn't `active`. A header that isn't valid UTF-8, has no scheme token, or has `Bearer` with no key value returns 400 with this same JSON shape instead of 401.
- `404`: Mailbox not found (or not owned by the caller). Note: any database error on this lookup ALSO surfaces as this same 404 (the handler uses `fetch_one().map_err(|_| ...)` without distinguishing "no row" from a query failure).
- `429`: Tier send cap reached (daily or monthly). Unlike `POST .../send`, the error body here is JUST `{"error": "<human-readable message>"}` — there is no separate machine-readable `rule` field; the message text names the plan and links to the pricing page.
- `502`: Failed to fetch the original message via JMAP, or failed to send/park the reply. Message text is a wrapped upstream error string.

---

[API](https://skmtc.dev/mailbuttons/apis/mailbuttons-agent-mail-api.md) · [All operations](https://skmtc.dev/mailbuttons/apis/mailbuttons-agent-mail-api/llms.txt) · [OpenAPI document](https://skmtc.dev/mailbuttons/apis/mailbuttons-agent-mail-api/revisions/fe2761823978?raw)
