# anypost 中文文档

规范地址：https://npmcn.com/package/anypost/

版本：1\.5\.0
npm 元数据更新时间：2026-11-03T01:15:06.003Z
中文译文更新时间：2026-11-03T03:26:41.167Z
软件包许可：MIT

本文由上游 README 机器翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/anypost
原文对照：https://npmcn.com/package/anypost/?lang=original
项目主页：https://anypost.com/
源码仓库：https://github.com/anypost/anypost-node

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：373 kB，9 个文件
- 模块格式：ESM + CJS
- TypeScript 类型：包内自带
- Node.js 要求：\>=18
- 运行依赖：0 个
- peer 依赖：emailmd \>=0\.3\.0
- 首次发布：2026-06-09T05:09:15.801Z
- 最新版本发布：2026-11-03T01:15:05.656Z
- 已发布版本数：7（近 12 个月 7 个）

## 中文简介

Anypost邮件API的官方TypeScript SDK。

## 安装

```sh
npm install anypost
```

也可以使用：`pnpm add anypost`、`yarn add anypost`、`bun add anypost`

## README

# Anypost Node SDK

官方的TypeScript和JavaScript客户端 [Anypost](https://anypost.com/) 电子邮件API。

运行在Node 18+、Bun、Deno和edge运行时。零依赖关系。

这个自述文件涵盖了SDK本身：安装、习惯用法和配置。有关平台概念和完整的字段级API参考，请参阅 [Anypost文档](https://anypost.com/docs).

## 安装

```bash
npm install anypost
```

## 快速入门

```ts
import { Anypost } from "anypost";

const anypost = new Anypost("ap_your_api_key");

const { id } = await anypost.email.send({
  from: "YourCo <you@yourdomain.com>",
  to: ["you@example.com"],
  subject: "Welcome to Anypost",
  html: "<p>Hello, inbox!</p>",
});
```

构造函数还读取 `ANYPOST_API_KEY` 来自环境：

```ts
const anypost = new Anypost();
```

将密钥保存在服务器端。它是无记名凭证；永远不要将其发布到浏览器或移动应用程序中。

## 发送

其中一个 `text`， `html`，或者 `template_id` 是必需的。所有在 `to`， `cc`, `bcc` 共用一个信封，并以合计50个为限计算。

```ts
await anypost.email.send({
  from: "YourCo <you@yourdomain.com>",
  to: ["a@example.com", "b@example.com"],
  cc: ["team@example.com"],
  reply_to: "support@yourdomain.com",
  subject: "Receipt #4823",
  html: "<p>Thanks for your order.</p>",
  text: "Thanks for your order.",
  tags: ["receipt"],
});
```

通过附件 `content` 作为原始字节，客户端用base64编码；传递一个已经编码的字符串，然后按原样发送。请求体的上限为5 MB。

```ts
import { readFileSync } from "node:fs";

await anypost.email.send({
  from: "YourCo <you@yourdomain.com>",
  to: ["someone@example.com"],
  subject: "Your report",
  text: "Attached.",
  attachments: [
    { filename: "report.pdf", content: readFileSync("report.pdf") },
  ],
});
```

发送一个已发布的模板和每个收件人变量：

```ts
await anypost.email.send({
  from: "YourCo <you@yourdomain.com>",
  to: ["someone@example.com"],
  template_id: "template_018f2c5e-3a40-7a91-9c25-3a0b1d5e6f78",
  variables: { name: "Ada", plan: "pro" },
});
```

看到 [发送参考](https://anypost.com/docs/reference/emails) 以获取完整的字段列表。

## 减价

在Markdown中编写正文，SDK将其呈现为电子邮件安全的HTML和纯文本替代方案 [`emailmd`](https://www.emailmd.dev/)。通过 `markdown` 代替 `html`/`text`：

```ts
await anypost.email.send({
  from: "YourCo <you@yourdomain.com>",
  to: ["someone@example.com"],
  subject: "Welcome",
  markdown: "# Welcome\n\nThanks for signing up. [Get started](https://app.example.com).",
});
```

`emailmd` 是可选的对等依赖项。只在发送Markdown时安装它：

```bash
npm install emailmd
```

它需要Node 20+并且不运行在边缘或浏览器运行时，因此SDK的其余部分保持无依赖和可移植。调用Markdown路径 `emailmd` Installed抛出一个明确的错误。

传递渲染选项（主题、字体、验证） `markdownOptions`，并直接使用 `renderMarkdown` 渲染一次并重用结果，或者读取提取的前页内容：

```ts
import { renderMarkdown } from "anypost";

const { html, text, meta } = await renderMarkdown("# Hi", {
  theme: { brandColor: "#4f46e5" },
});
// meta.preheader, etc.
```

`markdown` 不能与…结合 `html` 或者 `text`． 在一个批次中，设置 `markdownOptions` 在 `defaults` 将一个主题应用于每个条目的渲染。

## 批处理

在一个请求中发送1 ~ 100条独立消息。 `defaults` 填充条目遗漏的任何字段。

```ts
const result = await anypost.email.sendBatch({
  defaults: { from: "YourCo <you@yourdomain.com>" },
  emails: [
    { to: ["a@example.com"], subject: "Hi A", text: "..." },
    { to: ["b@example.com"], subject: "Hi B", text: "..." },
  ],
});
```

具有混合结果的批处理返回HTTP `207` 正常分解。检查每个条目，而不是依赖于抛出的错误：

```ts
console.log(result.summary); // { total, queued, failed }

for (const entry of result.data) {
  if (entry.status === "queued") {
    console.log(entry.index, entry.id);
  } else if (entry.error.type === "quota_exceeded") {
    // A label cap denied this entry. Narrowing on `type` reaches the denial
    // detail, so there is no need to parse the message.
    retryAfter(entry.index, entry.error.scope, entry.error.retry_after_seconds);
  } else {
    console.error(entry.index, entry.error.type, entry.error.message);
  }
}
```

## 域

管理以下的发送域 `anypost.domains`． 添加一个域，发布它返回的DNS记录，然后验证。

```ts
const domain = await anypost.domains.create({ name: "example.com" });

for (const record of domain.dns_records) {
  console.log(record.type, record.name, "->", record.value);
}

const checked = await anypost.domains.verify(domain.id);
if (checked.status !== "verified") {
  // verify resolves with the current domain even while pending; it never throws
  console.log(checked.verification_failure?.code);
}
```

`get`， `update` （仅跟踪配置），以及 `delete` 把资源填满。看到 [域](https://anypost.com/docs/reference/domains) 用于验证生命周期和字段参考。

## API密钥

管理下面的密钥 `anypost.apiKeys`． 明文密码只返回一次，即on `create`,因为 `key`，那么就把它存起来吧。

```ts
const created = await anypost.apiKeys.create({
  name: "Production server",
  permissions: "send_only",
  allowed_domains: ["example.com"],
});
console.log(created.key); // never retrievable again
```

`get` 只返回元数据(`key_prefix`，从来不是秘密)； `update` 和 `delete` 把资源填满。看到 [API密钥](https://anypost.com/docs/reference/api-keys) 用于权限模型和缓存传播。

## 模板

模板使用草稿/已发布的模型：编辑在草稿中进行，并且 `publish` 促进它。模板在发布之前不能用于发送。

```ts
const template = await anypost.templates.create({
  name: "Welcome email",
  kind: "html",
  html: "<h1>Welcome, {{ name }}</h1>",
});

await anypost.templates.publish(template.id);
```

`kind` （`html` 或者 `markdown`)一旦设置就不可变；纯文本主体总是在服务器端派生。 `getDraft`， `updateDraft`， `deleteDraft`， `duplicate`， `get`， `update` （仅提供姓名），以及 `delete` 把资源填满。发送已发布的模板 `template_id` (见 [发送](https://npmcn.com/package/anypost/#readme-sending)）. 看到 [模板](https://anypost.com/docs/reference/templates) 查看完整模型。

## 一种

一个抑制块发送到一个地址，范围为A `topic`． 通配符 `*` 屏蔽每个话题；已命名的主题(例如： `marketing`)使事务性流量保持不变。

```ts
await anypost.suppressions.create({
  email: "alice@example.com",
  topic: "marketing",
  note: "Customer requested removal",
});

await anypost.suppressions.delete("alice@example.com", "marketing");
```

每次加起来是1000 `createBatch`，例如当从另一个提供商移动列表时：

```ts
const result = await anypost.suppressions.createBatch([
  { email: "alice@example.com", reason: "hard bounce" },
  { email: "bob@example.com", topic: "marketing", reason: "unsubscribed" },
]);
// { imported: 2, already_suppressed: 0, invalid: [] }
```

已经为该主题抑制的地址被计算在内 `already_suppressed` 并且保持不变，所以可以安全地重试一批。无效地址返回 `invalid` 与他们的位置，而不是失败的批次。

`get`， `list` (与 `email_contains`， `topic`， `reason`, `origin` 过滤器), `listForEmail`, `deleteForEmail` 把资源填满。看到 [一种](https://anypost.com/docs/reference/suppressions) 为范围和自动抑制规则的反弹和投诉。

## 人则

管理webhook订阅 `anypost.webhooks`。这个 `signing_secret` 只回来一次，一次 `create`;后期读取只返回 `signing_secret_prefix`.

```ts
const webhook = await anypost.webhooks.create({
  name: "Production events",
  url: "https://hooks.example.com/anypost",
  events: ["email.delivered", "email.bounced", "email.complained"],
});
console.log(webhook.signing_secret); // store now; never retrievable again
```

`update`， `test`， `rotateSecret`， `get`， `list`, `delete` 把资源填满。看到 [人则](https://anypost.com/docs/reference/webhooks) 对于事件目录、状态转换和秘密旋转宽限期窗口。

### 确认交付

`verifyWebhookSignature` 是一个独立函数。它需要签名秘密，而不是API密钥，所以在处理程序中调用它，而不需要客户机。通过 **生** 请求体（JSON解析前的确切字节数）、 `Anypost-Signature` 头，和秘密。它决心成功并投掷 `WebhookVerificationError` 否则。 `unwrapWebhookEvent` 执行相同的操作并返回解析过的内容 `WebhookDelivery`.

```ts
import { unwrapWebhookEvent, WebhookVerificationError } from "anypost";

try {
  const delivery = await unwrapWebhookEvent(rawBody, signatureHeader, secret);
  for (const event of delivery.events) {
    console.log(event.type, event.data.email_id);
  }
} catch (err) {
  if (err instanceof WebhookVerificationError) {
    // err.reason: "no_match" | "timestamp_out_of_tolerance" | ...
    return res.status(400).end();
  }
  throw err;
}
```

伸手去拿 `verifyWebhookSignature` 当别的东西已经解析了身体。保留验证步骤的原始字节（捕获） `req.rawBody`，然后使用你的框架的解析对象，一旦它通过：

```ts
import { verifyWebhookSignature, WebhookVerificationError } from "anypost";

app.post("/anypost", async (req, res) => {
  try {
    await verifyWebhookSignature(req.rawBody, req.header("Anypost-Signature"), secret);
  } catch (err) {
    if (err instanceof WebhookVerificationError) return res.status(400).end();
    throw err;
  }
  for (const event of req.body.events) handle(event); // req.body is already parsed
  res.status(204).end();
});
```

超过五分钟的交付将默认拒绝绑定重放；通过 `toleranceSeconds` 扩大、缩小或使那个检查失效在秘密旋转期间，头部携带 `v1=` 组件每个活动秘密，并且任何一个匹配通过，因此在您重新部署时，交付将继续验证。

## 事件

`anypost.events.list` 页面团队的事件流，最新优先。该窗口默认为过去24小时，并锁定为您的计划保留。事件是只读的，不能通过id寻址，所以没有 `get`.

```ts
for await (const event of await anypost.events.list({ event_type: "email.bounced" })) {
  console.log(event.occurred_at, event.recipient, event.bounce_classification);
}
```

通过过滤 `start`， `end`， `event_type`， `recipient`， `email_id`， `message_id`， `domain`， `topic`， `campaign`， `template_id`， `ip_pool`, `tags`,一个 `string[]` 这符合事件携带 _任何_ 给定标签的。其他每个过滤器都是精确匹配的。这也是在webhook端点被禁用后如何填补空白的方法：在中断期间，一旦它恢复正常，就会记录发生的事件。看到 [事件](https://anypost.com/docs/reference/events) 供现场参考。

## 分页

列表端点返回a `Page`． 直接读一页，或者迭代它 `for await` 走完每一页；客户端根据需要获取每一个。

```ts
const page = await anypost.domains.list({ limit: 50 });
page.data;        // this page's items
page.has_more;    // whether another page exists
page.next_cursor; // pass as `after` to fetch it yourself

for await (const domain of await anypost.domains.list()) {
  console.log(domain.name); // every domain, across all pages
}
```

## 错误

请求失败将抛出 `AnypostError` 子类。分支 `error.type`，稳定的机器可读代码，不上HTTP状态。

```ts
import { AnypostError, ValidationError, RateLimitError } from "anypost";

try {
  await anypost.email.send({ from, to, subject, html });
} catch (err) {
  if (err instanceof ValidationError) {
    console.error(err.errors); // { from: ["The from field is required."] }
  } else if (err instanceof RateLimitError) {
    console.error(err.retryAfterMs);
  } else if (err instanceof AnypostError) {
    console.error(err.type, err.status, err.message);
  }
}
```

| 类 | `type` | 状态 |
| --- | --- | --- |
| `ValidationError` | `validation_error` | `400`， `422` |
| `AuthenticationError` | `authentication_error` | `401` |
| `PermissionError` | `permission_error` | `403` |
| `NotFoundError` | `not_found` | `404` |
| `ConflictError` | `conflict`， `idempotency_concurrent`， `webhook_rotation_in_progress` | `409` |
| `IdempotencyMismatchError` | `idempotency_mismatch` | `422` |
| `RateLimitError` | `rate_limit_exceeded` | `429` |
| `PayloadTooLargeError` | `payload_too_large` | `413` |
| `APIError` | `internal_error`， `provisioning_error` | `5xx` |
| `APIConnectionError` | `connection_error` | 无 |

每个错误都携带 `type`， `status`， `message`和解析 `raw` 的身体。

## 重试和幂等

客户端重试 `429`， `502`， `503`，网络故障达 `maxRetries` 时间（默认为2），具有指数回退和全抖动。它的荣誉 `Retry-After`.

发送是安全的，可以自动重试：当重试启用时，您没有通过 `idempotencyKey`，客户端生成一个并在多次尝试中重用它，因此重试的发送不能传递两次。在进程重启时传递自己的密钥给重复数据删除：

```ts
await anypost.email.send(message, { idempotencyKey: orderId });
```

## 配置

```ts
new Anypost("ap_your_api_key", options);
new Anypost({ apiKey: "ap_your_api_key", ...options });
```

| 选项 | 默认的 | 描述 |
| --- | --- | --- |
| `apiKey` | `ANYPOST_API_KEY` | 无记名证书(`ap_...`）。 |
| `baseUrl` | `https://api.anypost.com/v1` | API基础URL。 |
| `timeoutMs` | `30000` | 每个请求超时。 |
| `maxRetries` | `2` | 自动重试瞬态故障。 |
| `fetch` | 全球 `fetch` | 自定义获取实现。 |
| `headers` | `{}` | 在每个请求上发送额外的报头。 |

每次认购期权(`idempotencyKey`， `headers`， `maxRetries`， `timeoutMs`， `signal`)覆盖单个请求的客户端默认值。

## 许可证

麻省理工学院

## 常见问题

### anypost 是什么？

Anypost邮件API的官方TypeScript SDK。

### 如何安装 anypost？

在项目目录运行 npm install anypost，或使用 pnpm add / yarn add / bun add。

### anypost 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### anypost 支持 ESM 还是 CommonJS？

同时提供 ES Module 与 CommonJS 入口，import 和 require\(\) 都可以使用，打包工具会自动选择合适的版本。

### anypost 需要什么版本的 Node\.js？

package\.json 的 engines 字段要求 Node\.js \>=18。版本不符时 npm 默认只给出警告，开启 engine-strict 后会拒绝安装。

### anypost 可以商用吗？

anypost 使用 MIT 许可。宽松许可：允许商业使用、修改和再分发，通常只需在分发时保留原版权与许可声明。以上为概括，不构成法律意见，请以许可原文为准。

### anypost 还在维护吗？

最新版本 1\.5\.0 发布于 2026/11/03（今天）；至今共发布 7 个版本，其中过去 12 个月发布了 7 个；首次发布于 2026/06/09。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

