---
title: "Reply to a message"
method: POST
path: "/v1/email/messages/{messageId}/reply"
tags: ["Email"]
---

# Reply to a message

`POST /v1/email/messages/{messageId}/reply`

## Request

- Base URL: `https://zernio.com/api`
- URL: `https://zernio.com/api/v1/email/messages/{messageId}/reply`
- Auth: HTTP bearer

Replies inside the provider's thread: the provider builds the quoted original and threading headers, your `html` or `text` goes above the quote. `to`, `cc`, `bcc`, `replyTo`, `subject`, `headers` and `attachments` are honoured; `scheduledFor` and `holdAsDraft` are rejected like on send. Same protection, caps and headers as sending.

## Path parameters

- `messageId` string, required

## Request body

- object
  - `accountId` string, required
  - `to` EmailAddress[], required
    - `email` string, email, required
    - `name` string, nullable
  - `cc` EmailAddress[]
    - `email` string, email, required
    - `name` string, nullable
  - `bcc` EmailAddress[]
    - `email` string, email, required
    - `name` string, nullable
  - `replyTo` EmailAddress[]
    - `email` string, email, required
    - `name` string, nullable
  - `fromName` string
  - `subject` string, required: Optional on replies; the provider keeps the thread subject.
  - `text` string: Plain-text body. One of `text` or `html` is required.
  - `html` string
  - `attachments` object[]: 25 MB in total; 3 MB per attachment on Microsoft.
    - `filename` string, required
    - `contentType` string, required
    - `url` string, uri: Fetched at send time. Exactly one of `url` or `base64`.
    - `base64` string
    - `contentId` string: Makes the attachment inline (`cid:` reference in the html).
  - `headers` object: Custom `X-` headers only. `List-Unsubscribe` is dropped on Microsoft and reported in `metadata.droppedHeaders`.
  - `metadata` object
  - `scheduledFor` string, date-time: Reserved; currently rejected.
  - `holdAsDraft` boolean: Reserved; currently rejected.
  - `trackOpens` boolean: Not available; must be false.
  - `trackLinks` boolean: Not available; must be false.
  - `mode` 'reply' | 'replyAll'

## Response `201`

The sent reply

- object
  - `data` EmailMessage
    - `id` string: The provider's message id.
    - `threadId` string
    - `accountId` string
    - `provider` 'google' | 'microsoft'
    - `folders` string[]
    - `from` EmailAddress
      - `email` string, email, required
      - `name` string, nullable
    - `to` EmailAddress[]
      - `email` string, email, required
      - `name` string, nullable
    - `cc` EmailAddress[]
      - `email` string, email, required
      - `name` string, nullable
    - `bcc` EmailAddress[]
      - `email` string, email, required
      - `name` string, nullable
    - `replyTo` EmailAddress[]
      - `email` string, email, required
      - `name` string, nullable
    - `subject` string
    - `snippet` string
    - `text` string, nullable
    - `html` string, nullable
    - `attachments` EmailAttachment[]
      - `id` string
      - `filename` string
      - `contentType` string
      - `size` integer
      - `isInline` boolean
      - `contentId` string, nullable
    - `headers` object: Only with `includeHeaders=true`.
    - `raw` string: MIME source, only with `raw=true`.
    - `date` string, date-time
    - `unread` boolean
    - `starred` boolean
    - `metadata` object

## Other responses

- `400`: Invalid request
- `401`: Missing or invalid API key. `code` is `missing_credentials` when no Authorization header was sent and `invalid_credentials` when the key is unknown, revoked or expired.
- `402`: Payment method or enterprise contract required: the authenticated account hit a billing gate before the connection could proceed. Switch on `reason` to render the right prompt: - `free_tier_exceeded`: the team has connected more accounts than the free tier allows. Add a payment method to continue (billed per additional connected account). - `twitter_passthrough`: connecting an X account requires a card on file from day one, because X API calls incur real per-call pass-through costs. Applies to the 1st X account, not only the 3rd+. - `enterprise_required`: the team's enterprise contract sets a connected-account cap and it has been reached. Self-service teams have NO cap (the $1/account rate continues at any scale). The user already has a card on file; this gate is about contract terms. - `card_verification_required`: the card on file was flagged at add time (prepaid, or issued in a different country than the billing address). A one-time verification charge of `details.verification_amount_cents` (USD) is pending; it is credited to the team's usage balance, not a fee. **What to do:** redirect the end-user to `dashboard_url`. - `free_tier_exceeded`, `twitter_passthrough`, `card_verification_required`: it opens the Zernio billing page (the add-payment-method drawer, or the verification step). The request succeeds once the card is on file (or verified, or paid). - `enterprise_required`: it opens the enterprise contact form (not the billing tab), to adjust the contract's limit.
- `404`: Resource not found
- `412`: The connected account cannot serve this request until something outside the request changes. Two codes: - `account_scope_missing`: the account was connected without the area this endpoint needs (`details.grantedAreas`, `details.missingArea`), or Google reports the grant lacks a permission. Reconnect with a wider `scopes=`. - `admin_consent_required` (Microsoft): the tenant requires an administrator's approval. `details.adminConsentUrl` is the Microsoft Entra admin-consent link to hand to one; it opens in a browser with no API key and stays valid for one hour. `GET /v1/connect/microsoft/admin-consent` mints a fresh one.
- `429`: The send was refused before reaching the provider. - `email_send_cap_reached`: today's cap for the mailbox is spent. `details.cap` names the limit and `resetsAt`; `retryAfterSeconds` counts to midnight UTC. - `rate_limited`: the mailbox is in a sending cooldown, either from a same-body burst to more than 50 recipients in an hour (`details.cap.kind: recipient_burst`) or an earlier trigger. `retryAfterSeconds` says when it lifts, and `account.rate_limited` fired when it started.

## Changes

- **2026-10-27** `0a24331f5431`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/zernio/apis/zernio-api/changes/v1/email/messages/:messageId/reply/post.md)

---

[API](https://skmtc.dev/zernio/apis/zernio-api.md) - [All operations](https://skmtc.dev/zernio/apis/zernio-api/llms.txt) - [OpenAPI document](https://skmtc.dev/zernio/apis/zernio-api/revisions/c319eb5d95b6?raw)
