---
title: "Send a message"
method: POST
path: "/send"
tags: ["Sending"]
---

# Send a message

`POST /send`

## Request

- Base URL: `https://api.oveyon.com/v1`
- URL: `https://api.oveyon.com/v1/send`
- Auth: HTTP bearer

Queues a message for up to 5 recipients (to + cc + bcc). Each recipient is one quota unit. `Idempotency-Key` (header) or `idempotencyKey` (body) deduplicates safely; `X-Oveyon-Sandbox: 1` accepts and freezes without delivering. On a 500, only retry with an idempotencyKey.

## Headers

- `Idempotency-Key` string
- `X-Oveyon-Sandbox` '1'

## Request body

- SendRequest
  - `hold` boolean: F3b — hold the message for HUMAN approval instead of delivering: accepted, signed and stored, not sent. The 202 answers status `held` with approve/reject URLs; the human decides in the portal, by Telegram, or by POST /v1/messages/{id}/approve|reject (scope approve:holds — the key that sends is not the key that approves). Default TTL 24 h, max 7 days; expired drafts never go out. Counts one quota unit per recipient at acceptance; rejected/expired refunds it. Ignored (status `sandbox`) when the key itself is in sandbox.
  - `holdTtl` integer: Seconds until the draft expires (default 86400).
  - `holdNote` string: What the agent wants the approver to know.
  - `from` string, required: `a@b.com` or `Name <a@b.com>`; the domain must be verified in the account. Exactly one address: the display name cannot carry another address, and a bare address is the whole value (`a@b.com (Name)` and `Name a@b.com` are 400). At most 998 characters (the RFC 5322 line limit). The display name may have accents; the address is ASCII only (an IDN domain in xn-- form), with no Unicode space next to it — otherwise 400 with `reason: address_not_ascii`.
  - `to` union, required: One address or a list; at most 5 recipients across to, cc and bcc. A string may carry several, comma-separated. `"Name <a@b.com>"` is accepted too. Each item must be exactly `a@b.com` or `Name <a@b.com>`: two addresses separated by whitespace, a name without <…>, a comment after a bare address, a group, an item with no address, or @ in the display name are 400 bad_recipient — never rewritten. A Unicode space next to the address is 400 with `reason: address_not_ascii`.
    - string, email: ASCII only: no accents; an international (IDN) domain only in its xn-- (punycode) form. Anything else is refused with `reason: address_not_ascii`.
    - string[]
  - `cc` union: One address or a list; at most 5 recipients across to, cc and bcc. A string may carry several, comma-separated. `"Name <a@b.com>"` is accepted too. Each item must be exactly `a@b.com` or `Name <a@b.com>`: two addresses separated by whitespace, a name without <…>, a comment after a bare address, a group, an item with no address, or @ in the display name are 400 bad_recipient — never rewritten. A Unicode space next to the address is 400 with `reason: address_not_ascii`.
    - string, email: ASCII only: no accents; an international (IDN) domain only in its xn-- (punycode) form. Anything else is refused with `reason: address_not_ascii`.
    - string[]
  - `bcc` union: One address or a list; at most 5 recipients across to, cc and bcc. A string may carry several, comma-separated. `"Name <a@b.com>"` is accepted too. Each item must be exactly `a@b.com` or `Name <a@b.com>`: two addresses separated by whitespace, a name without <…>, a comment after a bare address, a group, an item with no address, or @ in the display name are 400 bad_recipient — never rewritten. A Unicode space next to the address is 400 with `reason: address_not_ascii`.
    - string, email: ASCII only: no accents; an international (IDN) domain only in its xn-- (punycode) form. Anything else is refused with `reason: address_not_ascii`.
    - string[]
  - `subject` string
  - `html` string
  - `text` string: At least one of `html`/`text` is required — unless `templateId` is used.
  - `templateId` union: Numeric id or `name` of a published template. Mutually exclusive with subject/html/text.
    - integer
    - string
  - `data` object: Template variables.
  - `version` integer: Pins a published template version.
  - `headers` object: Extra headers. List-Unsubscribe, X-Report-Abuse and X-Oveyon-* are ignored. Each value is a string (or a list of strings); an object is 400. Address headers (Reply-To, Sender, Cc, Disposition-Notification-To…) follow the same rules as the addresses above — ASCII only, and read like `to` (a group is allowed and each member is checked).
  - `idempotencyKey` string: Optional task/order key (alternative to the Idempotency-Key header). Within the account, a retained key returns the original message id for 24 hours from its first claim. After expiry a retry can create another message.
  - `attachments` Attachment[]: Up to 20 attachments, ≤ 15 MB in total.
    - `filename` string, required
    - `content` string, required: Base64-encoded content.
    - `contentType` string
  - `sandbox` boolean: true accepts and freezes without delivering (or header X-Oveyon-Sandbox: 1).
  - `return_path` 'sender' | 'platform': The envelope sender (Return-Path) of THIS message; without it, the key's default applies (set under Credentials). `platform` (the default for new keys): a per-message platform address — bounces come back to us, show up on the message, fire the bounce webhook and suppress addresses that do not exist. `sender`: the envelope is the From address itself, stable across messages — for a personal mailbox and mailing lists (list confirmation, greylisting, allowlists by envelope sender); a bounce the destination sends back goes straight to the sender's mailbox and is NOT tracked (no suppression, no bounce webhook) — an immediate refusal our server sees still shows up in the message status. `sender` requires the From domain to authorize us: its SPF must have our `include:` (and the `_mailchannels` TXT when that relay is active) — otherwise 422 return_path_not_authorized. `sender` is for paid plans: on an account without one it is 403 return_path_requires_paid_plan (unless our team switched the key to sender as an exception); a key whose default is `sender` on an account that moved to a free plan simply sends with the platform address. Any other value (an address included) is 400.

## Response `200`

Duplicate: the idempotencyKey was already used; returns the original id.

- DuplicateResponse
  - `id` string, uuid, required: The id of the ORIGINAL message (the one the idempotencyKey created).
  - `status` 'duplicate', required
  - `message` string, required

## Other responses

- `202`: Accepted (`accepted`) or frozen in sandbox (`sandbox`).
- `400`: bad_request, too_many_recipients, bad_recipient, too_many_attachments, bad_attachment, template_conflict, template_var_missing. `bad_request` also covers a `return_path` other than `sender` or `platform`.
- `401`: Missing or invalid API key.
- `403`: return_path_requires_paid_plan — `return_path: "sender"` on an account without a paid plan (nothing is queued, no quota is used; upgrade, or send with `return_path: "platform"`). Or any of the common 403s: insufficient_scope, scope_blocked_by_approval_lock, key_disabled, sending_disabled, account_suspended, account_unknown, ip_not_allowed. 403 is not a limit: do not retry.
- `413`: attachments_too_large (attachments above 15 MB), or payload_too_large (the request body above 25 MB, with `limit`).
- `415`: unsupported_media_type — send the body as `application/json` (form and plain-text bodies are not accepted).
- `422`: domain_not_verified, domain_on_hold (sending from the domain is held for review: recently registered, listed on the Spamhaus DBL, or flagged by the reputation screening — the message says which and what to do), domain_not_allowed_for_credential (the key is limited to selected domains of the account and the `from` domain is not one of them — adjust it under Credentials; the domain itself is fine), managed_domain_requires_paid_plan (the `from` is your instant inbox `<code>.oveyon.app` and the account is on the free plan — the instant inbox only receives there), invalid_recipient_domain, recipient_suppressed, recipient_blocked, recipient_not_allowed, policy_refused, unsub_footer_multi_recipient, template_not_found and template render errors. return_path_not_authorized: the message goes out with the sender as Return-Path (`return_path: "sender"`, or the key's default) and the From domain does not authorize it yet — the response carries `reason` (`spf_include_missing`, `spf_multiple` or `mailchannels_lockdown_missing`), `domain`, and what to publish: `include` (the SPF mechanism to add) or `record` (`{ type, name, value }`, the `_mailchannels` TXT). Nothing is queued and no quota is used; publish it and retry, or send this message with `return_path: "platform"`. The `message` is localized (Portuguese for callers from Brazil, English otherwise). return_path_managed_domain (with `domain`): `return_path: "sender"` from your instant inbox `<code>.oveyon.app` — that domain is ours and has nothing for you to publish; send with `return_path: "platform"` (a key whose default is `sender` sends from it with the platform address).
- `429`: Per-key limit (`rate_limited`, 600 requests/min by default per API key — or per MCP OAuth connection —, from any IP), per-IP limit for requests WITHOUT a valid credential (`rate_limited`, 600/min by default), too many unknown or malformed credentials from your address (`too_many_auth_failures`, 20/min by default, counted per IPv4 address or per IPv6 /48; a missing `Authorization`, an expired OAuth token or a disabled key do not count, and anything without the shape of one of our keys counts without being checked; from an address over that limit a valid key still gets through within 120 credential checks/min, and above them every credential not validated in the last 5 minutes gets this 429 until the minute turns) — all checked before the route, on every operation —, account quota (`quota_exceeded` with `window`, `used`, `limit`), warmup cap (`warmup_cap_reached`) or saturation (`queue_full`). Honor `Retry-After` with exponential backoff and jitter.
- `500`: internal_error — an unexpected failure on our side. The detail stays in our logs, never in the body. Retry reads; retry a write only when it is idempotent or carries an idempotency key.
- `503`: injection_failed — the message could not be queued; the quota is refunded, retry with the same idempotencyKey. Or ip_rules_unavailable. Or return_path_check_unavailable — the message asked for the sender as Return-Path and the DNS of the From domain could not be checked right now (not a refusal): nothing was queued, retry after `Retry-After` (60 s); the localized `message` names the domain.

---

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