# 在 Abnormal 上开发

Abnormal 工作区的管理 API 和 MCP 服务器：用你自己的代码或 AI Agent 管理域名、邮箱、别名、邮件列表和 Webhook，密钥按权限范围授权，事件带签名。

最近核对：2026-11-12 · API 版本：1.8.0

## API 能做什么

API 管理一个工作区的邮件服务。它能做的事，管理员在管理控制台里也都能做；有了 API，就可以交给脚本、集成或 Agent 去做。

- **域名**：验证所有权、接入域名、检查 DNS、设置 catch-all、添加别名域名。
- **邮箱**：创建、删除和恢复邮箱，修改密码、状态、容量、备注和每日发信上限。
- **地址**：转发规则、普通别名和邮件列表。
- **合规与报表**：法律保留、不活跃邮箱、各域名的存储用量和发信最多的邮箱。
- **事件**：邮箱、域名、订单、批量任务和安全事件的签名 Webhook。
- **收发邮件**：用你的域名发信、跟踪每封邮件的投递，并以 Webhook 接收邮件（需开通）。
- **Agent 邮箱**：一次调用为 AI Agent 建好邮箱，读收件箱、按会话回复，并发一把只能用于这个邮箱的密钥。
- **邮件应用**：让邮箱用户通过 OAuth 授权你的应用读取他们的邮件、以他们的身份发信。
- **投递**：抑制列表、邮件追踪，以及替你读回的延迟退信和垃圾邮件投诉。
- **客户端与连接器**：TypeScript 和 Python SDK、命令行工具、n8n 和 Zapier，以及飞书、钉钉群机器人（见下方应用目录）。
- **批量**：把多项修改放进一个按顺序执行、可安全重试的请求。

## 申请工作区和密钥

1. **申请工作区。** API 属于管理控制台里的一个工作区。[申请工作区](https://console.abnormalhq.com/signup)后会有人工审核，通过后你会作为管理员收到邀请。如果申请页提供“开发者工作区”，选它即可在确认邮箱后立即获得一个，用测试密钥试用 API。
2. **创建密钥。** 在管理控制台打开 **设置 → API 密钥**，创建一个密钥，只勾选代码需要的权限范围。只有管理员能创建密钥。
3. **立即保存。** 完整的密钥只在创建时显示一次。把它放在密钥管理工具或环境变量里，不要写进代码仓库、网址或日志。

密钥能做的事永远不会超过其所有者当前在控制台里的角色；吊销后，下一次请求就会被拒绝。工作区的每位管理员都能看到并吊销其他账号的密钥。

## 第一个请求

所有请求都发往 `https://console.abnormalhq.com/api/v1`，并以 Bearer 令牌的形式带上密钥。建议第一次先调用 `GET /capabilities`：它会返回可用的权限范围、功能和当前生效的限制。

```sh
API_BASE='https://console.abnormalhq.com/api/v1'
printf 'API key: '; stty -echo; IFS= read -r API_KEY; stty echo; printf '\n'

curl --fail-with-body "$API_BASE/capabilities" \
  -H "Authorization: Bearer $API_KEY"
```

用隐藏输入读入密钥，它就不会留在 shell 历史里。用完后运行 `unset API_KEY`。

## 权限范围

每个操作需要一个权限范围，在下方的接口列表里标在操作旁边。写权限不包含对应的读权限。有两个例外：`POST /batch` 会逐一检查其中每项操作的权限范围，`GET /batch/{requestId}` 只接受提交该批次的那个密钥。

- `domains:read`、`domains:write`：域名、接入申请、DNS 与连接状态、catch-all、别名域名。
- `mailboxes:read`、`mailboxes:write`：邮箱、回收站、发信上限和用量报表。
- `forwarders:*`、`aliases:*`、`lists:*`：转发规则、普通别名和邮件列表。
- `holds:*`、`webhooks:*`、`suppressions:*`、`trace:read`：法律保留、Webhook、抑制列表和邮件追踪。
- `agent_tasks:*`：跟进任务，即以某个邮箱身份回复邮件的 Agent。
- `emails:*`、`inbound:*`：API 发信与收信路由（需手动勾选，新密钥默认不含）。
- `agent_mailboxes:*`、`mail_apps:*`：Agent 邮箱，以及邮箱用户可以授权的应用（同样需手动勾选）。

域名、转发、别名、邮件列表、法律保留和 Webhook 即使只读也需要管理员角色；邮箱和跟进任务的权限范围运营人员也能用，其中的读权限查看者也能用。

## 请求、响应与错误

- 请求体是 JSON（`Content-Type: application/json`），最大 16 KiB。
- 成功结果是 `{ "data": … }`，列表另带 `pagination`。创建成功返回 `201`。
- 路径里的 `{id}` 是 API 返回的记录 ID，不是邮箱地址或域名。
- `POST /batch` 按顺序执行 1 到 25 个操作，并且必须带 `Idempotency-Key`：用同一个键重发同一批操作，会直接返回保存的结果，不会执行两次。

所有错误的格式都一样。请根据 `code` 判断，不要根据 `message`：后者是写给人看的，内容可能会变。

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Try again in 42 seconds.",
    "details": { "retryAfter": 42, "limit": 300 }
  }
}
```

遇到超时、连接中断或 `502` 后，先读取当前状态，再决定是否重做写操作。返回 `409` 且错误码为 `provisioning_uncertain` 时，邮箱可能已经创建也可能没有：请用 `details.mailboxId` 查询，不要再创建一次。

## 频率限制

- 按密钥计数，REST 和 MCP 合并计算。默认每分钟 300 次；当前生效的限制由 `GET /capabilities` 的 `limits.requestsPerMinute` 给出。
- 每个响应都带 `RateLimit-Limit`、`RateLimit-Remaining`、`RateLimit-Reset` 和 `RateLimit-Policy`。
- 超出限制时返回 `429`，并用 `Retry-After` 给出需要等待的秒数。被拒绝的请求什么都没做，等待之后重发是安全的。
- 同一地址发来太多无效密钥，该地址会被暂时封禁，有效密钥也会被拒绝。请修好发错密钥的客户端。

## Webhook

Webhook 是一个 `https` 地址，工作区里发生事情时会向它发送 POST 请求，你就不用轮询。用 `POST /webhooks` 创建；签名密钥（`whsec_…`）只在这次响应里出现一次。每个工作区最多 10 个。

每次投递都在 `X-Mailhq-Signature` 头里签名，格式为 `t=<milliseconds>,v1=<hex HMAC-SHA256>`（毫秒时间戳和十六进制的 HMAC-SHA256），签名内容是时间戳、一个点和原始请求体。先校验签名，再信任请求体：

```js
import { createHmac, timingSafeEqual } from "node:crypto";

/** rawBody: the body exactly as received; header: the X-Mailhq-Signature value. */
export function verifyMailhqWebhook(secret, rawBody, header, toleranceMs = 5 * 60_000) {
  const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? "");
  if (!match) return false;
  const timestamp = Number(match[1]);
  if (Math.abs(Date.now() - timestamp) > toleranceMs) return false;
  const expected = createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest();
  const given = Buffer.from(match[2], "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}
```

- 在 10 秒内返回 `2xx`，之后再处理业务。
- 投递至少一次且不保证顺序：用事件的 `id` 去重。
- 投递失败会以逐渐拉长的间隔重试约 17 小时。连续失败 3 天的 Webhook 会被停用，直到你重新启用。
- `POST /webhooks/{id}/test` 会发送一个测试事件，用来检查接收端。**设置 → 开发者** 会列出所有 Webhook 最近的投递及接收方的回应，并可重新投递；状态页会显示 Webhook 投递是否准时。
- 想不写接收端就在群聊里收到事件，创建 Webhook 时设 `"format": "feishu"` 或 `"dingtalk"`，填写群机器人的地址（签名密钥填在 `botSecret`）：每个事件会以一条简短消息到达。

## 收发邮件

用你自己的域名、通过域名自己的邮件服务器发信，跟踪每封邮件的投递结果，并把指定邮箱收到的邮件推送到 Webhook。发信功能默认关闭，需由我们为你的工作区开通并设定每日上限，请来信申请。测试密钥可以直接向沙箱发信。

- `PUT /domains/{id}/sending` 为域名创建发信账号：邮件以该域名的 DKIM 密钥签名发出，退信回到这个账号，From 可以是该域名下的任意地址。
- `POST /emails` 发送一封邮件（`POST /emails/batch` 一次最多 50 封）；带上 `Idempotency-Key` 即可安全重试。抑制列表中的收件人会被跳过。
- `GET /emails/{id}` 给出每个收件人的结果：已发出、已送达、投递延迟或退信。同样的变化也会以 `email.sent`、`email.delivered`、`email.delivery_delayed`、`email.bounced` 和 `email.failed` 事件推送。
- `PUT /mailboxes/{id}/inbound` 开启收信路由：邮箱收到的每封邮件都会以 `message.received` 推送，附带纯文本正文；完整邮件和附件用 `GET /mailboxes/{id}/messages/{emailId}` 读取。用别名或 catch-all 可以把多个地址汇入同一个路由邮箱。
- 如果你的套餐按发信量计费，需先在 **设置 → 开发者 → 计费** 中订阅，否则正式密钥发信会得到 `402 billing_required`。每封交给邮件服务器的邮件计一次，与收件人数量无关。
- 管理后台的 **设置 → 开发者** 会显示当天的发信额度、最近的 API 调用及其错误码、每封发出的邮件和每个收件人的结果。
- 回到域名发信账号的延迟退信和垃圾邮件投诉（反馈环报告）会被读回：收件人变为 `bounced`，或得到 `complainedAt` 和一个 `email.complained` 事件，并被加入抑制列表。
- 新开通的工作区会先预热：第一天最多 50 封，此后每天翻倍，直到上限。退信或投诉过多时正式发信会自动暂停（`403 email_api_paused`，并推送 `email.sending_paused` 事件），直到我们核查。
- 只能走 SMTP 的软件可以用 `POST /domains/{id}/smtp-credentials` 经域名自己的服务器发信：这个密码只能登录和发信，与 API 密钥分开创建和撤销。这样发出的邮件不是 API 邮件：没有 `GET /emails` 记录，也没有投递事件，计入的是发信账户在服务器上自己的每日限额，而不是工作区的发信额度。

## Agent 邮箱

Agent 邮箱是你域名下一个真正的邮箱，一次调用就为 AI Agent 建好，收信路由和发信都已开启。

- `POST /agent-mailboxes` 传入 `domain` 和 `name` 即可创建；`GET /agent-mailboxes/{id}/messages` 读取收件箱，`POST …/send` 发送新邮件，`POST …/messages/{emailId}/reply` 在原会话中回复。
- `POST /agent-mailboxes/{id}/keys` 返回一把只对这个邮箱有效的密钥：交给 Agent，它就碰不到工作区里的其他任何东西。
- Agent 收到的邮件同时以 `message.received` 事件推送，无需轮询。`PATCH` 和 `DELETE …/messages/{emailId}` 可把邮件标为已读或删除，`GET …/attachments/{index}` 可下载附件。

## 由邮箱用户授权的应用

你的应用可以在邮箱用户同意后读取其邮件、以其身份发信，全程看不到用户的密码：在 Abnormal 网页邮箱上使用 OAuth 2.0 授权码流程加 PKCE。

- 用 `POST /mail-apps` 注册应用：名称、回调地址，以及它可以申请的权限 `mail.read`、`mail.send` 和 `mail.modify`。所有应用都是公开客户端：没有 client secret，必须使用 PKCE（`S256`）。
- 把用户引导到网页邮箱的 `/oauth/authorize`；用户登录后会看到你的应用名称和申请的权限，确认即可。在 `/api/oauth/token` 用授权码换取访问令牌（1 小时）和每次使用都会轮换的刷新令牌。
- 带着令牌调用 Mail API：`GET /api/mail/v1/messages` 和 `/messages/{id}` 读取、搜索邮件（`…/attachments/{index}` 下载附件），`POST /api/mail/v1/send` 以该邮箱发信，可带附件并在“已发送”中留存副本；有 `mail.modify` 时应用还能标记、移动和删除邮件。
- 用户随时可以在网页邮箱设置里撤销授权，修改邮箱密码也会终止全部授权；`DELETE /mail-apps/{id}` 会一次终止你的应用的全部授权。

## 给 AI Agent 用的 MCP

同样的操作也作为工具提供在 MCP 服务器 `https://console.abnormalhq.com/api/mcp` 上（Streamable HTTP），密钥、权限范围和频率限制都相同。只给 Agent 它需要的权限范围。以 Claude Code 为例：

```sh
claude mcp add --transport http abnormal https://console.abnormalhq.com/api/mcp \
  --header "Authorization: Bearer $ABNORMAL_API_KEY"
```

支持 OAuth 登录的客户端（例如 claude.ai、Claude Desktop 和 Claude Code 上的 Claude）只需要这个地址：它们会自行注册，由管理员在授权页上批准权限，令牌会自动续期。不加请求头，客户端就会打开登录页。

## 应用目录

除了自己调用 API，把其他软件接入 Abnormal 的所有方式。管理控制台的 **设置 → 开发者 → 集成** 也列出了同样的内容。

- **Claude 及其他 MCP 客户端**（可用）：上文的 MCP 服务器，支持 OAuth 登录。
- **你自己的应用，面向你的邮箱用户**（可用）：使用 OAuth 的邮件应用，见上文。
- **飞书和钉钉**（可用）：把 Webhook 事件或你的控制台通知推送到群机器人。
- **Slack 和 Telegram**（可用）：把你的控制台通知发到频道或会话。
- **TypeScript SDK、Python SDK 和 `abnormal` 命令行工具**（尚未发布）：带类型的调用、重试、幂等键和 Webhook 校验；命令行工具将可用 `brew install abnormalhq/tap/abnormal` 安装。
- **n8n 和 Zapier**（尚未发布）：n8n 的节点和触发器，以及 Zapier 的触发器、动作和搜索。

## 版本与变更

- 版本写在路径里：`/api/v1`。在同一版本内只做新增：新接口、新的可选字段、新的响应字段、新的枚举值和新的事件类型。你的代码应当能接受不认识的字段和值。
- 在 `v1` 里删除或修改任何东西都会先公告，在规范里标为弃用，并在每个响应里带上 `Deprecation` 和 `Sunset` 头：代码会发送的内容至少提前 6 个月，只会读取的字段或值至少提前 3 个月。
- 下方接口列表所依据的 OpenAPI 3.1 规范可以[下载 JSON 文件](https://developers.abnormalhq.com/openapi.json)，供代码生成器和 API 工具使用。
- [服务状态页](https://console.abnormalhq.com/status)会显示管理 API 近 30 天的可用率（每分钟检测一次），以及故障和维护。你自己的监控可以轮询 `GET https://console.abnormalhq.com/api/health`，无需密钥。

## 测试模式

创建密钥时打开 **测试密钥**，就可以在不动到正式域名和邮箱的情况下开发集成。它的令牌以 `mhq_test_` 开头，所有调用都发生在本工作区的沙箱里；沙箱的邮件服务器是模拟的，不会投递任何邮件，也不会查询 DNS。

- 域名必须以工作区的专属区域 `<slug>.test` 结尾（例如 `shop.acme.test`），`GET /capabilities` 会在 `sandbox.domainZone` 里给出它。所有权验证无需 DNS 即可通过。
- 邮箱、别名、邮件列表、转发、法律保留、批量操作和抑制列表都和正式工作区一样可用，Webhook 也会真实投递。
- 发信无需开通：除本地部分以 `bounce` 开头的地址会退信外，所有收件人都会立即“送达”；发到开启收信路由的沙箱邮箱的邮件会以 `message.received` 推送。
- 邮件追踪、跟进任务、传输安全、读取收到的邮件（包括 Agent 邮箱的）和邮件应用需要真实的邮件，测试密钥调用它们会得到 `403 test_mode_unsupported`。
- 每个沙箱最多 5 个域名、100 个邮箱。

## 暂不提供

- 未经邮箱用户授权读取未开启路由的邮箱。
- 打开与点击跟踪：这是有意不做的，它会读取人们怎么处理自己的邮件，我们的网页邮箱也不这样做。
- SDK 包、命令行工具以及 n8n、Zapier 的上架已经做好但尚未发布：发布后本页会给出链接。

邮箱仍然通过网页邮箱、IMAP 和 SMTP 收发自己的邮件。关于 API 的问题请发邮件到 [support@abnormalhq.com](mailto:support@abnormalhq.com)。

## 错误码

每个错误都有一个稳定的 `code`。下表列出全部错误码，以及对应的状态码、含义、处理办法和能否原样重试。`v1` 内可能新增错误码，遇到不认识的请按 HTTP 状态码处理。说明为英文。

- **不能**：请求不改就会一直以同样方式失败。
- **稍后**：什么都没发生，等一会儿可能成功。有 `Retry-After` 时按它等待。
- **先核对**：结果未知或状态已变。先读取当前状态再决定，盲目重发可能把事情做两遍。

### 认证、请求格式与限制

- `body_too_large` (413): The request body is over 16 KiB (8 MiB for POST /emails and /emails/batch). 处理办法：Send less per request; use several requests or a batch. 重试：不能.
- `endpoint_retired` (410, 预留): The operation was retired after its announced sunset date. 处理办法：Use the replacement in `details.replacement`; see the API changelog. 重试：不能.
- `field_retired` (422, 预留): The request sent a field, query parameter or enum value retired after its sunset date. 处理办法：Drop it or use `details.replacement`; `details.field` names it. 重试：不能.
- `forbidden` (403): The key lacks a scope the operation needs, or its owner's current role no longer allows it. 处理办法：Create a key with the scope listed for the operation in the reference, owned by an account whose role allows it. 重试：不能.
- `invalid_arguments` (400): An MCP tool was called with arguments that are not a JSON object of the tool's own fields. 处理办法：Pass exactly the fields in the tool's input schema. 重试：不能.
- `invalid_id` (400): An ID in the path or body is not in the format of a record ID. 处理办法：Use the IDs the API returns. 重试：不能.
- `invalid_json` (400): The body is not valid JSON, or not a JSON object. 处理办法：Send a JSON object. 重试：不能.
- `invalid_query` (400, 422): A query parameter is unknown, malformed or out of range. 处理办法：Fix the parameter `details` names; the reference lists each operation's parameters and their ranges. 重试：不能.
- `method_not_allowed` (405): The path exists but not with this HTTP method. 处理办法：Use one of the methods in the `Allow` header. 重试：不能.
- `not_found` (404): The record in the path does not exist in this workspace (or is in the recycle bin, for mailboxes). 处理办法：Check the ID: paths take the record IDs the API returns, not addresses or domain names. 重试：不能.
- `rate_limited` (429): The key's (or the address's) rate limit is used up for this window; nothing was done. 处理办法：Wait `Retry-After` seconds, then send the same request again. Use batches for bulk changes. 重试：稍后.
- `unauthorized` (401): No Bearer key was sent, or the key is unknown, expired, revoked or no longer valid for its owner. 处理办法：Send a working key in `Authorization: Bearer`. Repeated invalid keys get the sending address rate-limited. 重试：不能.
- `unknown_field` (400): The body has a field the operation does not take. 处理办法：Remove it; `details` names it. 重试：不能.
- `unsupported_media_type` (415): A request body was sent without `Content-Type: application/json`. 处理办法：Send JSON with that content type. 重试：不能.

### 域名、接入与别名域名

- `alias_changed` (409): The alias domain changed while the request ran. 处理办法：Read it again, then decide whether to repeat the request. 重试：先核对.
- `alias_limit` (409): The domain already has the most alias domains allowed. 处理办法：Remove one before adding another. 重试：不能.
- `already_exists` (409): The domain, alias domain or address already exists (here, or claimed by another workspace). 处理办法：Use the existing record, or choose another name. 重试：不能.
- `dns_lookup_failed` (502): The DNS lookup for the ownership check failed; nothing was changed. 处理办法：Try again later. 重试：稍后.
- `domain_binding_conflict` (409): The domain's server binding is ambiguous or changed while the request ran. 处理办法：Have an admin review the domain's binding, then try again. 重试：先核对.
- `domain_changed` (409): The domain's binding changed while the request ran. 处理办法：Read the domain again, then decide whether to repeat the request. 重试：先核对.
- `domain_has_aliases` (409): The domain still has alias domains. 处理办法：Remove its alias domains first. 重试：不能.
- `domain_not_empty` (409): The domain still has local mailboxes, so its record cannot be removed. 处理办法：Delete or move its mailboxes first. 重试：不能.
- `domain_not_provisioned` (409): The domain to import is not on the chosen server. 处理办法：Create it on that server first, or onboard it with `/domain-onboardings`. 重试：不能.
- `domain_onboarding_conflict` (409): The domain's onboarding has not finished, or its state does not allow this step. 处理办法：Read the onboarding's state and DNS status; never submit the remote provisioning twice. 重试：先核对.
- `domain_ownership_required` (403): The domain cannot be imported without a verified ownership onboarding in this workspace. 处理办法：Onboard it with `/domain-onboardings` instead. 重试：不能.
- `domain_unbound` (409): The domain is not bound to a valid mail server, so nothing on it can be changed remotely. 处理办法：Bind the domain to a server in the admin console. 重试：不能.
- `invalid_domain` (400): The domain name is missing or not a valid name. 处理办法：Send a lowercase domain name such as example.com. 重试：不能.
- `invalid_domain_update` (400): The status change does not say whether to enable or disable the domain. 处理办法：Send `{ "enabled": true }` or `{ "enabled": false }`. 重试：不能.
- `ownership_unverified` (409): The ownership TXT record was not found with the expected value. 处理办法：Publish the record the connection details show, wait for DNS, then verify again. 重试：稍后.
- `remote_domain_delete_unavailable` (422): Deleting a domain on the mail server is not offered. 处理办法：Disable the domain, then remove only the local record; the server keeps the domain and its data. 重试：不能.
- `subscription_not_found` (404): The mail server in `subscriptionId` does not exist or is not available to this workspace. 处理办法：Use an ID from `GET /servers`. 重试：不能.
- `subscription_required` (400): Several mail servers are configured and the request does not say which one to use. 处理办法：Send `subscriptionId` (one of `GET /servers`). 重试：不能.
- `verification_expired` (409): The alias domain's ownership value expired before it was verified. 处理办法：Remove the alias domain, add it again and publish the new value. 重试：不能.

### 邮箱与发信上限

- `invalid_daily_limit` (400): The daily sending limit is not an integer in range. 处理办法：Send an integer from 0 to 1000000. 重试：不能.
- `invalid_mailbox` (422): The mailbox fields failed validation. 处理办法：Fix the fields listed in `details.fields`. 重试：不能.
- `invalid_mailbox_ids` (400): The bulk sending-limit request does not name 1–100 distinct mailbox IDs. 处理办法：Send between 1 and 100 distinct mailbox record IDs. 重试：不能.
- `invalid_note` (400, 422): The note (of a mailbox or a suppression) is too long or not text. 处理办法：Send at most the documented number of characters. 重试：不能.
- `invalid_password` (400): The password is not 8–128 characters without whitespace. 处理办法：Send a password that meets the rule. 重试：不能.
- `invalid_quota` (400): The quota is not a whole number of MB in range. 处理办法：Send an integer from 0 (no limit) to the documented maximum. 重试：不能.
- `invalid_status` (400): The status is not one of `active`, `disabled` or `pending`. 处理办法：Send one of those values. 重试：不能.
- `invalid_username` (400): The username (the part before @) is not valid. 处理办法：Use 1–64 lowercase letters, digits, `.`, `_` or `-`, starting and ending with a letter or digit. 重试：不能.
- `provisioning_uncertain` (409): The mailbox write was sent to the server but its result is not confirmed; a pending record was kept. 处理办法：Look the mailbox up with `details.mailboxId` and verify it; do not create it again. 重试：先核对.
- `sending_limit_unavailable` (422): A sending limit cannot be set for this mailbox (not created on the server yet, or not supported). 处理办法：Create the mailbox first, or leave the limit to the server's default. 重试：不能.
- `sending_limit_uncertain` (409): The sending-limit write may or may not have been applied. 处理办法：Read the limit back (GET); repeat only the same target value, never a different one, if it is not set. 重试：先核对.

### 别名、邮件列表、转发与 catch-all

- `address_taken` (409): The address is already used by a mailbox, service mailbox, alias, list, forwarder or a mailbox in the recycle bin. 处理办法：Choose another address. 重试：不能.
- `external_member` (422): The list has members outside the workspace's domains, which this workspace does not allow. 处理办法：Remove the outside addresses, or ask an admin to allow external members. 重试：不能.
- `forwarding_loop` (409): The rule forwards to itself or would close a loop with existing forwarding. 处理办法：Choose a different destination. 重试：不能.
- `invalid_alias` (400): The alias fields are invalid. 处理办法：Fix the username, target or description. 重试：不能.
- `invalid_alias_target` (409): The alias target is not an existing managed mailbox of the same domain. 处理办法：Point the alias at a mailbox of its own domain. 重试：不能.
- `invalid_catch_all` (400): The catch-all setting is invalid. 处理办法：Send a mailbox address of the domain, or null to reject mail to unknown addresses. 重试：不能.
- `invalid_catch_all_destination` (409): The catch-all destination is not an existing managed mailbox of this domain. 处理办法：Use one of the domain's own mailboxes. 重试：不能.
- `invalid_forwarder` (400): The forwarding rule's source or destination is invalid. 处理办法：Send one source address of the domain and one destination address. 重试：不能.
- `invalid_list` (400): The distribution list fields are invalid. 处理办法：Fix the username, description or members. 重试：不能.
- `list_limit` (409): The domain already has the most distribution lists allowed. 处理办法：Delete a list before creating another. 重试：不能.
- `readonly_alias` (409): This alias is a special one managed on the mail server (or a user's masked address) and cannot be changed here. 处理办法：Leave it as it is. 重试：不能.
- `readonly_forwarder` (409): This rule is not managed here (a list, alias, catch-all or custom Sieve rule). 处理办法：Change it where it is managed. 重试：不能.
- `readonly_list` (409): This list is managed on the mail server and cannot be changed here. 处理办法：Leave it as it is. 重试：不能.
- `unsupported` (422): The domain's mail server does not support this kind of address management. 处理办法：Use a Stalwart 1.0 or later server for the domain. 重试：不能.

### 批量操作

- `batch_stopped` (424): An item of a batch that was not run because an earlier item's outcome is unconfirmed (item `error.code`). 处理办法：Verify the earlier item, then send this one again in a new batch. 重试：先核对.
- `idempotency_conflict` (409): This idempotency key was already used, with this key, for a different batch (or follow-up task, or messages sent through /emails). 处理办法：Use a new idempotency key for different content. 重试：不能.
- `idempotency_running` (409): A batch with this idempotency key is still running. 处理办法：Read its state with `GET /batch/{requestId}`; do not resend under another key. 重试：先核对.
- `idempotency_uncertain` (409): A batch with this idempotency key stopped with an unconfirmed write. 处理办法：Verify the affected records, then send only what is confirmed missing, in a new batch with a new key. 重试：先核对.
- `invalid_batch` (400): The batch body is invalid: not 1–25 operations, duplicate item IDs, an unknown operation or bad input; for `POST /emails/batch`, `emails` is not a list of 1 to 50 messages. 处理办法：Fix the batch; nothing in it was run or sent. 重试：不能.
- `invalid_idempotency_key` (400): The `Idempotency-Key` header is missing or not 8–128 letters, digits, `-` or `_`. 处理办法：Send a valid key, unique per batch. 重试：不能.
- `request_budget_exceeded` (422): The batch reached its request budget on the mail server and the remaining operations were stopped. 处理办法：Check the results, then send the rest in smaller batches. 重试：先核对.

### Webhook

- `delivery_in_progress` (409): The delivery is still waiting for its own retry. 处理办法：Only a delivered or dead-lettered delivery can be sent again; wait for it to settle. 重试：稍后.
- `invalid_description` (400): The description is longer than 200 characters. 处理办法：Shorten it. 重试：不能.
- `invalid_enabled` (400): `enabled` is not a boolean. 处理办法：Send true or false. 重试：不能.
- `invalid_events` (400): The events list names an unknown event type. 处理办法：Use the event types in the reference, or `["*"]`. 重试：不能.
- `invalid_webhook` (400): A webhook update changes nothing. 处理办法：Send at least one of `url`, `description`, `events` and `enabled`. 重试：不能.
- `invalid_webhook_format` (400): `format` is not standard, feishu or dingtalk, `botSecret` came without a bot format or is not one, or a PATCH tried to change either (they are set when the webhook is made). 处理办法：Send one of the formats, and a bot's signing secret only with a bot format; to change the format, make a new webhook. 重试：不能.
- `invalid_webhook_url` (400): The address is not https on the default port with a public host name, or, for a bot format, not that bot platform's webhook address. 处理办法：Use a public https address without credentials, IP literal or internal name. 重试：不能.
- `webhook_limit` (409): The workspace already has the most webhooks allowed (10). 处理办法：Delete one before creating another. 重试：不能.

### 法律保留与抑制列表

- `hold_not_active` (409): The legal hold was already released or has ended. 处理办法：Nothing to release; place a new hold if needed. 重试：不能.
- `invalid_address` (422): The suppression list address is not a valid email address. 处理办法：Send one valid address. 重试：不能.
- `invalid_ends_at` (400): The hold's end date is not a future ISO 8601 time. 处理办法：Send a future date, or none. 重试：不能.
- `invalid_reason` (400): The required reason is missing or too long. 处理办法：Send a reason. 重试：不能.

### 邮件追踪

- `missing_criteria` (400): A trace search names none of sender, recipient, Message-ID or queue ID. 处理办法：Give at least one of them. 重试：不能.
- `trace_unavailable` (409): No mail server whose logs can be read is configured. 处理办法：Ask an admin to connect a mail server. 重试：不能.

### 跟进任务

- `already_decided` (409): This email awaiting approval was already approved or rejected. 处理办法：Read the task's approvals again. 重试：不能.
- `auto_send_disabled` (409): An admin switched automatic sending off, so every email of a task needs approval. 处理办法：Create the task with approval required, or ask an admin. 重试：不能.
- `invalid_agent_task` (400, 422): The follow-up task's fields are invalid. 处理办法：Fix what the message describes. 重试：不能.
- `invalid_state` (409): The task is not in a state that allows this (only a running task can be paused, a paused one resumed). 处理办法：Read the task's state first. 重试：先核对.
- `mailbox_unavailable` (409): The task's mailbox cannot be used (disabled, no stored password, or its domain has no mail server). 处理办法：Fix the mailbox, or use another one. 重试：不能.
- `task_finished` (409): The task has already ended. 处理办法：Create a new task if more follow-up is needed. 重试：不能.

### 收发邮件、Agent 邮箱与邮件应用

- `attachments_too_large` (413): The attachments are over 5 MiB in all, decoded. 处理办法：Send smaller files, or a link to them. 重试：不能.
- `billing_required` (402): The workspace's plan bills sending through the API, and the workspace has no subscription that may send (none, canceled, unpaid). 处理办法：An admin subscribes, or fixes the payment, under Settings → Developers → Billing. `details.status` is the subscription's Stripe status. 重试：不能.
- `domain_not_sending` (409): The domain does not send through the API, so it has no sending account to make SMTP relay credentials for. 处理办法：Turn sending on for the domain (`PUT /domains/{id}/sending`) first. 重试：不能.
- `email_api_not_approved` (403): Sending through the API is not turned on for this workspace: a platform admin has not approved it. 处理办法：Ask the platform's operator to approve it (with a daily limit). Test keys can send to the sandbox meanwhile. 重试：不能.
- `email_api_paused` (403): Live sending is paused for the workspace: in the last 7 days (or since it was last resumed) too many recipients bounced or complained. `details.reason` says which, `details.pausedAt` since when. 处理办法：Find out why (a stale list, a missing unsubscribe, a stolen key) and fix it, then ask the platform's operator to resume sending. Test keys still send to the sandbox. 重试：不能.
- `email_daily_limit` (429): The workspace's daily allowance of messages is used up (a batch counts all of its messages). 处理办法：Send again after `details.resetAt` (midnight UTC), or ask for a higher limit. Nothing of the request was sent. 重试：稍后.
- `from_domain_not_sending` (422): The From address is not on a domain of this workspace that sends through the API. 处理办法：Turn sending on for the domain (`PUT /domains/{id}/sending`), or send from a domain that has it. `details.index` names the message in a batch. 重试：不能.
- `invalid_agent_name` (400): An agent mailbox's `name` is not a single line of at most 200 characters without quotes or angle brackets. 处理办法：Fix it. 重试：不能.
- `invalid_attachments` (400): An attachment is malformed: no filename, content that is not base64, a bad content type, or more than 10 attachments. 处理办法：Fix the attachments. 重试：不能.
- `invalid_body` (400): The message has neither `text` nor `html`, or one of them is too long. 处理办法：Send `text`, `html` or both, within the documented sizes. 重试：不能.
- `invalid_from` (400): `from` is not an address or `Name <address>`. 处理办法：Fix it. 重试：不能.
- `invalid_headers` (400): `headers` holds a header that is not allowed (only X- headers, List-Unsubscribe and List-Unsubscribe-Post), repeats one, or has a multi-line value. 处理办法：Fix the headers. 重试：不能.
- `invalid_mail_app` (400): A mail app needs a name, 1 to 5 redirect URIs (https on a public host, or http on the loopback) and scopes from mail.read, mail.send and mail.modify. 处理办法：Fix the app. 重试：不能.
- `invalid_message_change` (400): A change to an agent mailbox's message needs `read` and/or `flagged`, each true or false, and nothing else. 处理办法：Send `{ "read": true }`, `{ "flagged": false }` or both. 重试：不能.
- `invalid_recipients` (400): `to`, `cc`, `bcc` or `replyTo` holds something that is not an address, or the message has no recipient or more than 50. 处理办法：Fix the lists; send to more people in several messages or a batch. 重试：不能.
- `invalid_smtp_credential` (400): An SMTP relay credential needs a name: one line of at most 100 characters. 处理办法：Name what the credential is for (the application that will use it). 重试：不能.
- `invalid_subject` (400): `subject` is missing, empty, over 500 characters or not a single line. 处理办法：Fix it. 重试：不能.
- `invalid_tags` (400): `tags` is not an object of up to 10 names with string values. 处理办法：Fix the tags. 重试：不能.
- `key_mailbox_restricted` (403): A key restricted to one agent mailbox called something other than that mailbox's own paths. 处理办法：Use the key only for `/agent-mailboxes/{its id}` and the paths under it (not `/keys`, not DELETE); use another key for the rest. 重试：不能.
- `mail_app_limit` (409): The workspace already has 20 mail apps that are not revoked. 处理办法：Revoke one first (DELETE /mail-apps/{id}). 重试：不能.
- `mailbox_unreadable` (409): The mailbox cannot be read for inbound routing: it is not active, or has no stored password. 处理办法：Use an active mailbox with a stored password (created through the console or the API). 重试：不能.
- `nothing_to_reply_to` (422): The message has no Reply-To or From address other than the agent's own. 处理办法：Send a new message with POST /agent-mailboxes/{id}/send instead. 重试：不能.
- `smtp_credential_limit` (409): The domain already has 10 SMTP relay credentials in use. 处理办法：Revoke one you no longer need (`DELETE /domains/{id}/smtp-credentials/{credentialId}`). 重试：不能.

### 测试模式

- `sandbox_domain_zone` (422): A test key asked for a domain outside its sandbox's zone (<workspace slug>.test). 处理办法：Use a name under the zone `GET /capabilities` reports in `sandbox.domainZone`. 重试：不能.
- `test_mode_unsupported` (403): A test key called an endpoint that needs real mail (message trace, follow-up tasks, transport security). 处理办法：Use a live key for it. `GET /capabilities` lists these paths under `sandbox.unsupportedPaths`. 重试：不能.

### 邮件服务器与服务可用性

- `service_unavailable` (503): The service or its mail server configuration is unavailable. 处理办法：Try again later; read the current state before repeating a write. 重试：先核对.
- `upstream_error` (502): The mail server refused or failed the request. 处理办法：Read the current state before repeating a write; try reads again later. 重试：先核对.

## 接口列表

`v1` 的全部操作，按功能分组，并标出所需的权限范围。路径相对于 API 地址；请求和响应的结构见规范。接口说明为英文。

### Capabilities

Boundaries of the current API.

- `GET /capabilities`: Get API capabilities (`domains:read`)

### Domain onboarding

Provision a domain after verifying ownership, or look up / cancel pending requests.

- `GET /servers`: List available mail servers (`domains:read`)
- `GET /domain-onboardings`: List domain onboarding requests (`domains:read`)
- `POST /domain-onboardings`: Request verification and provisioning of a domain (`domains:write`)
- `GET /domain-onboardings/{id}`: Get a domain onboarding request (`domains:read`)
- `DELETE /domain-onboardings/{id}`: Cancel an unsubmitted domain request (`domains:write`)
- `POST /domain-onboardings/{id}/verify`: Verify TXT and provision the domain (`domains:write`)
- `POST /domain-onboardings/{id}/refresh`: Regenerate the ownership verification record (`domains:write`)

### Domains

Import existing Stalwart domains and manage local records.

- `GET /domains`: List domains (`domains:read`)
- `POST /domains`: Import an existing domain (`domains:write`)
- `GET /domains/{id}`: Get a domain (`domains:read`)
- `DELETE /domains/{id}`: Remove the local domain record (`domains:write`)
- `GET /domains/{id}/connection`: Get domain connection and DNS status (`domains:read`)
- `PUT /domains/{id}/status`: Enable or disable a domain (`domains:write`)
- `GET /domains/{id}/catch-all`: Get a domain's catch-all (`domains:read`)
- `PUT /domains/{id}/catch-all`: Set or clear a domain's catch-all (`domains:write`)
- `GET /domains/{id}/domain-aliases`: List a domain's alias domains (`domains:read`)
- `POST /domains/{id}/domain-aliases`: Request an alias domain (`domains:write`)
- `GET /domains/{id}/domain-aliases/{aliasId}`: Get an alias domain and its DNS status (`domains:read`)
- `DELETE /domains/{id}/domain-aliases/{aliasId}`: Remove an alias domain (`domains:write`)
- `POST /domains/{id}/domain-aliases/{aliasId}/verify`: Verify ownership and add the alias domain (`domains:write`)
- `POST /domains/{id}/transport-security`: Verify and enable domain transport security (`domains:write`)

### Mailboxes

Manage mailboxes; passwords and redemption-code links are never returned.

- `GET /mailboxes`: List mailboxes (`mailboxes:read`)
- `POST /mailboxes`: Create a mailbox (`mailboxes:write`)
- `GET /mailboxes/{id}`: Get a mailbox (`mailboxes:read`)
- `DELETE /mailboxes/{id}`: Delete a mailbox (`mailboxes:write`)
- `GET /mailboxes/deleted`: List deleted mailboxes (`mailboxes:read`)
- `POST /mailboxes/{id}/restore`: Restore a deleted mailbox (`mailboxes:write`)
- `PUT /mailboxes/{id}/password`: Change mailbox password (`mailboxes:write`)
- `PUT /mailboxes/{id}/status`: Change mailbox status (`mailboxes:write`)
- `PUT /mailboxes/{id}/quota`: Change mailbox quota (`mailboxes:write`)
- `POST /mailboxes/sending-limits`: Set daily sending limits for multiple mailboxes (`mailboxes:write`)
- `GET /mailboxes/{id}/sending-limit`: Get a mailbox's daily sending limit (`mailboxes:read`)
- `PUT /mailboxes/{id}/sending-limit`: Set a mailbox's daily sending limit (`mailboxes:write`)
- `PUT /mailboxes/{id}/note`: Change mailbox note (`mailboxes:write`)

### Forwarders

Forwarding rules between single regular email addresses.

- `GET /domains/{id}/forwarders`: List forwarding rules (`forwarders:read`)
- `POST /domains/{id}/forwarders`: Create a forwarding rule (`forwarders:write`)
- `DELETE /domains/{id}/forwarders/{forwarderId}`: Delete a forwarding rule (`forwarders:write`)

### Aliases

Regular aliases delivering to managed mailboxes on the same domain.

- `GET /domains/{id}/aliases`: List regular aliases (`aliases:read`)
- `POST /domains/{id}/aliases`: Create a regular alias (`aliases:write`)
- `GET /domains/{id}/aliases/{aliasId}`: Get a regular alias (`aliases:read`)
- `PATCH /domains/{id}/aliases/{aliasId}`: Update a regular alias (`aliases:write`)
- `DELETE /domains/{id}/aliases/{aliasId}`: Delete a regular alias (`aliases:write`)

### Lists

Distribution lists: an address of a domain that delivers to all of its members.

- `GET /lists`: List distribution lists (`lists:read`)
- `POST /lists`: Create a distribution list (`lists:write`)
- `GET /lists/{listId}`: Get a distribution list (`lists:read`)
- `PATCH /lists/{listId}`: Update a distribution list (`lists:write`)
- `DELETE /lists/{listId}`: Delete a distribution list (`lists:write`)

### Legal holds

Legal holds: a held mailbox cannot be deleted permanently or purged. A hold preserves the mailbox, not individual messages.

- `GET /holds`: List legal holds (`holds:read`)
- `POST /holds`: Place a legal hold (`holds:write`)
- `GET /holds/{id}`: Get a legal hold (`holds:read`)
- `POST /holds/{id}/release`: Release a legal hold (`holds:write`)

### Reports

Usage reports: inactive mailboxes, storage per domain, top senders. Computed from this app's own records.

- `GET /reports/inactive-mailboxes`: Inactive mailboxes (`mailboxes:read`)
- `GET /reports/storage`: Storage per domain (`mailboxes:read`)
- `GET /reports/top-senders`: Top senders by daily sending-limit usage (`mailboxes:read`)

### Batch

Manage domains, mailboxes and aliases in ordered batches with persisted idempotent results.

- `POST /batch`: Batch-manage domains, mailboxes and aliases (按每项操作：`mailboxes:write`, `domains:write`, `aliases:write`)
- `GET /batch/{requestId}`: Get batch execution status (仅限创建该批次的密钥)

### Agent tasks

An agent follows up email as a given mailbox: it starts from a prompt, sends fully autonomously by default, and continues when replies arrive.

- `GET /agent-tasks`: List follow-up tasks (`agent_tasks:read`)
- `POST /agent-tasks`: Create a follow-up task (`agent_tasks:write`)
- `POST /agent-tasks/batch`: Create a follow-up task for each of several mailboxes (`agent_tasks:write`)
- `GET /agent-tasks/{id}`: Get a follow-up task (`agent_tasks:read`)
- `PATCH /agent-tasks/{id}`: Change a follow-up task's limits (`agent_tasks:write`)
- `GET /agent-tasks/{id}/events`: Fetch the task timeline (`agent_tasks:read`)
- `GET /agent-tasks/{id}/threads`: List the conversations a task follows (`agent_tasks:read`)
- `POST /agent-tasks/{id}/threads`: Hand a conversation to a task (`agent_tasks:write`)
- `POST /agent-tasks/{id}/messages`: Message the agent (`agent_tasks:write`)
- `POST /agent-tasks/{id}/pause`: Pause a follow-up task (`agent_tasks:write`)
- `POST /agent-tasks/{id}/resume`: Resume a follow-up task (`agent_tasks:write`)
- `POST /agent-tasks/{id}/cancel`: Cancel a follow-up task (`agent_tasks:write`)
- `GET /agent-tasks/{id}/approvals`: List emails awaiting approval (`agent_tasks:read`)
- `POST /agent-tasks/{id}/approvals/{approvalId}`: Approve or reject an email (`agent_tasks:write`)

### Webhooks

Signed event webhooks: mailbox, domain, order, bulk job and security events POSTed to your https address, with retries and a delivery log.

- `GET /webhooks`: List webhooks (`webhooks:read`)
- `POST /webhooks`: Create a webhook (`webhooks:write`)
- `GET /webhooks/{id}`: Get a webhook (`webhooks:read`)
- `PATCH /webhooks/{id}`: Update a webhook (`webhooks:write`)
- `DELETE /webhooks/{id}`: Delete a webhook (`webhooks:write`)
- `GET /webhooks/{id}/deliveries`: List a webhook's recent delivery attempts (`webhooks:read`)
- `POST /webhooks/{id}/deliveries/{deliveryId}/redeliver`: Send a delivery's event again (`webhooks:write`)
- `POST /webhooks/{id}/test`: Send a test event (`webhooks:write`)

### Trace

Message trace: the delivery timeline of messages sent or received by the workspace's domains, read from the mail server's logs and queue. Every call is recorded in the activity log.

- `GET /trace`: Trace messages (`trace:read`)

### Emails

Send mail from the workspace's domains through their own mail server, and follow each message's delivery. Off until a platform admin approves the workspace; test keys send to the sandbox.

- `GET /emails`: List sent messages (`emails:read`)
- `POST /emails`: Send a message (`emails:write`)
- `POST /emails/batch`: Send several messages (`emails:write`)
- `GET /emails/{id}`: Get a message's delivery status (`emails:read`)
- `GET /sending`: Get the workspace's sending state (`emails:read`)
- `GET /domains/{id}/sending`: Get a domain's sending state (`emails:read`)
- `PUT /domains/{id}/sending`: Turn sending on for a domain (`emails:write`)
- `DELETE /domains/{id}/sending`: Turn sending off for a domain (`emails:write`)
- `GET /domains/{id}/smtp-credentials`: List a domain's SMTP relay credentials (`emails:read`)
- `POST /domains/{id}/smtp-credentials`: Make an SMTP relay credential (`emails:write`)
- `DELETE /domains/{id}/smtp-credentials/{credentialId}`: Revoke an SMTP relay credential (`emails:write`)

### Inbound

Inbound routing: mail that chosen mailboxes receive is POSTed to webhooks as message.received, and can be read through the API.

- `GET /inbound-routes`: List inbound routes (`inbound:read`)
- `GET /mailboxes/{id}/inbound`: Get a mailbox's inbound route (`inbound:read`)
- `PUT /mailboxes/{id}/inbound`: Turn inbound routing on for a mailbox (`inbound:write`)
- `DELETE /mailboxes/{id}/inbound`: Turn inbound routing off for a mailbox (`inbound:write`)
- `GET /mailboxes/{id}/messages/{emailId}`: Read a received message (`inbound:read`)
- `GET /mailboxes/{id}/messages/{emailId}/attachments/{index}`: Download an attachment of a received message (`inbound:read`)

### Agent mailboxes

A mailbox an AI agent reads and sends from, made in one call, and a key restricted to just that mailbox for the agent itself.

- `GET /agent-mailboxes`: List agent mailboxes (`agent_mailboxes:read`)
- `POST /agent-mailboxes`: Create an agent mailbox (`agent_mailboxes:write`)
- `GET /agent-mailboxes/{id}`: Get an agent mailbox (`agent_mailboxes:read`)
- `DELETE /agent-mailboxes/{id}`: Delete an agent mailbox (`agent_mailboxes:write`)
- `GET /agent-mailboxes/{id}/messages`: List an agent's inbox (`agent_mailboxes:read`)
- `GET /agent-mailboxes/{id}/messages/{emailId}`: Read a message an agent received (`agent_mailboxes:read`)
- `PATCH /agent-mailboxes/{id}/messages/{emailId}`: Mark a message an agent received read or flagged (`agent_mailboxes:write`)
- `DELETE /agent-mailboxes/{id}/messages/{emailId}`: Delete a message an agent received (`agent_mailboxes:write`)
- `GET /agent-mailboxes/{id}/messages/{emailId}/attachments/{index}`: Download an attachment of a received message (`agent_mailboxes:read`)
- `POST /agent-mailboxes/{id}/messages/{emailId}/reply`: Reply to a message as the agent (`agent_mailboxes:write`)
- `POST /agent-mailboxes/{id}/send`: Send a message as the agent (`agent_mailboxes:write`)
- `POST /agent-mailboxes/{id}/keys`: Create a key for one agent mailbox (`agent_mailboxes:write`)

### Mail apps

Third-party apps a workspace registers so its mailbox users can authorize them, with OAuth on WebMail, to read their mail or send as them through WebMail's Mail API.

- `GET /mail-apps`: List mail apps (`mail_apps:read`)
- `POST /mail-apps`: Register a mail app (`mail_apps:write`)
- `GET /mail-apps/{id}`: Get a mail app (`mail_apps:read`)
- `DELETE /mail-apps/{id}`: Revoke a mail app (`mail_apps:write`)

### Suppressions

The workspace's suppression list: recipients that hard-bounced repeatedly or were added by hand. Advisory: nothing is refused at submission; check an address before sending to it.

- `GET /suppressions`: List suppressed recipients (`suppressions:read`)
- `POST /suppressions`: Suppress a recipient (`suppressions:write`)
- `GET /suppressions/{address}`: Check whether a recipient is suppressed (`suppressions:read`)
- `DELETE /suppressions/{address}`: Remove a recipient from the suppression list (`suppressions:write`)

- [下载 OpenAPI 规范](https://developers.abnormalhq.com/openapi.json)
- [服务状态](https://console.abnormalhq.com/status)
