{"openapi":"3.0.3","info":{"title":"OpenMail API","description":"Email infrastructure API for AI agents. Create inboxes, send and receive email, and get real-time notifications via webhooks.","version":"1.0.0","license":{"name":"Proprietary"}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"API key"}},"schemas":{"MessageDirection":{"type":"string","enum":["inbound","outbound"]},"Pod":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"string","description":"OpenMail pod ID"},"clientId":{"type":"string","description":"Your own identifier for the pod, if set.","nullable":true},"name":{"type":"string","description":"Human-readable label, if set.","nullable":true},"isDefault":{"type":"boolean","description":"Whether this is the account's default pod. Every account has exactly one; it is created automatically and cannot be deleted."},"createdAt":{"type":"string","format":"date-time"}}},"ApiKey":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"string","description":"OpenMail API key ID"},"name":{"type":"string","description":"Human-readable label for the key, if set.","nullable":true},"podId":{"type":"string","description":"ID of the pod this key is scoped to, or null for an account-wide key. A pod-scoped key can only access resources in this pod.","nullable":true},"inboxId":{"type":"string","description":"ID of the inbox this key is confined to, or null. When set, the key can only access this single inbox and its threads, messages, and drafts (its `podId` is the inbox's pod).","nullable":true},"tokenPrefix":{"type":"string","description":"Non-secret leading characters of the token, to help you identify it."},"last4":{"type":"string","description":"Last 4 characters of the token, shown alongside the prefix."},"createdAt":{"type":"string","format":"date-time"},"lastUsedAt":{"type":"string","format":"date-time","description":"When the key last authenticated a request, or null if never used.","nullable":true}}},"ApiKeyWithToken":{"type":"object","additionalProperties":true,"description":"An API key plus its raw `token`. The token is returned only once, at creation, and is never retrievable again.","allOf":[{"$ref":"#/components/schemas/ApiKey"}],"properties":{"token":{"type":"string","description":"The secret API key. Store it securely — it is shown only once and cannot be retrieved later."}}},"Inbox":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"string","description":"OpenMail inbox ID"},"podId":{"type":"string","description":"ID of the pod this inbox belongs to. Inboxes created without an explicit pod land in your account's default pod."},"address":{"type":"string","format":"email","description":"Full email address"},"displayName":{"type":"string","description":"Sender display name","nullable":true},"webhookUrl":{"type":"string","description":"URL that receives signed webhook events for this inbox, if set. Overrides the account-level webhook for this inbox only.","nullable":true},"webhookSecret":{"type":"string","description":"Signing secret for this inbox's webhook. Only present in the response when the secret is generated or rotated (on create, on first setting a webhook URL, or via the rotate-secret endpoint) — it is never returned afterward.","nullable":true},"forwardTo":{"type":"string","description":"Address that receives copies of this inbox's inbound mail, if set.","nullable":true},"forwardWhen":{"type":"string","enum":["always","needs_human","webhook_failed",null],"description":"Which inbound mail is forwarded. Null when forwarding is not set.","nullable":true},"forwardStatus":{"type":"string","enum":["pending","active","disabled",null],"description":"`pending` until the address confirms via the emailed link (members of your organisation are confirmed at once); `active` once copies are flowing; `disabled` when forwarding paused itself after the address hard-bounced repeatedly or complained — set `forwardTo` again to resume. Null when forwarding is not set.","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"Message":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"string"},"threadId":{"type":"string"},"direction":{"$ref":"#/components/schemas/MessageDirection"},"fromAddr":{"type":"string"},"toAddr":{"type":"string","description":"Your OpenMail inbox address for inbound messages. For outbound sends, the primary recipient."},"headerTo":{"type":"string","description":"Inbound only. The address the sender wrote to (comma-separated if multiple). When deliveryRole is cc, reply to this address — not fromAddr.","nullable":true},"deliveryRole":{"type":"string","enum":["to","cc",null],"description":"Inbound only. to if your inbox was the main recipient; cc if you were copied on the email.","nullable":true},"cc":{"type":"array","items":{"type":"string"},"description":"Email addresses copied on the message."},"addressNames":{"type":"object","additionalProperties":{"type":"string"},"description":"Inbound only. Display names the sender's mail client put on From, To and Cc, keyed by lower-cased address, e.g. `{\"alice@example.com\": \"Alice Smith\"}`. Address fields stay bare so you can match on them. Null when the headers carried no names.","nullable":true},"bcc":{"type":"array","items":{"type":"string"},"description":"Blind-copy recipients on outbound mail sent from this inbox. Empty on inbound. Not included in the copy delivered to To or Cc."},"subject":{"type":"string"},"bodyText":{"type":"string"},"bodyHtml":{"type":"string"},"attachments":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"filename":{"type":"string"},"contentType":{"type":"string"},"sizeBytes":{"type":"integer"},"url":{"type":"string","description":"Signed URL for download"}}}},"rawUrl":{"type":"string","description":"URL of the original RFC 822 message (GET /v1/messages/{id}/raw). null when the bytes weren't kept.","nullable":true},"status":{"type":"string","enum":["pending","sent","received","failed"]},"createdAt":{"type":"string","format":"date-time"}}},"Thread":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"string"},"subject":{"type":"string"},"isRead":{"type":"boolean","description":"Whether the thread has been marked as read. Defaults to false for new inbound threads. Auto-set to true when a reply is sent."},"lastMessageAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"},"messageCount":{"type":"integer"}}},"DomainRecord":{"type":"object","additionalProperties":true,"description":"A single DNS record to publish for a custom domain. Publish every record whose `status` is not already `valid`, then the domain verifies automatically within a couple of minutes.","properties":{"type":{"type":"string","enum":["TXT","CNAME","MX"]},"name":{"type":"string","description":"The record's host, fully qualified, e.g. `_dmarc.agent-mail.example.com`."},"value":{"type":"string","description":"The record value to publish."},"priority":{"type":"integer","description":"MX records only.","nullable":true},"purpose":{"type":"string","enum":["spf","dkim","dmarc","mx","return-path"],"description":"What the record is for. Act on this rather than inferring from record shape: SPF must be merged into any the owner already has rather than replacing it, a DMARC policy at an apex can override the owner's own, and an apex MX takes over all their inbound mail."},"status":{"type":"string","enum":["missing","invalid","valid"],"description":"Whether this individual record is published and correct as of the last check. `missing` — not found yet. `invalid` — found but wrong. `valid` — published and correct. Shows which records are still outstanding, not only whether the whole domain passed."}}},"DomainWarning":{"type":"object","properties":{"code":{"type":"string","enum":["competing_mx"],"description":"`competing_mx` — another MX record is published at the same or a more preferred priority than ours, so senders split the domain's inbound mail between us and a server that does not know its inboxes. Part of the mail bounces `550` while every record shows `valid`. Fix: remove the other MX, or give it a higher priority number than ours."},"message":{"type":"string","description":"Human-readable explanation and fix, safe to show to the domain owner."},"hosts":{"type":"array","items":{"type":"string"},"description":"The competing MX hosts, lowercase, without trailing dot."}}},"Domain":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"string","description":"OpenMail domain ID"},"domain":{"type":"string","description":"The domain or subdomain, e.g. `example.com` or `agent-mail.example.com`."},"podId":{"type":"string","description":"Pod this domain is scoped to, so only inboxes in that pod can use it, or null for an account-wide domain.","nullable":true},"status":{"type":"string","enum":["pending","verifying","verified","failed","under_review"],"description":"`pending` — added, records not yet published. `verifying` — being checked. `verified` — sending and receiving enabled. `failed` — records missing or wrong; publishing them and calling verify again is the fix. `under_review` — flagged as suspicious; DNS setup is blocked until approved."},"records":{"type":"array","items":{"$ref":"#/components/schemas/DomainRecord"},"description":"The DNS records to publish for this domain. Returned even where the values are the same for every customer, so sending infrastructure can change without breaking clients."},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/DomainWarning"},"description":"Problems detected in the domain's DNS that do not block verification but do lose mail. Empty when none. Check this even when `status` is `verified` — every record can be correct and the zone still broken by something else in it."},"verifiedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"Error":{"type":"object","additionalProperties":true,"properties":{"error":{"type":"string"},"message":{"type":"string"}}}}},"paths":{"/v1/pods":{"post":{"operationId":"createPod","summary":"Create pod","tags":["pods"],"description":"Create a new pod (an isolated sub-account for one of your end users or tenants). Pass your own `clientId` to map the pod to a record in your system and address it later without storing the OpenMail ID.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"clientId":{"type":"string","maxLength":255,"description":"Your own stable identifier for this pod (e.g. your user/tenant ID). Must be unique within your account. Use it in place of the pod ID on any pod or inbox endpoint.","nullable":true},"name":{"type":"string","maxLength":255,"description":"Human-readable label shown in the console.","nullable":true}}}}}},"responses":{"201":{"description":"Pod created","content":{"application/json":{"schema":{"description":"Pod created","allOf":[{"$ref":"#/components/schemas/Pod"}]}}}},"409":{"description":"A pod with the same `clientId` already exists in your account","content":{"application/json":{"schema":{"description":"A pod with the same `clientId` already exists in your account","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}},"get":{"operationId":"listPods","summary":"List pods","tags":["pods"],"description":"List all pods for your account. The default pod is always returned first. Supports pagination.","parameters":[{"schema":{"type":"integer","default":50},"in":"query","name":"limit","required":false,"description":"Max results to return (clamped to 100)"},{"schema":{"type":"integer","default":0},"in":"query","name":"offset","required":false,"description":"Number of results to skip"}],"responses":{"200":{"description":"List of pods","content":{"application/json":{"schema":{"description":"List of pods","type":"object","additionalProperties":true,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Pod"}},"total":{"type":"integer"}}}}}},"401":{"description":"Missing, invalid, or expired API key","content":{"application/json":{"schema":{"description":"Missing, invalid, or expired API key","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/pods/{id}":{"get":{"operationId":"getPod","summary":"Get pod","tags":["pods"],"description":"Retrieve a single pod by its OpenMail ID or your own `clientId`.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"Pod ID or your own `clientId`"}],"responses":{"200":{"description":"Pod details","content":{"application/json":{"schema":{"description":"Pod details","allOf":[{"$ref":"#/components/schemas/Pod"}]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"description":"Not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}},"patch":{"operationId":"updatePod","summary":"Update pod","tags":["pods"],"description":"Update a pod's `clientId` or `name`. Only the fields you include are changed.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"clientId":{"type":"string","maxLength":255,"description":"Your own identifier for this pod. Set null to clear.","nullable":true},"name":{"type":"string","maxLength":255,"description":"Human-readable label. Set null to clear.","nullable":true}}}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"Pod ID or your own `clientId`"}],"responses":{"200":{"description":"Pod updated","content":{"application/json":{"schema":{"description":"Pod updated","allOf":[{"$ref":"#/components/schemas/Pod"}]}}}},"403":{"description":"The authenticating key is pod-scoped and cannot modify pods","content":{"application/json":{"schema":{"description":"The authenticating key is pod-scoped and cannot modify pods","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"description":"Not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"409":{"description":"A pod with the same `clientId` already exists in your account","content":{"application/json":{"schema":{"description":"A pod with the same `clientId` already exists in your account","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}},"delete":{"operationId":"deletePod","summary":"Delete pod","tags":["pods"],"description":"Permanently delete a pod. The pod must be empty (no inboxes or pod-scoped domains) and cannot be the default pod.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"Pod ID or your own `clientId`"}],"responses":{"204":{"description":"Pod deleted"},"404":{"description":"Not found","content":{"application/json":{"schema":{"description":"Not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"409":{"description":"Pod still has inboxes (`pod_not_empty`), still owns pod-scoped domains (`pod_has_domains`), or is the default pod (`default_pod`)","content":{"application/json":{"schema":{"description":"Pod still has inboxes (`pod_not_empty`), still owns pod-scoped domains (`pod_has_domains`), or is the default pod (`default_pod`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/pods/{id}/api-keys":{"post":{"operationId":"createPodApiKey","summary":"Create pod API key","tags":["pods"],"description":"Mint an API key scoped to this pod. The key can read and send from the pod's inboxes, create and delete inboxes in this pod, mint/revoke inbox keys for those inboxes, and manage pod/inbox policy and pod-scoped domains. It cannot change webhook config, manage pods, mint further pod keys, reach another pod, or address account-wide policy or domains. The full `token` is returned once, here — store it securely, it cannot be retrieved again. Requires an account-wide key (a pod-scoped key cannot mint keys).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","maxLength":255,"description":"Human-readable label to identify the key later.","nullable":true}}}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"Pod ID or your own `clientId`"}],"responses":{"201":{"description":"API key created","content":{"application/json":{"schema":{"description":"API key created","allOf":[{"$ref":"#/components/schemas/ApiKeyWithToken"}]}}}},"403":{"description":"The authenticating key is pod-scoped and cannot mint keys","content":{"application/json":{"schema":{"description":"The authenticating key is pod-scoped and cannot mint keys","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Pod not found","content":{"application/json":{"schema":{"description":"Pod not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"422":{"description":"The pod already has the maximum number of active keys (`api_key_limit_reached`)","content":{"application/json":{"schema":{"description":"The pod already has the maximum number of active keys (`api_key_limit_reached`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}},"get":{"operationId":"listPodApiKeys","summary":"List pod API keys","tags":["pods"],"description":"List the (non-revoked) API keys scoped to this pod. Only non-secret fields are returned — the token itself is never retrievable after creation.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"Pod ID or your own `clientId`"}],"responses":{"200":{"description":"List of API keys","content":{"application/json":{"schema":{"description":"List of API keys","type":"object","additionalProperties":true,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}}}}}}},"403":{"description":"The authenticating key is pod-scoped and cannot manage keys","content":{"application/json":{"schema":{"description":"The authenticating key is pod-scoped and cannot manage keys","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Pod not found","content":{"application/json":{"schema":{"description":"Pod not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/pods/{id}/api-keys/{keyId}":{"delete":{"operationId":"deletePodApiKey","summary":"Revoke pod API key","tags":["pods"],"description":"Permanently revoke a pod-scoped API key. Any integration using it immediately starts getting 401s. Requires an account-wide key.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"Pod ID or your own `clientId`"},{"schema":{"type":"string"},"in":"path","name":"keyId","required":true,"description":"The API key ID to revoke"}],"responses":{"204":{"description":"Key revoked"},"403":{"description":"The authenticating key is pod-scoped and cannot manage keys","content":{"application/json":{"schema":{"description":"The authenticating key is pod-scoped and cannot manage keys","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Key not found","content":{"application/json":{"schema":{"description":"Key not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/inboxes":{"post":{"operationId":"createInbox","summary":"Create inbox","tags":["inboxes"],"description":"Create a new email inbox. Optionally set a sender display name.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"mailboxName":{"type":"string","minLength":3,"maxLength":30,"description":"Inbox local part (default: random)"},"displayName":{"type":"string","maxLength":200,"description":"Sender display name shown in the From header"},"domain":{"type":"string","description":"Domain for the inbox address. Defaults to your account domain (e.g. `openmail.sh`). Pass a verified custom domain you own to create the inbox on it (e.g. `agent-mail.example.com`). The domain must already be added and verified for your account; the account default never switches automatically."},"webhookUrl":{"type":"string","minLength":0,"description":"URL to receive signed webhook events for this inbox (e.g. inbound mail). Overrides the account-level webhook for this inbox only. A signing secret is generated and returned once in the response body."},"forwardTo":{"type":"string","format":"email","description":"Email address that receives a copy of inbound messages, e.g. your own Gmail. Members of your organisation are confirmed at once; any other address is emailed a confirmation link and nothing is forwarded until they click it (`forwardStatus` is `pending` meanwhile). Spam and bulk mail are never forwarded. Replies go to the original sender."},"forwardWhen":{"type":"string","enum":["always","needs_human","webhook_failed"],"description":"When to forward. `always` (default); `needs_human` only mail the agent should not auto-handle (`autoReplyable` false); `webhook_failed` only once webhook delivery has exhausted its retries or there was no webhook or WebSocket to deliver to."},"podId":{"type":"string","description":"Pod to create the inbox in. Accepts the pod ID or your own `clientId`. Omit to place the inbox in your account's default pod."}}}}}},"responses":{"201":{"description":"Inbox created","content":{"application/json":{"schema":{"description":"Inbox created","allOf":[{"$ref":"#/components/schemas/Inbox"}]}}}},"400":{"description":"Invalid mailbox name, `domain` is not verified or not available in the selected pod, or `podId` does not match a pod in your account","content":{"application/json":{"schema":{"description":"Invalid mailbox name, `domain` is not verified or not available in the selected pod, or `podId` does not match a pod in your account","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"402":{"description":"Pro plan: inbox creation is paused because the account's monthly spend limit was reached","content":{"application/json":{"schema":{"description":"Pro plan: inbox creation is paused because the account's monthly spend limit was reached","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"409":{"description":"Address already in use, or the mailbox name is reserved","content":{"application/json":{"schema":{"description":"Address already in use, or the mailbox name is reserved","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"422":{"description":"Plan inbox limit reached, `webhookUrl` failed validation (`invalid_webhook_url`), or forwarding settings were rejected (`invalid_forward_to`, `forward_to_is_openmail_address`, `forward_verification_cooldown`, `invalid_forward_when`, `forward_when_requires_forward_to`)","content":{"application/json":{"schema":{"description":"Plan inbox limit reached, `webhookUrl` failed validation (`invalid_webhook_url`), or forwarding settings were rejected (`invalid_forward_to`, `forward_to_is_openmail_address`, `forward_verification_cooldown`, `invalid_forward_when`, `forward_when_requires_forward_to`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"429":{"description":"Rate limit exceeded (100 inbox creations/day, or 10 forwarding confirmation emails/hour — `forward_verification_limit`)","content":{"application/json":{"schema":{"description":"Rate limit exceeded (100 inbox creations/day, or 10 forwarding confirmation emails/hour — `forward_verification_limit`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}},"get":{"operationId":"listInboxes","summary":"List inboxes","tags":["inboxes"],"description":"List all inboxes for your account. Supports pagination.","parameters":[{"schema":{"type":"integer","default":50},"in":"query","name":"limit","required":false,"description":"Max results to return (clamped to 100)"},{"schema":{"type":"integer","default":0},"in":"query","name":"offset","required":false,"description":"Number of results to skip"},{"schema":{"type":"string"},"in":"query","name":"podId","required":false,"description":"Filter to inboxes in a single pod. Accepts the pod ID or your own `clientId`. An unknown value returns an empty list."}],"responses":{"200":{"description":"List of inboxes","content":{"application/json":{"schema":{"description":"List of inboxes","type":"object","additionalProperties":true,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Inbox"}},"total":{"type":"integer"}}}}}},"401":{"description":"Missing, invalid, or expired API key","content":{"application/json":{"schema":{"description":"Missing, invalid, or expired API key","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/inboxes/{id}":{"get":{"operationId":"getInbox","summary":"Get inbox","tags":["inboxes"],"description":"Retrieve a single inbox by its ID, including its address, display name, and creation time.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Inbox details","content":{"application/json":{"schema":{"description":"Inbox details","allOf":[{"$ref":"#/components/schemas/Inbox"}]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"description":"Not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}},"patch":{"operationId":"updateInbox","summary":"Update inbox","tags":["inboxes"],"description":"Update an inbox's settings, such as the sender display name shown in the From header.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"displayName":{"type":"string","maxLength":200,"description":"Sender display name. Set null to clear.","nullable":true},"webhookUrl":{"type":"string","minLength":0,"description":"URL to receive signed webhook events for this inbox. Set null to clear and revert to the account-level webhook. A signing secret is generated the first time a webhook URL is set on this inbox and returned once in the response body.","nullable":true},"forwardTo":{"type":"string","description":"Email address that receives a copy of inbound messages, e.g. your own Gmail. Members of your organisation are confirmed at once; any other address is emailed a confirmation link and `forwardStatus` stays `pending` until they click it. Re-sending the same pending address re-issues the link. Setting a new address also re-enables forwarding that was paused for bouncing. Set null to stop forwarding.","nullable":true},"forwardWhen":{"type":"string","enum":["always","needs_human","webhook_failed",null],"description":"When to forward. `always` (default); `needs_human` only mail the agent should not auto-handle (`autoReplyable` false); `webhook_failed` only once webhook delivery has exhausted its retries or there was no webhook or WebSocket to deliver to.","nullable":true}}}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Inbox updated","content":{"application/json":{"schema":{"description":"Inbox updated","allOf":[{"$ref":"#/components/schemas/Inbox"}]}}}},"403":{"description":"The authenticating key is inbox-scoped (read + send only) and cannot modify the inbox","content":{"application/json":{"schema":{"description":"The authenticating key is inbox-scoped (read + send only) and cannot modify the inbox","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"description":"Not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"422":{"description":"`webhookUrl` failed validation (`invalid_webhook_url`), or forwarding settings were rejected (`invalid_forward_to`, `forward_to_is_openmail_address`, `forward_verification_cooldown`, `invalid_forward_when`, `forward_when_requires_forward_to`)","content":{"application/json":{"schema":{"description":"`webhookUrl` failed validation (`invalid_webhook_url`), or forwarding settings were rejected (`invalid_forward_to`, `forward_to_is_openmail_address`, `forward_verification_cooldown`, `invalid_forward_when`, `forward_when_requires_forward_to`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"429":{"description":"10 forwarding confirmation emails already sent this hour (`forward_verification_limit`)","content":{"application/json":{"schema":{"description":"10 forwarding confirmation emails already sent this hour (`forward_verification_limit`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}},"delete":{"operationId":"deleteInbox","summary":"Delete inbox","tags":["inboxes"],"description":"Permanently delete an inbox and stop delivery to its address. This action cannot be undone. An account-wide key or a pod-scoped key for this inbox's pod may delete it; an inbox-scoped key cannot.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Inbox deleted"},"403":{"description":"The authenticating key is inbox-scoped, or a pod-scoped key for a different pod, and cannot delete this inbox","content":{"application/json":{"schema":{"description":"The authenticating key is inbox-scoped, or a pod-scoped key for a different pod, and cannot delete this inbox","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"description":"Not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/inboxes/{id}/webhook/rotate-secret":{"post":{"operationId":"rotateInboxWebhookSecret","summary":"Rotate webhook secret","tags":["inboxes"],"description":"Generate a new signing secret for this inbox's webhook. The inbox must already have a webhook URL set. The previous secret is invalidated immediately.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"New signing secret","content":{"application/json":{"schema":{"description":"New signing secret","type":"object","additionalProperties":true,"required":["webhookSecret"],"properties":{"webhookSecret":{"type":"string"}}}}}},"400":{"description":"No webhook URL is set on this inbox (`no_webhook_url`)","content":{"application/json":{"schema":{"description":"No webhook URL is set on this inbox (`no_webhook_url`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"403":{"description":"The authenticating key is inbox-scoped (read + send only) and cannot rotate webhook secrets","content":{"application/json":{"schema":{"description":"The authenticating key is inbox-scoped (read + send only) and cannot rotate webhook secrets","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"description":"Not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/inboxes/{id}/webhook/test":{"post":{"operationId":"testInboxWebhook","summary":"Send test webhook","tags":["inboxes"],"description":"Send a signed test event to this inbox's webhook URL. Requires a webhook URL (and signing secret) to already be set on the inbox.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Test webhook sent successfully","content":{"application/json":{"schema":{"description":"Test webhook sent successfully","type":"object","additionalProperties":true,"properties":{"ok":{"type":"boolean"},"message":{"type":"string"}}}}}},"400":{"description":"No webhook URL is set on this inbox (`no_webhook_url`)","content":{"application/json":{"schema":{"description":"No webhook URL is set on this inbox (`no_webhook_url`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"description":"Not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"502":{"description":"The webhook endpoint returned an error status, or the request to it failed (`webhook_failed`)","content":{"application/json":{"schema":{"description":"The webhook endpoint returned an error status, or the request to it failed (`webhook_failed`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/inboxes/{id}/api-keys":{"post":{"operationId":"createInboxApiKey","summary":"Create inbox API key","tags":["inboxes"],"description":"Mint an API key scoped to this inbox. The key can only read and send from this one inbox — its threads, messages, and drafts — and can never reach another inbox, manage pods, or mint keys. The full `token` is returned once, here — store it securely, it cannot be retrieved again. An account-wide key, or a pod-scoped key for this inbox's pod, may mint; an inbox-scoped key cannot.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","maxLength":255,"description":"Human-readable label to identify the key later.","nullable":true}}}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"Inbox ID"}],"responses":{"201":{"description":"API key created","content":{"application/json":{"schema":{"description":"API key created","allOf":[{"$ref":"#/components/schemas/ApiKeyWithToken"}]}}}},"403":{"description":"The authenticating key is inbox-scoped, or a pod-scoped key for a different pod, and cannot mint keys","content":{"application/json":{"schema":{"description":"The authenticating key is inbox-scoped, or a pod-scoped key for a different pod, and cannot mint keys","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Inbox not found","content":{"application/json":{"schema":{"description":"Inbox not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"409":{"description":"The inbox is not assigned to a pod, so an inbox-scoped key cannot be minted for it (`inbox_has_no_pod`)","content":{"application/json":{"schema":{"description":"The inbox is not assigned to a pod, so an inbox-scoped key cannot be minted for it (`inbox_has_no_pod`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"422":{"description":"The inbox already has the maximum number of active keys (`api_key_limit_reached`)","content":{"application/json":{"schema":{"description":"The inbox already has the maximum number of active keys (`api_key_limit_reached`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}},"get":{"operationId":"listInboxApiKeys","summary":"List inbox API keys","tags":["inboxes"],"description":"List the (non-revoked) API keys scoped to this inbox. Only non-secret fields are returned — the token itself is never retrievable after creation. An account-wide key, or a pod-scoped key for this inbox's pod, may list; an inbox-scoped key cannot.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"Inbox ID"}],"responses":{"200":{"description":"List of API keys","content":{"application/json":{"schema":{"description":"List of API keys","type":"object","additionalProperties":true,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}}}}}}},"403":{"description":"The authenticating key is inbox-scoped, or a pod-scoped key for a different pod, and cannot manage keys","content":{"application/json":{"schema":{"description":"The authenticating key is inbox-scoped, or a pod-scoped key for a different pod, and cannot manage keys","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Inbox not found","content":{"application/json":{"schema":{"description":"Inbox not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/inboxes/{id}/api-keys/{keyId}":{"delete":{"operationId":"deleteInboxApiKey","summary":"Revoke inbox API key","tags":["inboxes"],"description":"Permanently revoke an inbox-scoped API key. Any integration using it immediately starts getting 401s. An account-wide key, or a pod-scoped key for this inbox's pod, may revoke; an inbox-scoped key cannot.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"Inbox ID"},{"schema":{"type":"string"},"in":"path","name":"keyId","required":true,"description":"The API key ID to revoke"}],"responses":{"204":{"description":"Key revoked"},"403":{"description":"The authenticating key is inbox-scoped, or a pod-scoped key for a different pod, and cannot manage keys","content":{"application/json":{"schema":{"description":"The authenticating key is inbox-scoped, or a pod-scoped key for a different pod, and cannot manage keys","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Key not found","content":{"application/json":{"schema":{"description":"Key not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/domains":{"post":{"operationId":"createDomain","summary":"Add custom domain","tags":["domains"],"description":"Add a custom domain to your account. The response includes the DNS records to publish (`records`). Once you publish them, the domain verifies automatically within a couple of minutes — poll `GET /v1/domains/{id}` until `status` is `verified`, then create inboxes on it. A pod-scoped API key creates the domain scoped to its own pod; inbox-scoped keys cannot manage domains.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["domain"],"properties":{"domain":{"type":"string","minLength":4,"maxLength":253,"description":"The domain to add, e.g. `mail.acme.com`. A subdomain is easiest to set up without affecting your existing mail."},"podId":{"type":"string","description":"Pod to scope the domain to. Omit or null for an account-wide domain usable by any pod. A pod-scoped API key may only pass its own pod.","nullable":true}}}}}},"responses":{"201":{"description":"Domain added; publish the DNS records to verify it","content":{"application/json":{"schema":{"description":"Domain added; publish the DNS records to verify it","allOf":[{"$ref":"#/components/schemas/Domain"}]}}}},"202":{"description":"Domain accepted but under review; DNS setup is blocked until approved. The response includes a `message` explaining why.","content":{"application/json":{"schema":{"description":"Domain accepted but under review; DNS setup is blocked until approved. The response includes a `message` explaining why.","allOf":[{"$ref":"#/components/schemas/Domain"}]}}}},"402":{"description":"Pro plan: domain creation is paused because the account's monthly spend limit was reached (`spend_cap_paused`)","content":{"application/json":{"schema":{"description":"Pro plan: domain creation is paused because the account's monthly spend limit was reached (`spend_cap_paused`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"403":{"description":"Plan does not allow custom domains (`plan_limit`), or the API key is scoped and cannot manage domains (`forbidden`)","content":{"application/json":{"schema":{"description":"Plan does not allow custom domains (`plan_limit`), or the API key is scoped and cannot manage domains (`forbidden`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"409":{"description":"You already added this domain (`domain_exists`), it belongs to another account (`domain_taken`), or it needs manual reconnection (`identity_exists`)","content":{"application/json":{"schema":{"description":"You already added this domain (`domain_exists`), it belongs to another account (`domain_taken`), or it needs manual reconnection (`identity_exists`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"422":{"description":"Invalid domain (`invalid_domain`) or the domain was flagged as unsafe (`domain_blocked`)","content":{"application/json":{"schema":{"description":"Invalid domain (`invalid_domain`) or the domain was flagged as unsafe (`domain_blocked`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"502":{"description":"The mail provider could not set up the domain (`provider_error`)","content":{"application/json":{"schema":{"description":"The mail provider could not set up the domain (`provider_error`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}},"get":{"operationId":"listDomains","summary":"List custom domains","tags":["domains"],"description":"List the custom domains on your account. A pod-scoped key sees only its pod's domains plus account-wide ones.","parameters":[{"schema":{"type":"integer","default":50},"in":"query","name":"limit","required":false,"description":"Max results to return (clamped to 100)"},{"schema":{"type":"integer","default":0},"in":"query","name":"offset","required":false,"description":"Number of results to skip"}],"responses":{"200":{"description":"List of custom domains","content":{"application/json":{"schema":{"description":"List of custom domains","type":"object","additionalProperties":true,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Domain"}},"total":{"type":"integer"}}}}}},"401":{"description":"Missing, invalid, or expired API key","content":{"application/json":{"schema":{"description":"Missing, invalid, or expired API key","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/domains/{id}":{"get":{"operationId":"getDomain","summary":"Get custom domain","tags":["domains"],"description":"Retrieve a single custom domain by its ID, including its verification `status` and the DNS records to publish. Poll this after adding a domain to watch it flip to `verified`.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Custom domain details","content":{"application/json":{"schema":{"description":"Custom domain details","allOf":[{"$ref":"#/components/schemas/Domain"}]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"description":"Not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}},"patch":{"operationId":"updateDomain","summary":"Change custom domain pod scope","tags":["domains"],"description":"Change which pods can create inboxes on a custom domain. Pass `podId: null` to make it account-wide (always allowed), or a pod id to scope it to that pod — allowed only while every inbox on the domain already lives in that pod, since inboxes cannot move between pods. DNS records and verification are untouched. Requires an account-wide API key.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["podId"],"properties":{"podId":{"type":"string","description":"Pod to scope the domain to (id or clientId). `null` makes the domain available to all pods.","nullable":true}}}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Scope changed; the returned domain reflects the new `podId`","content":{"application/json":{"schema":{"description":"Scope changed; the returned domain reflects the new `podId`","allOf":[{"$ref":"#/components/schemas/Domain"}]}}}},"400":{"description":"Pod not found or not yours (`invalid_pod`)","content":{"application/json":{"schema":{"description":"Pod not found or not yours (`invalid_pod`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"403":{"description":"The API key is scoped and cannot change domain scope (`forbidden`)","content":{"application/json":{"schema":{"description":"The API key is scoped and cannot change domain scope (`forbidden`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"description":"Not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"409":{"description":"Inboxes on this domain live outside the target pod (`domain_has_inboxes`); the body includes `outsideCount`","content":{"application/json":{"schema":{"description":"Inboxes on this domain live outside the target pod (`domain_has_inboxes`); the body includes `outsideCount`","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}},"delete":{"operationId":"deleteDomain","summary":"Delete custom domain","tags":["domains"],"description":"Remove a custom domain from your account. The domain must have no inboxes — delete those first. This does not remove the DNS records from your registrar. An account-wide key may delete any domain; a pod-scoped key may delete only a domain scoped to its own pod, never an account-wide domain. Inbox-scoped keys cannot manage domains.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Domain deleted"},"403":{"description":"The API key is scoped and cannot manage domains (`forbidden`)","content":{"application/json":{"schema":{"description":"The API key is scoped and cannot manage domains (`forbidden`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"description":"Not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"422":{"description":"The domain still has inboxes (`domain_has_inboxes`)","content":{"application/json":{"schema":{"description":"The domain still has inboxes (`domain_has_inboxes`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/domains/{id}/verify":{"post":{"operationId":"verifyDomain","summary":"Re-check custom domain","tags":["domains"],"description":"Force an immediate DNS re-check of a custom domain, instead of waiting for the automatic background check. The domain verifies on its own within a couple of minutes of you publishing the records, so this is only needed to speed that up. An account-wide key may re-check any domain; a pod-scoped key may re-check only a domain scoped to its own pod, never an account-wide domain. Inbox-scoped keys cannot manage domains.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Re-checked; the returned domain reflects the latest status","content":{"application/json":{"schema":{"description":"Re-checked; the returned domain reflects the latest status","allOf":[{"$ref":"#/components/schemas/Domain"}]}}}},"403":{"description":"The API key is scoped and cannot manage domains (`forbidden`)","content":{"application/json":{"schema":{"description":"The API key is scoped and cannot manage domains (`forbidden`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"description":"Not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"502":{"description":"The mail provider check failed (`provider_error`)","content":{"application/json":{"schema":{"description":"The mail provider check failed (`provider_error`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/inboxes/{id}/send":{"post":{"operationId":"sendEmail","summary":"Send email","tags":["messages"],"description":"Send an email from an inbox. Optionally pass an Idempotency-Key header to make retries safe: a repeat with the same key within 24 hours returns the original message instead of sending again. Without it the API generates a key and sends once. Use threadId to reply to an existing thread. When replying on a thread where your inbox was CC'd (deliveryRole cc), the original sender is automatically CC'd.\n\nEvery recipient this call can reach — `to`, `cc` (including any address auto-CC'd on a thread reply), `bcc`, and `replyTo` — is checked against the inbox's outbound correspondent policy before anything is sent. A single denied recipient rejects the whole send with 403 `recipient_not_permitted`; there is no partial delivery.\n\n`bcc` recipients are delivered and stored on the outbound message, but they are not listed on the copy To and Cc recipients receive. An address already in `to` or `cc` is not also blind-copied.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["to","body"],"properties":{"to":{"type":"string","format":"email"},"subject":{"type":"string","minLength":1,"maxLength":998,"description":"Required for new threads. Optional when threadId is set — the thread subject is used automatically (with Re: prefix)."},"body":{"type":"string","minLength":1,"maxLength":100000,"description":"Email body. Plain text or HTML — HTML is auto-detected and rendered, with a plain-text fallback generated automatically."},"bodyHtml":{"type":"string","maxLength":500000,"description":"Provide only when the plain-text and HTML versions must differ. Omit for normal sends — put HTML in `body` instead."},"threadId":{"type":"string","description":"Thread ID for replies"},"includeQuote":{"type":"boolean","description":"When replying with threadId, append a quoted copy of the previous message to the body. Defaults to true. Set false to send only your reply text."},"replyTo":{"type":"string","format":"email","description":"Optional. Address that replies should be routed to (set as the `Reply-To` MIME header). Free plan: must be the address of an inbox you own in OpenMail. Pro plan or higher: any valid email address."},"cc":{"type":"array","items":{"type":"string","format":"email"},"description":"Optional CC recipients. Visible to every recipient."},"bcc":{"type":"array","items":{"type":"string","format":"email"},"description":"Optional BCC recipients. Delivered, but omitted from the message To and Cc recipients can read. Use this for CRM logging addresses (for example HubSpot or Salesforce) that only record the email when they are in BCC. Checked against the inbox's outbound recipient policy, same as `to` and `cc`."}}}},"multipart/form-data":{"schema":{"type":"object","required":["to","body"],"properties":{"to":{"type":"string","format":"email"},"subject":{"type":"string","minLength":1,"maxLength":998,"description":"Required for new threads. Optional when threadId is set — the thread subject is used automatically (with Re: prefix)."},"body":{"type":"string","minLength":1,"maxLength":100000,"description":"Email body. Plain text or HTML — HTML is auto-detected and rendered, with a plain-text fallback generated automatically."},"bodyHtml":{"type":"string","maxLength":500000,"description":"Provide only when the plain-text and HTML versions must differ. Omit for normal sends — put HTML in `body` instead."},"threadId":{"type":"string","description":"Thread ID for replies"},"includeQuote":{"type":"boolean","description":"When replying with threadId, append a quoted copy of the previous message to the body. Defaults to true. Set false to send only your reply text."},"replyTo":{"type":"string","format":"email","description":"Optional. Address that replies should be routed to (set as the `Reply-To` MIME header). Free plan: must be the address of an inbox you own in OpenMail. Pro plan or higher: any valid email address."},"cc":{"type":"array","items":{"type":"string","format":"email"},"description":"Optional CC recipients. Repeat the `cc` field once per address; a single field may also hold a comma-separated list or a JSON-encoded array of addresses."},"bcc":{"type":"array","items":{"type":"string","format":"email"},"description":"Optional BCC recipients. Repeat the `bcc` field once per address; a single field may also hold a comma-separated list or a JSON-encoded array of addresses. Not visible to To or Cc recipients."},"attachments":{"type":"array","items":{"type":"string","format":"binary"},"description":"File attachments (max 25 MB total per email)"}}}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"header","name":"idempotency-key","required":false,"description":"Optional. Unique key (e.g. UUID) that makes retries safe: a repeat with the same key within 24 hours returns the original message instead of sending again. Generated server-side when omitted."}],"responses":{"200":{"description":"Email sent (or idempotent replay)","content":{"application/json":{"schema":{"description":"Email sent (or idempotent replay)","type":"object","additionalProperties":true,"properties":{"messageId":{"type":"string"},"threadId":{"type":"string"},"providerMessageId":{"type":"string"},"status":{"type":"string","enum":["pending","sent","failed"]}}}}}},"400":{"description":"Missing request body, invalid 'to'/'replyTo'/'cc'/'bcc' address, or missing subject when required","content":{"application/json":{"schema":{"description":"Missing request body, invalid 'to'/'replyTo'/'cc'/'bcc' address, or missing subject when required","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"402":{"description":"Sending is paused because this account's monthly spend limit was reached (`spend_cap_paused`)","content":{"application/json":{"schema":{"description":"Sending is paused because this account's monthly spend limit was reached (`spend_cap_paused`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"403":{"description":"A recipient is not permitted by the inbox's outbound correspondent policy (`recipient_not_permitted`, with the refused address in `recipient`). Also returned when a free-plan customer sets `replyTo` to an address that is not one of their own OpenMail inboxes (`feature_requires_paid_plan`), the account is banned (`account_banned`), or sending is suspended due to deliverability/abuse signals (`reputation_suspended`).","content":{"application/json":{"schema":{"description":"A recipient is not permitted by the inbox's outbound correspondent policy (`recipient_not_permitted`, with the refused address in `recipient`). Also returned when a free-plan customer sets `replyTo` to an address that is not one of their own OpenMail inboxes (`feature_requires_paid_plan`), the account is banned (`account_banned`), or sending is suspended due to deliverability/abuse signals (`reputation_suspended`).","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Inbox or thread not found","content":{"application/json":{"schema":{"description":"Inbox or thread not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"409":{"description":"The custom domain isn't ready to send yet (`domain_sending_pending`). Usually clears within minutes of verification; check the Retry-After response header.","content":{"application/json":{"schema":{"description":"The custom domain isn't ready to send yet (`domain_sending_pending`). Usually clears within minutes of verification; check the Retry-After response header.","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"422":{"description":"Content blocked (`unsafe_content`, `phishing_detected`), a recipient domain has no MX records (`no_mx_records`, with the failing domain in `domain`), recipient suppressed for bounce/complaint (`recipient_suppressed`), or account storage limit exceeded (`storage_limit_exceeded`)","content":{"application/json":{"schema":{"description":"Content blocked (`unsafe_content`, `phishing_detected`), a recipient domain has no MX records (`no_mx_records`, with the failing domain in `domain`), recipient suppressed for bounce/complaint (`recipient_suppressed`), or account storage limit exceeded (`storage_limit_exceeded`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"429":{"description":"Rate limit exceeded (global daily, per-inbox daily, per-inbox burst) or cold outreach limit. Check the Retry-After response header.","content":{"application/json":{"schema":{"description":"Rate limit exceeded (global daily, per-inbox daily, per-inbox burst) or cold outreach limit. Check the Retry-After response header.","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/attachments/{messageId}/{filename}":{"get":{"operationId":"getAttachment","summary":"Download attachment","tags":["attachments"],"description":"Returns the attachment bytes. Authenticate with `Authorization: Bearer <API_KEY>`. The response is the file itself (not a redirect), so clients can fetch the URL from `attachments[].url` with the same API key they use for the rest of the API.","parameters":[{"schema":{"type":"string"},"in":"path","name":"messageId","required":true},{"schema":{"type":"string"},"in":"path","name":"filename","required":true}],"responses":{"200":{"description":"Attachment file bytes","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"Attachment not found","content":{"application/json":{"schema":{"description":"Attachment not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/attachments/{messageId}/{filename}/text":{"get":{"operationId":"getAttachmentText","summary":"Extract attachment text","tags":["attachments"],"description":"Extracts and returns the plain text content of an attachment. Supports PDF, DOCX, XLSX, PPTX, images (via OCR), and other common formats. The result is cached on the message after the first extraction.","parameters":[{"schema":{"type":"string"},"in":"path","name":"messageId","required":true},{"schema":{"type":"string"},"in":"path","name":"filename","required":true}],"responses":{"200":{"description":"Extracted text content","content":{"application/json":{"schema":{"description":"Extracted text content","type":"object","additionalProperties":true,"properties":{"filename":{"type":"string"},"inboxId":{"type":"string","description":"Inbox the parent message belongs to"},"contentType":{"type":"string"},"extractionMethod":{"type":"string","description":"Method used to extract text (e.g. pdf, docx, ocr)"},"text":{"type":"string"}}}}}},"404":{"description":"Attachment not found","content":{"application/json":{"schema":{"description":"Attachment not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"422":{"description":"Text could not be extracted from the attachment","content":{"application/json":{"schema":{"description":"Text could not be extracted from the attachment","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/inboxes/{id}/messages":{"get":{"operationId":"listMessages","summary":"List messages","tags":["messages"],"description":"List all messages in an inbox, across every thread. Supports pagination for high-volume inboxes.","parameters":[{"schema":{"type":"integer","default":50},"in":"query","name":"limit","required":false},{"schema":{"type":"integer","default":0},"in":"query","name":"offset","required":false},{"schema":{"$ref":"#/components/schemas/MessageDirection"},"in":"query","name":"direction","required":false},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"List of messages","content":{"application/json":{"schema":{"description":"List of messages","type":"object","additionalProperties":true,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Message"}},"total":{"type":"integer"}}}}}},"401":{"description":"Missing, invalid, or expired API key","content":{"application/json":{"schema":{"description":"Missing, invalid, or expired API key","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Inbox not found","content":{"application/json":{"schema":{"description":"Inbox not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/inboxes/{id}/threads":{"get":{"operationId":"listThreads","summary":"List threads","tags":["threads"],"description":"List threads for an inbox. Use isRead to filter by read status — for example, ?isRead=false returns only unread threads. (snake_case is_read is accepted as an alias.)","parameters":[{"schema":{"type":"integer","default":50},"in":"query","name":"limit","required":false},{"schema":{"type":"integer","default":0},"in":"query","name":"offset","required":false},{"schema":{"type":"string","enum":["true","false"]},"in":"query","name":"isRead","required":false,"description":"Filter by read status. true = read threads only, false = unread threads only. Omit to return all."},{"schema":{"type":"string","enum":["true","false"]},"in":"query","name":"is_read","required":false,"description":"Alias of isRead, kept for compatibility. Prefer isRead."},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"List of threads","content":{"application/json":{"schema":{"description":"List of threads","type":"object","additionalProperties":true,"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Thread"}},"total":{"type":"integer"}}}}}},"401":{"description":"Missing, invalid, or expired API key","content":{"application/json":{"schema":{"description":"Missing, invalid, or expired API key","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Inbox not found","content":{"application/json":{"schema":{"description":"Inbox not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/threads/{id}/messages":{"get":{"operationId":"getThreadMessages","summary":"Get thread messages","tags":["threads"],"description":"Retrieve a thread and all of its messages in order, with read status and full message content.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Thread with messages","content":{"application/json":{"schema":{"description":"Thread with messages","type":"object","additionalProperties":true,"properties":{"threadId":{"type":"string"},"inboxId":{"type":"string","description":"Inbox this thread belongs to"},"subject":{"type":"string"},"isRead":{"type":"boolean","description":"Whether the thread has been marked as read"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Message"}}}}}}},"401":{"description":"Missing, invalid, or expired API key","content":{"application/json":{"schema":{"description":"Missing, invalid, or expired API key","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Thread not found","content":{"application/json":{"schema":{"description":"Thread not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/messages/{id}/raw":{"get":{"operationId":"getRawMessage","summary":"Download raw message","tags":["messages"],"description":"Returns the original RFC 822 bytes of an inbound message as `message/rfc822`, exactly as received.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Original message bytes","content":{"message/rfc822":{"schema":{"type":"string","format":"binary"},"example":"Return-Path: <alice@example.com>\r\nReceived: from mail.example.com by inbound-smtp.eu-north-1.amazonaws.com\r\n for bot@yourdomain.com; Wed, 16 Sep 2026 18:22:58 +0000 (UTC)\r\nAuthentication-Results: amazonses.com; spf=pass; dkim=pass header.i=@example.com; dmarc=pass\r\nDKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=sel;\r\n h=from:to:subject:date:message-id:content-type; bh=y/Rmyv...; b=ZoOqF3...\r\nFrom: Alice <alice@example.com>\r\nTo: bot@yourdomain.com\r\nSubject: Q3 report\r\nDate: Wed, 16 Sep 2026 14:22:53 -0400\r\nMessage-ID: <CAJ=a=Hx...@mail.example.com>\r\nMIME-Version: 1.0\r\nContent-Type: multipart/mixed; boundary=\"b1\"\r\n\r\n--b1\r\nContent-Type: text/plain; charset=\"UTF-8\"\r\n\r\nReport attached.\r\n\r\n--b1\r\nContent-Type: application/pdf; name=\"report.pdf\"\r\nContent-Disposition: attachment; filename=\"report.pdf\"\r\nContent-Transfer-Encoding: base64\r\n\r\nJVBERi0xLjQKJ...\r\n--b1--\r\n"}}},"401":{"description":"Missing, invalid, or expired API key","content":{"application/json":{"schema":{"description":"Missing, invalid, or expired API key","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Message not found, or its original bytes weren't kept","content":{"application/json":{"schema":{"description":"Message not found, or its original bytes weren't kept","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/messages/{id}":{"delete":{"operationId":"deleteMessage","summary":"Delete message","tags":["messages"],"description":"Delete a message from its inbox.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Message deleted"},"401":{"description":"Missing, invalid, or expired API key","content":{"application/json":{"schema":{"description":"Missing, invalid, or expired API key","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"403":{"description":"API key is inbox-scoped, or is not scoped to this message's inbox. Deleting mail needs an account-wide or pod-scoped key.","content":{"application/json":{"schema":{"description":"API key is inbox-scoped, or is not scoped to this message's inbox. Deleting mail needs an account-wide or pod-scoped key.","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Message not found","content":{"application/json":{"schema":{"description":"Message not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/threads/{id}":{"delete":{"operationId":"deleteThread","summary":"Delete thread","tags":["threads"],"description":"Move a thread and all of its messages to Trash. The API stops returning it. In the console it sits under Trash, where it can be restored or deleted for good; unrestored threads are deleted automatically after 30 days.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Thread deleted"},"401":{"description":"Missing, invalid, or expired API key","content":{"application/json":{"schema":{"description":"Missing, invalid, or expired API key","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"403":{"description":"API key is inbox-scoped, or is not scoped to this thread's inbox. Deleting mail needs an account-wide or pod-scoped key.","content":{"application/json":{"schema":{"description":"API key is inbox-scoped, or is not scoped to this thread's inbox. Deleting mail needs an account-wide or pod-scoped key.","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Thread not found","content":{"application/json":{"schema":{"description":"Thread not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}},"patch":{"operationId":"updateThread","summary":"Update thread","tags":["threads"],"description":"Update a thread's read status. Use this to mark a thread as read after your agent has processed it, or as unread to re-queue it for later processing.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","anyOf":[{"required":["isRead"]},{"required":["is_read"]}],"properties":{"isRead":{"type":"boolean","description":"Set to true to mark as read, false to mark as unread"},"is_read":{"type":"boolean","description":"Alias of isRead, kept for compatibility. Prefer isRead."}}}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Thread updated","content":{"application/json":{"schema":{"description":"Thread updated","type":"object","additionalProperties":true,"properties":{"ok":{"type":"boolean"}}}}}},"404":{"description":"Thread not found","content":{"application/json":{"schema":{"description":"Thread not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/feedback":{"post":{"operationId":"createFeedback","summary":"Report feedback","tags":["feedback"],"description":"Report a bug, a point of friction, or a feature request directly to the OpenMail team. If an API call fails unexpectedly, a response looks wrong, or you notice something that would make OpenMail work better for you, send it here — one call, no confirmation needed, and the team reads every report. Works even while an inbox or pod is suspended. Include what you were trying to do and what you observed; attach the endpoint, error code, and request id in `context` when the report concerns a specific failed call.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["type","message"],"properties":{"type":{"type":"string","enum":["bug","friction","feature_request"],"description":"`bug` for something broken or a response that looks wrong, `friction` for something that works but was confusing or harder than it should be, `feature_request` for a capability OpenMail lacks."},"message":{"type":"string","minLength":1,"maxLength":5000,"description":"What you were trying to do, what you expected, and what actually happened. Plain text."},"context":{"type":"object","additionalProperties":false,"description":"Optional details of the specific API call the report concerns.","properties":{"endpoint":{"type":"string","maxLength":255,"description":"The API path involved, e.g. `/v1/inboxes/{id}/send`."},"errorCode":{"type":"string","maxLength":255,"description":"The `error` field of the response you received, if any."},"requestId":{"type":"string","maxLength":255,"description":"The request id from the response, if you have one."}}}}}}}},"responses":{"201":{"description":"Feedback recorded","content":{"application/json":{"schema":{"description":"Feedback recorded","type":"object","additionalProperties":false,"properties":{"id":{"type":"string","description":"Id of the recorded report"},"status":{"type":"string","enum":["received"]}}}}}},"429":{"description":"Feedback rate limit reached for your account (`feedback_rate_limited`) — the daily cap resets at midnight UTC","content":{"application/json":{"schema":{"description":"Feedback rate limit reached for your account (`feedback_rate_limited`) — the daily cap resets at midnight UTC","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/policy":{"get":{"operationId":"getPolicy","summary":"Get correspondent policy","tags":["policy"],"description":"Read the correspondent policy for a scope (account by default, or a pod/inbox). Returns the mode this scope sets for each direction, the effective mode after inheritance, this scope's own rules, and the rules inherited from parent scopes. Inbox-scoped keys are rejected. A pod-scoped key may read its own pod or an inbox in that pod, never account-wide policy. Account-wide keys may read any scope.","parameters":[{"schema":{"type":"string"},"in":"query","name":"podId","required":false,"description":"Confine the operation to one pod's policy. Accepts the pod ID. Mutually exclusive with `inboxId`."},{"schema":{"type":"string"},"in":"query","name":"inboxId","required":false,"description":"Confine the operation to one inbox's policy. Mutually exclusive with `podId`."}],"responses":{"200":{"description":"The resolved policy for the scope","content":{"application/json":{"schema":{"description":"The resolved policy for the scope","type":"object","additionalProperties":true}}}},"403":{"description":"The authenticating key is scoped and cannot read policy (`forbidden`)","content":{"application/json":{"schema":{"description":"The authenticating key is scoped and cannot read policy (`forbidden`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Pod or inbox not found","content":{"application/json":{"schema":{"description":"Pod or inbox not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}},"put":{"operationId":"replacePolicy","summary":"Replace correspondent policy","tags":["policy"],"description":"Declaratively set a scope's entire policy in one call — ideal for automated provisioning. Sets the inbound and/or outbound mode and REPLACES the scope's own rules for any direction you include. Rules created here are fail-closed (an empty allowlist denies all). Omitted directions are left untouched. Inbox-scoped keys are rejected. A pod-scoped key may mutate its own pod or an inbox in that pod, never account-wide policy. Pod keys cannot set `none` or extra allows on a parent allowlist.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"inbound":{"type":"object","additionalProperties":false,"description":"Inbound (who may send to this scope) policy to set. Omit to leave inbound untouched.","properties":{"mode":{"type":"string","enum":["none","allowlist","inherit"],"description":"`none` = no filtering; `allowlist` = only listed correspondents are permitted (empty allowlist denies all — fail closed); `inherit` = defer to the parent scope (pod, then account). Not valid at the account scope."},"rules":{"type":"array","maxItems":500,"description":"Replaces this scope's own inbound rules. Omit to leave rules untouched; pass [] to clear them.","items":{"type":"object","additionalProperties":false,"required":["type","value"],"properties":{"type":{"type":"string","enum":["allow","block"]},"value":{"type":"string","minLength":3,"maxLength":255,"description":"An email address (`user@example.com`), a whole domain (`example.com`), or a domain and all its subdomains (`*.example.com`, which also covers `example.com` itself)."}}}}}},"outbound":{"type":"object","additionalProperties":false,"description":"Outbound (who this scope may send to) policy to set. Omit to leave outbound untouched.","properties":{"mode":{"type":"string","enum":["none","allowlist","inherit"],"description":"`none` = no filtering; `allowlist` = only listed correspondents are permitted (empty allowlist denies all — fail closed); `inherit` = defer to the parent scope (pod, then account). Not valid at the account scope."},"rules":{"type":"array","maxItems":500,"description":"Replaces this scope's own outbound rules. Omit to leave rules untouched; pass [] to clear them.","items":{"type":"object","additionalProperties":false,"required":["type","value"],"properties":{"type":{"type":"string","enum":["allow","block"]},"value":{"type":"string","minLength":3,"maxLength":255}}}}}}}}}}},"parameters":[{"schema":{"type":"string"},"in":"query","name":"podId","required":false,"description":"Confine the operation to one pod's policy. Accepts the pod ID. Mutually exclusive with `inboxId`."},{"schema":{"type":"string"},"in":"query","name":"inboxId","required":false,"description":"Confine the operation to one inbox's policy. Mutually exclusive with `podId`."}],"responses":{"200":{"description":"The resulting policy for the scope","content":{"application/json":{"schema":{"description":"The resulting policy for the scope","type":"object","additionalProperties":true}}}},"403":{"description":"The authenticating key is scoped and cannot manage policy","content":{"application/json":{"schema":{"description":"The authenticating key is scoped and cannot manage policy","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Pod or inbox not found","content":{"application/json":{"schema":{"description":"Pod or inbox not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"422":{"description":"Invalid mode for the scope, or an invalid rule value","content":{"application/json":{"schema":{"description":"Invalid mode for the scope, or an invalid rule value","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/policy/audit":{"get":{"operationId":"getPolicyAudit","summary":"List correspondent policy audit events","tags":["policy"],"description":"Read the append-only audit trail for correspondent policy, newest first. Two kinds of event share one stream: `rule_added`, `rule_removed`, `mode_changed`, and `scope_deleted` record who moved the boundary and how (`scope_deleted` covers a pod or inbox being deleted, which cascade-deletes its policy — the row carries the modes and rules that were removed, so lifting a restriction by deleting its scope is not a silent operation). `send_rejected` and `inbound_rejected` record what the boundary refused: a send the outbound policy would not make, and inbound mail it would not accept. Both are deduped to one row per inbox + correspondent per hour, so neither a looping agent nor an outside sender can flood the trail. Rows are never updated or deleted. Inbox-scoped keys are rejected. A pod-scoped key may read the trail for its own pod or an inbox in that pod (`?podId=` / `?inboxId=`), never the unfiltered account-wide trail.","parameters":[{"schema":{"type":"string"},"in":"query","name":"podId","required":false,"description":"Confine the operation to one pod's policy. Accepts the pod ID. Mutually exclusive with `inboxId`."},{"schema":{"type":"string"},"in":"query","name":"inboxId","required":false,"description":"Confine the operation to one inbox's policy. Mutually exclusive with `podId`."},{"schema":{"type":"string"},"in":"query","name":"action","required":false,"description":"Comma-separated actions to include: `rule_added`, `rule_removed`, `mode_changed`, `scope_deleted`, `send_rejected`, `inbound_rejected`. Omit for all."},{"schema":{"type":"string","enum":["inbound","outbound"]},"in":"query","name":"direction","required":false,"description":"`inbound` governs who may send to the inbox; `outbound` governs who the inbox may send to (enforced across To, Cc, Bcc, and Reply-To on every send path)."},{"schema":{"type":"string"},"in":"query","name":"since","required":false,"description":"ISO-8601 timestamp. Only events at or after this time (inclusive)."},{"schema":{"type":"string"},"in":"query","name":"until","required":false,"description":"ISO-8601 timestamp. Only events strictly before this time."},{"schema":{"type":"string"},"in":"query","name":"limit","required":false,"description":"Page size, 1–100. Defaults to 50."},{"schema":{"type":"string"},"in":"query","name":"offset","required":false,"description":"Rows to skip. Defaults to 0."}],"responses":{"200":{"description":"A page of audit events plus the unpaginated `total`. Each event carries its scope (with the pod name / inbox address resolved at read time, or null if since deleted), the acting console user where there was one, and an action-specific `detail` object: `type`/`valueType`/`value` for rule events, `from`/`to` for mode changes, and `rejectedRecipient`/`reason`/`matchedScope` for rejected sends.","content":{"application/json":{"schema":{"description":"A page of audit events plus the unpaginated `total`. Each event carries its scope (with the pod name / inbox address resolved at read time, or null if since deleted), the acting console user where there was one, and an action-specific `detail` object: `type`/`valueType`/`value` for rule events, `from`/`to` for mode changes, and `rejectedRecipient`/`reason`/`matchedScope` for rejected sends.","type":"object","additionalProperties":true}}}},"400":{"description":"An unknown `action`, or a `since`/`until` that is not a valid timestamp","content":{"application/json":{"schema":{"description":"An unknown `action`, or a `since`/`until` that is not a valid timestamp","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"403":{"description":"The authenticating key is scoped and cannot read the audit trail (`forbidden`)","content":{"application/json":{"schema":{"description":"The authenticating key is scoped and cannot read the audit trail (`forbidden`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Pod or inbox not found","content":{"application/json":{"schema":{"description":"Pod or inbox not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/policy/mode":{"put":{"operationId":"setPolicyMode","summary":"Set correspondent policy mode","tags":["policy"],"description":"Set the filtering mode for one direction of a scope's policy, without touching rules. Inbox-scoped keys are rejected. A pod-scoped key may mutate its own pod or an inbox in that pod, never account-wide policy. Pod keys cannot set `none`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["mode"],"properties":{"mode":{"type":"string","enum":["none","allowlist","inherit"],"description":"`none` = no filtering; `allowlist` = only listed correspondents are permitted (empty allowlist denies all — fail closed); `inherit` = defer to the parent scope (pod, then account). Not valid at the account scope."},"direction":{"type":"string","enum":["inbound","outbound"],"description":"`inbound` governs who may send to the inbox; `outbound` governs who the inbox may send to (enforced across To, Cc, Bcc, and Reply-To on every send path)."}}}}}},"parameters":[{"schema":{"type":"string"},"in":"query","name":"podId","required":false,"description":"Confine the operation to one pod's policy. Accepts the pod ID. Mutually exclusive with `inboxId`."},{"schema":{"type":"string"},"in":"query","name":"inboxId","required":false,"description":"Confine the operation to one inbox's policy. Mutually exclusive with `podId`."}],"responses":{"200":{"description":"The updated mode","content":{"application/json":{"schema":{"description":"The updated mode","type":"object","additionalProperties":true}}}},"403":{"description":"The authenticating key is scoped and cannot manage policy","content":{"application/json":{"schema":{"description":"The authenticating key is scoped and cannot manage policy","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Pod or inbox not found","content":{"application/json":{"schema":{"description":"Pod or inbox not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"422":{"description":"Invalid mode for the scope","content":{"application/json":{"schema":{"description":"Invalid mode for the scope","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/policy/rules":{"post":{"operationId":"addPolicyRule","summary":"Add correspondent policy rule","tags":["policy"],"description":"Add one allow/block rule for a direction of a scope's policy. Inbox-scoped keys are rejected. A pod-scoped key may mutate its own pod or an inbox in that pod, never account-wide policy. Pod keys cannot add allows on a parent allowlist (blocks are fine).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["type","value"],"properties":{"type":{"type":"string","enum":["allow","block"]},"value":{"type":"string","minLength":3,"maxLength":255,"description":"An email address (`user@example.com`), a whole domain (`example.com`), or a domain and all its subdomains (`*.example.com`, which also covers `example.com` itself)."},"direction":{"type":"string","enum":["inbound","outbound"],"description":"`inbound` governs who may send to the inbox; `outbound` governs who the inbox may send to (enforced across To, Cc, Bcc, and Reply-To on every send path)."}}}}}},"parameters":[{"schema":{"type":"string"},"in":"query","name":"podId","required":false,"description":"Confine the operation to one pod's policy. Accepts the pod ID. Mutually exclusive with `inboxId`."},{"schema":{"type":"string"},"in":"query","name":"inboxId","required":false,"description":"Confine the operation to one inbox's policy. Mutually exclusive with `podId`."}],"responses":{"201":{"description":"Rule created","content":{"application/json":{"schema":{"description":"Rule created","type":"object","additionalProperties":true}}}},"403":{"description":"The authenticating key is scoped and cannot manage policy","content":{"application/json":{"schema":{"description":"The authenticating key is scoped and cannot manage policy","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Pod or inbox not found","content":{"application/json":{"schema":{"description":"Pod or inbox not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"409":{"description":"An identical rule already exists (`rule_exists`)","content":{"application/json":{"schema":{"description":"An identical rule already exists (`rule_exists`)","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"422":{"description":"The value is not a valid email or domain","content":{"application/json":{"schema":{"description":"The value is not a valid email or domain","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}},"/v1/policy/rules/{id}":{"delete":{"operationId":"deletePolicyRule","summary":"Delete correspondent policy rule","tags":["policy"],"description":"Remove a single policy rule by its ID. Inbox-scoped keys are rejected. A pod-scoped key may delete a rule on its own pod or an inbox in that pod, never an account-wide rule.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"The rule ID to delete"}],"responses":{"204":{"description":"Rule deleted"},"403":{"description":"The authenticating key is scoped and cannot manage policy","content":{"application/json":{"schema":{"description":"The authenticating key is scoped and cannot manage policy","allOf":[{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"Rule not found","content":{"application/json":{"schema":{"description":"Rule not found","allOf":[{"$ref":"#/components/schemas/Error"}]}}}}}}}},"servers":[{"url":"https://api.openmail.sh"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"pods","description":"Isolated sub-accounts for your end users or tenants"},{"name":"inboxes","description":"Email inboxes and their webhook configuration"},{"name":"domains","description":"Custom domains: add your own sending/receiving domain, publish DNS, and verify it"},{"name":"messages","description":"Sending and listing messages"},{"name":"threads","description":"Conversation threads and read status"},{"name":"attachments","description":"Downloading and extracting text from attachments"},{"name":"policy","description":"Correspondent policy — provider-enforced allow/block rules and allowlist mode, scoped per account, pod, or inbox. Account-wide key only."},{"name":"feedback","description":"Reporting bugs, friction, and feature requests to the OpenMail team"}]}