English
Messages
Routes are relative to <PUBLIC_BASE_URL>; request and response bodies are JSON unless stated otherwise. Authentication, scopes, pagination and the error format are described in the REST API guides.
GET /api/v1/messages
List mirrored messages
The flat, most-recent-first view of the mirror (not the threaded inbox), cursor-paginated. q searches subject, preview and sender, with the same grammar as GET /api/v1/threads, and never leaves the member’s mailboxes.
Access — Member session or API key with scope messages:read.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
mailboxId | query | string | no |
cursor | query | string | no |
limit | query | integer | no |
q | query | string | no |
Responses
200 — A page of messages.
| Field | Type | Required |
|---|---|---|
messages | object[] | yes |
nextCursor | string | null | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"messages": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"mailboxId": { "type": "string" },
"from": {
"anyOf": [
{
"type": "object",
"properties": { "name": { "type": "string" }, "email": { "type": "string" } },
"required": [ "email" ],
"additionalProperties": false
},
{ "type": "null" }
]
},
"subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"snippet": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"receivedAt": { "type": "string" },
"folderLabels": { "type": "array", "items": { "type": "string" } },
"flags": {
"type": "object",
"properties": {
"seen": { "type": "boolean" },
"flagged": { "type": "boolean" },
"draft": { "type": "boolean" },
"sent": { "type": "boolean" }
},
"required": [ "seen", "flagged", "draft", "sent" ],
"additionalProperties": false
},
"signals": {
"type": "object",
"properties": {
"isAutoReply": { "type": "boolean" },
"isNoReply": { "type": "boolean" },
"isMailingList": { "type": "boolean" },
"isFromSelf": { "type": "boolean" }
},
"required": [ "isAutoReply", "isNoReply", "isMailingList", "isFromSelf" ],
"additionalProperties": false
},
"hasAttachments": { "type": "boolean" }
},
"required": [
"id",
"mailboxId",
"from",
"subject",
"snippet",
"receivedAt",
"folderLabels",
"flags",
"signals",
"hasAttachments"
],
"additionalProperties": false
}
},
"nextCursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ] }
},
"required": [ "messages", "nextCursor" ],
"additionalProperties": false
}400 — The request does not match its schema.
401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
Error codes — request.bad_request. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.
GET /api/v1/messages/{id}
Read a message
The full message: headers, recipients, attachments list and bodyHtmlSafe, sanitised on every call and never stored. Render that HTML in a sandboxed frame, never inline.
Access — Member session or API key with scope messages:read.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
id | path | string (uuid) | yes |
Responses
200 — The message.
| Field | Type | Required |
|---|---|---|
id | string | yes |
mailboxId | string | yes |
from | object | null | yes |
subject | string | null | yes |
snippet | string | null | yes |
receivedAt | string | yes |
folderLabels | string[] | yes |
flags | object | yes |
signals | object | yes |
hasAttachments | boolean | yes |
threadId | string | null | yes |
classification | object | yes |
bodyText | string | null | yes |
bodyHtmlSafe | string | null | yes |
hasRemoteImages | boolean | yes |
bodyComplete | boolean | yes |
headersSubset | object | yes |
attachments | object[] | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"id": { "type": "string" },
"mailboxId": { "type": "string" },
"from": {
"anyOf": [
{
"type": "object",
"properties": { "name": { "type": "string" }, "email": { "type": "string" } },
"required": [ "email" ],
"additionalProperties": false
},
{ "type": "null" }
]
},
"subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"snippet": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"receivedAt": { "type": "string" },
"folderLabels": { "type": "array", "items": { "type": "string" } },
"flags": {
"type": "object",
"properties": {
"seen": { "type": "boolean" },
"flagged": { "type": "boolean" },
"draft": { "type": "boolean" },
"sent": { "type": "boolean" }
},
"required": [ "seen", "flagged", "draft", "sent" ],
"additionalProperties": false
},
"signals": {
"type": "object",
"properties": {
"isAutoReply": { "type": "boolean" },
"isNoReply": { "type": "boolean" },
"isMailingList": { "type": "boolean" },
"isFromSelf": { "type": "boolean" }
},
"required": [ "isAutoReply", "isNoReply", "isMailingList", "isFromSelf" ],
"additionalProperties": false
},
"hasAttachments": { "type": "boolean" },
"threadId": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"classification": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": {}
},
"bodyText": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"bodyHtmlSafe": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"hasRemoteImages": { "type": "boolean" },
"bodyComplete": { "type": "boolean" },
"headersSubset": {
"type": "object",
"properties": {
"to": {
"type": "array",
"items": {
"type": "object",
"properties": { "name": { "type": "string" }, "email": { "type": "string" } },
"required": [ "email" ],
"additionalProperties": false
}
},
"cc": {
"type": "array",
"items": {
"type": "object",
"properties": { "name": { "type": "string" }, "email": { "type": "string" } },
"required": [ "email" ],
"additionalProperties": false
}
},
"replyTo": {
"type": "array",
"items": {
"type": "object",
"properties": { "name": { "type": "string" }, "email": { "type": "string" } },
"required": [ "email" ],
"additionalProperties": false
}
},
"date": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"messageIdHeader": { "anyOf": [ { "type": "string" }, { "type": "null" } ] }
},
"required": [ "to", "cc", "replyTo", "date", "messageIdHeader" ],
"additionalProperties": false
},
"attachments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"position": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"filename": { "type": "string" },
"mime": { "type": "string" },
"size": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"disposition": { "type": "string", "enum": [ "attachment", "inline" ] },
"contentId": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"downloadUrl": { "type": "string" }
},
"required": [
"id",
"position",
"filename",
"mime",
"size",
"disposition",
"contentId",
"downloadUrl"
],
"additionalProperties": false
}
}
},
"required": [
"id",
"mailboxId",
"from",
"subject",
"snippet",
"receivedAt",
"folderLabels",
"flags",
"signals",
"hasAttachments",
"threadId",
"classification",
"bodyText",
"bodyHtmlSafe",
"hasRemoteImages",
"bodyComplete",
"headersSubset",
"attachments"
],
"additionalProperties": false
}401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
Error codes — request.not_found. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.
GET /api/v1/messages/{id}/attachments/{position}
Download an attachment
The raw bytes of the attachment at this MIME position, served as a download (Content-Disposition: attachment, nosniff, no-store). Active types (HTML, SVG, scripts) are forced to application/octet-stream. Out of scope, unknown position or purged blob: one and the same 404.
Access — Member session or API key with scope messages:read.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
id | path | string (uuid) | yes |
position | path | integer | yes |
Responses
200 — The file.
Content type : application/octet-stream
401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
Error codes — request.not_found. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.
GET /api/v1/messages/{id}/journal
Why did this message trigger what it did
The journal entry of one message and the executions it started.
Access — Member session or API key with scope messages:read.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
id | path | string (uuid) | yes |
Responses
200 — The entry and its executions.
| Field | Type | Required |
|---|---|---|
entry | object | null | yes |
executions | object[] | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"entry": {
"anyOf": [
{
"type": "object",
"properties": {
"id": { "type": "string" },
"outcome": {
"type": "string",
"enum": [
"excluded_org",
"excluded_member",
"guardrail",
"unmatched",
"dispatched"
]
},
"ruleRef": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"createdAt": { "type": "string" }
},
"required": [ "id", "outcome", "ruleRef", "createdAt" ],
"additionalProperties": false
},
{ "type": "null" }
]
},
"executions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"workflowId": { "type": "string" },
"workflowName": { "type": "string" },
"status": {
"type": "string",
"enum": [
"queued",
"running",
"waiting",
"succeeded",
"failed",
"cancelled"
]
},
"simulated": { "type": "boolean" },
"createdAt": { "type": "string" },
"finishedAt": { "anyOf": [ { "type": "string" }, { "type": "null" } ] }
},
"required": [
"id",
"workflowId",
"workflowName",
"status",
"simulated",
"createdAt",
"finishedAt"
],
"additionalProperties": false
}
}
},
"required": [ "entry", "executions" ],
"additionalProperties": false
}401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
Error codes — request.not_found. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.
POST /api/v1/messages/send
Send a message
Composes and queues a message from one of the member’s mailboxes. Nothing leaves synchronously: the response carries the opId of the outbound operation and the mirror reports the real state on the next delta. clientToken is mandatory and deduplicates: a replay returns 202 { duplicate: true } with the same opId. Attachments are references uploaded beforehand. Subject to the sending kill switch (403) and the per-mailbox rate limit (429, with Retry-After and retryAfterMs).
Access — Member session or API key with scope messages:write.
Request body (application/json)
| Field | Type | Required |
|---|---|---|
mailboxId | string | yes |
to | string (email)[] | yes |
cc | string (email)[] | no |
bcc | string (email)[] | no |
subject | string | yes |
bodyText | string | yes |
bodyHtml | string | no |
replyToMessageId | string | no |
forwardOfMessageId | string | no |
includeOriginalAttachments | boolean | no |
quoteOriginal | boolean | no |
attachments | object[] | no |
clientToken | string | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"mailboxId": { "type": "string", "minLength": 1 },
"to": {
"minItems": 1,
"maxItems": 100,
"type": "array",
"items": {
"type": "string",
"maxLength": 320,
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
}
},
"cc": {
"maxItems": 100,
"type": "array",
"items": {
"type": "string",
"maxLength": 320,
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
}
},
"bcc": {
"maxItems": 100,
"type": "array",
"items": {
"type": "string",
"maxLength": 320,
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
}
},
"subject": { "type": "string", "maxLength": 512 },
"bodyText": { "type": "string", "maxLength": 200000 },
"bodyHtml": { "type": "string", "maxLength": 500000 },
"replyToMessageId": { "type": "string", "minLength": 1 },
"forwardOfMessageId": { "type": "string", "minLength": 1 },
"includeOriginalAttachments": { "type": "boolean" },
"quoteOriginal": { "type": "boolean" },
"attachments": {
"maxItems": 20,
"type": "array",
"items": {
"type": "object",
"properties": {
"attachmentId": {
"type": "string",
"minLength": 8,
"maxLength": 128,
"pattern": "^[A-Za-z0-9._-]+$"
}
},
"required": [ "attachmentId" ]
}
},
"clientToken": {
"type": "string",
"minLength": 8,
"maxLength": 128,
"pattern": "^[A-Za-z0-9._:-]+$"
}
},
"required": [ "mailboxId", "to", "subject", "bodyText", "clientToken" ]
}Responses
202 — Queued.
| Field | Type | Required |
|---|---|---|
opId | string | yes |
kind | "send" | "draft" | yes |
duplicate | boolean | yes |
threadId | string | null | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"opId": { "type": "string" },
"kind": { "type": "string", "enum": [ "send", "draft" ] },
"duplicate": { "type": "boolean" },
"threadId": { "anyOf": [ { "type": "string" }, { "type": "null" } ] }
},
"required": [ "opId", "kind", "duplicate", "threadId" ],
"additionalProperties": false
}400 — The request does not match its schema.
401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
This operation accepts an Idempotency-Key header: replaying the same request with the same key returns the original response instead of acting twice. See Idempotency.
Error codes — webmail.bad_request, webmail.mailbox_not_found, webmail.message_not_found, webmail.attachment_not_found, webmail.message_too_large, webmail.sending_disabled, webmail.rate_limited. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.
Example request
json
{
"mailboxId": "0192f1c2-3b4d-7e8f-9a0b-1c2d3e4f5a6b",
"to": [ "bob@example.test" ],
"subject": "Quote #1042",
"bodyText": "Hello Bob,\n\nPlease find the quote attached.\n\nAlice",
"attachments": [ { "attachmentId": "0192f1c2-0000-7000-8000-00000000a7a7" } ],
"clientToken": "b2c7e4a0-9f3d-4c1e-8a6b-5d2f1e0c9b8a"
}Example response (202)
json
{
"opId": "0192f1c2-0000-7000-8000-0000000000aa",
"kind": "send",
"duplicate": false,
"threadId": null
}POST /api/v1/messages/drafts
Create a draft
The same body as /send, saved as a draft in the provider. Neither the kill switch nor the rate limit applies: a draft does not leave. A replayed clientToken is a duplicate, not an overwrite — use PUT to overwrite.
Access — Member session or API key with scope messages:write.
Request body (application/json)
| Field | Type | Required |
|---|---|---|
mailboxId | string | yes |
to | string (email)[] | yes |
cc | string (email)[] | no |
bcc | string (email)[] | no |
subject | string | yes |
bodyText | string | yes |
bodyHtml | string | no |
replyToMessageId | string | no |
forwardOfMessageId | string | no |
includeOriginalAttachments | boolean | no |
quoteOriginal | boolean | no |
attachments | object[] | no |
clientToken | string | yes |
Same schema as POST /api/v1/messages/send.
Responses
202 — Queued.
| Field | Type | Required |
|---|---|---|
opId | string | yes |
kind | "send" | "draft" | yes |
duplicate | boolean | yes |
threadId | string | null | yes |
Same schema as POST /api/v1/messages/send.
400 — The request does not match its schema.
401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
This operation accepts an Idempotency-Key header: replaying the same request with the same key returns the original response instead of acting twice. See Idempotency.
Error codes — webmail.bad_request, webmail.mailbox_not_found, webmail.message_not_found, webmail.attachment_not_found, webmail.message_too_large. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.
PUT /api/v1/messages/drafts
Save a draft (autosave)
Upserts the draft identified by clientToken: the content already saved under this token is rewritten, so one composing session yields one draft however many saves happen. savedAt is the time of this save. Neither the kill switch nor the rate limit applies.
Access — Member session or API key with scope messages:write.
Request body (application/json)
| Field | Type | Required |
|---|---|---|
mailboxId | string | yes |
to | string (email)[] | yes |
cc | string (email)[] | no |
bcc | string (email)[] | no |
subject | string | yes |
bodyText | string | yes |
bodyHtml | string | no |
replyToMessageId | string | no |
forwardOfMessageId | string | no |
includeOriginalAttachments | boolean | no |
quoteOriginal | boolean | no |
attachments | object[] | no |
clientToken | string | yes |
Same schema as POST /api/v1/messages/send.
Responses
202 — Queued.
| Field | Type | Required |
|---|---|---|
opId | string | yes |
kind | "send" | "draft" | yes |
duplicate | boolean | yes |
threadId | string | null | yes |
savedAt | string | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"opId": { "type": "string" },
"kind": { "type": "string", "enum": [ "send", "draft" ] },
"duplicate": { "type": "boolean" },
"threadId": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"savedAt": { "type": "string" }
},
"required": [ "opId", "kind", "duplicate", "threadId", "savedAt" ],
"additionalProperties": false
}400 — The request does not match its schema.
401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
Error codes — webmail.bad_request, webmail.mailbox_not_found, webmail.message_not_found, webmail.attachment_not_found, webmail.message_too_large. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.
POST /api/v1/messages/attachments
Upload an attachment
One file per request, as multipart, before composing: the response gives the attachmentId to reference in attachments. Returns what the instance observed (cleaned name, normalised type, measured size). Files over 25 MB are refused with 413; executables and scripts with 400.
Access — Member session or API key with scope messages:write.
Request body (multipart/form-data)
| Field | Type | Required | Description |
|---|---|---|---|
file | string | yes | The file, as a binary multipart part. |
JSON Schema
json
{
"type": "object",
"properties": {
"file": { "type": "string", "description": "The file, as a binary multipart part." }
},
"required": [ "file" ]
}Responses
201 — The stored attachment.
| Field | Type | Required |
|---|---|---|
attachmentId | string | yes |
filename | string | yes |
size | integer | yes |
mime | string | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"attachmentId": { "type": "string" },
"filename": { "type": "string" },
"size": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"mime": { "type": "string" }
},
"required": [ "attachmentId", "filename", "size", "mime" ],
"additionalProperties": false
}400 — The request does not match its schema.
401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
This operation accepts an Idempotency-Key header: replaying the same request with the same key returns the original response instead of acting twice. See Idempotency.
Error codes — webmail.bad_request, webmail.attachment_too_large, webmail.attachment_rejected. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.
POST /api/v1/messages/{id}/forward
Forward a message
A shortcut over /send: the original attachments are re-referenced by the instance (no re-upload), the subject gets Fwd:, the References chain is kept without In-Reply-To. Same kill switch and rate limit as a send.
Access — Member session or API key with scope messages:write.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
id | path | string | yes |
Request body (application/json)
| Field | Type | Required |
|---|---|---|
to | string (email)[] | yes |
cc | string (email)[] | no |
bcc | string (email)[] | no |
subject | string | no |
bodyText | string | yes |
bodyHtml | string | no |
includeOriginalAttachments | boolean | no |
quoteOriginal | boolean | no |
attachments | object[] | no |
asDraft | boolean | no |
clientToken | string | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"to": {
"minItems": 1,
"maxItems": 100,
"type": "array",
"items": {
"type": "string",
"maxLength": 320,
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
}
},
"cc": {
"maxItems": 100,
"type": "array",
"items": {
"type": "string",
"maxLength": 320,
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
}
},
"bcc": {
"maxItems": 100,
"type": "array",
"items": {
"type": "string",
"maxLength": 320,
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
}
},
"subject": { "type": "string", "maxLength": 512 },
"bodyText": { "type": "string", "maxLength": 200000 },
"bodyHtml": { "type": "string", "maxLength": 500000 },
"includeOriginalAttachments": { "type": "boolean" },
"quoteOriginal": { "type": "boolean" },
"attachments": {
"maxItems": 20,
"type": "array",
"items": {
"type": "object",
"properties": {
"attachmentId": {
"type": "string",
"minLength": 8,
"maxLength": 128,
"pattern": "^[A-Za-z0-9._-]+$"
}
},
"required": [ "attachmentId" ]
}
},
"asDraft": { "type": "boolean" },
"clientToken": {
"type": "string",
"minLength": 8,
"maxLength": 128,
"pattern": "^[A-Za-z0-9._:-]+$"
}
},
"required": [ "to", "bodyText", "clientToken" ]
}Responses
202 — Queued.
| Field | Type | Required |
|---|---|---|
opId | string | yes |
kind | "send" | "draft" | yes |
duplicate | boolean | yes |
threadId | string | null | yes |
Same schema as POST /api/v1/messages/send.
400 — The request does not match its schema.
401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
This operation accepts an Idempotency-Key header: replaying the same request with the same key returns the original response instead of acting twice. See Idempotency.
Error codes — webmail.bad_request, webmail.mailbox_not_found, webmail.message_not_found, webmail.attachment_not_found, webmail.message_too_large, webmail.sending_disabled, webmail.rate_limited. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.
POST /api/v1/messages/{id}/reply
Reply to a message
A shortcut over /send: mailbox, recipients, subject and thread are pre-resolved from the original, then exactly the same path as a send — no reply bypasses the kill switch or the rate limit.
Access — Member session or API key with scope messages:write.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
id | path | string | yes |
Request body (application/json)
| Field | Type | Required |
|---|---|---|
to | string (email)[] | no |
cc | string (email)[] | no |
bcc | string (email)[] | no |
subject | string | no |
bodyText | string | yes |
bodyHtml | string | no |
asDraft | boolean | no |
attachments | object[] | no |
clientToken | string | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"to": {
"minItems": 1,
"maxItems": 100,
"type": "array",
"items": {
"type": "string",
"maxLength": 320,
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
}
},
"cc": {
"maxItems": 100,
"type": "array",
"items": {
"type": "string",
"maxLength": 320,
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
}
},
"bcc": {
"maxItems": 100,
"type": "array",
"items": {
"type": "string",
"maxLength": 320,
"format": "email",
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
}
},
"subject": { "type": "string", "maxLength": 512 },
"bodyText": { "type": "string", "maxLength": 200000 },
"bodyHtml": { "type": "string", "maxLength": 500000 },
"asDraft": { "type": "boolean" },
"attachments": {
"maxItems": 20,
"type": "array",
"items": {
"type": "object",
"properties": {
"attachmentId": {
"type": "string",
"minLength": 8,
"maxLength": 128,
"pattern": "^[A-Za-z0-9._-]+$"
}
},
"required": [ "attachmentId" ]
}
},
"clientToken": {
"type": "string",
"minLength": 8,
"maxLength": 128,
"pattern": "^[A-Za-z0-9._:-]+$"
}
},
"required": [ "bodyText", "clientToken" ]
}Responses
202 — Queued.
| Field | Type | Required |
|---|---|---|
opId | string | yes |
kind | "send" | "draft" | yes |
duplicate | boolean | yes |
threadId | string | null | yes |
Same schema as POST /api/v1/messages/send.
400 — The request does not match its schema.
401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
This operation accepts an Idempotency-Key header: replaying the same request with the same key returns the original response instead of acting twice. See Idempotency.
Error codes — webmail.bad_request, webmail.mailbox_not_found, webmail.message_not_found, webmail.attachment_not_found, webmail.message_too_large, webmail.sending_disabled, webmail.rate_limited. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.
POST /api/v1/messages/{id}/actions
Act on a message
Archive, mark read or unread, flag, move to a folder, trash… The response says the operation is recorded, not that the state changed: the mirror reports it on the next delta. Gestures the provider cannot do (permanent deletion on Gmail) are refused before anything is queued.
Access — Member session or API key with scope messages:write.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
id | path | string | yes |
Request body (application/json)
| Field | Type | Required |
|---|---|---|
action | string (enum) | yes |
labels | object | no |
clientToken | string | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": [
"markRead",
"markUnread",
"flag",
"unflag",
"archive",
"unarchive",
"trash",
"restore",
"spam",
"notSpam",
"deletePermanently",
"move"
]
},
"labels": {
"type": "object",
"properties": {
"add": {
"maxItems": 20,
"type": "array",
"items": { "type": "string", "minLength": 1, "maxLength": 200 }
},
"remove": {
"maxItems": 20,
"type": "array",
"items": { "type": "string", "minLength": 1, "maxLength": 200 }
}
}
},
"clientToken": {
"type": "string",
"minLength": 8,
"maxLength": 128,
"pattern": "^[A-Za-z0-9._:-]+$"
}
},
"required": [ "action", "clientToken" ]
}Responses
202 — The queued operations.
| Field | Type | Required |
|---|---|---|
ops | object[] | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"ops": {
"type": "array",
"items": {
"type": "object",
"properties": {
"messageId": { "type": "string" },
"opId": { "type": "string" },
"duplicate": { "type": "boolean" }
},
"required": [ "messageId", "opId", "duplicate" ],
"additionalProperties": false
}
}
},
"required": [ "ops" ],
"additionalProperties": false
}400 — The request does not match its schema.
401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
This operation accepts an Idempotency-Key header: replaying the same request with the same key returns the original response instead of acting twice. See Idempotency.
Error codes — webmail.bad_request, webmail.message_not_found, webmail.unsupported_action. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.
POST /api/v1/messages/actions
Act on several messages
The same action on up to 100 messages, all or nothing on the perimeter: a single id outside the member’s mailboxes is a 404 and nothing is queued.
Access — Member session or API key with scope messages:write.
Request body (application/json)
| Field | Type | Required |
|---|---|---|
messageIds | string[] | yes |
action | string (enum) | yes |
labels | object | no |
clientToken | string | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"messageIds": {
"minItems": 1,
"maxItems": 100,
"type": "array",
"items": { "type": "string", "minLength": 1 }
},
"action": {
"type": "string",
"enum": [
"markRead",
"markUnread",
"flag",
"unflag",
"archive",
"unarchive",
"trash",
"restore",
"spam",
"notSpam",
"deletePermanently",
"move"
]
},
"labels": {
"type": "object",
"properties": {
"add": {
"maxItems": 20,
"type": "array",
"items": { "type": "string", "minLength": 1, "maxLength": 200 }
},
"remove": {
"maxItems": 20,
"type": "array",
"items": { "type": "string", "minLength": 1, "maxLength": 200 }
}
}
},
"clientToken": {
"type": "string",
"minLength": 8,
"maxLength": 128,
"pattern": "^[A-Za-z0-9._:-]+$"
}
},
"required": [ "messageIds", "action", "clientToken" ]
}Responses
202 — The queued operations.
| Field | Type | Required |
|---|---|---|
ops | object[] | yes |
Same schema as POST /api/v1/messages/{id}/actions.
400 — The request does not match its schema.
401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
This operation accepts an Idempotency-Key header: replaying the same request with the same key returns the original response instead of acting twice. See Idempotency.
Error codes — webmail.bad_request, webmail.message_not_found, webmail.unsupported_action. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.