anypost

中文文档
1.5.0发布于 2026/11/03MITTypeScriptESM + CJS零依赖构建来源已验证

Anypost邮件API的官方TypeScript SDK。

中文使用文档
文档目录17 节
  1. 安装
  2. 快速入门
  3. 发送
  4. 减价
  5. 批处理
  6. 域
  7. API密钥
  8. 模板
  9. 一种
  10. 人则
  11. 确认交付
  12. 事件
  13. 分页
  14. 错误
  15. 重试和幂等
  16. 配置
  17. 许可证

Anypost Node SDK

官方的TypeScript和JavaScript客户端 Anypost 电子邮件API。

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

这个自述文件涵盖了SDK本身:安装、习惯用法和配置。有关平台概念和完整的字段级API参考,请参阅 Anypost文档.

安装

npm install anypost

快速入门

import { Anypost } from "anypost";

const anypost = new Anypost("ap_your_api_key");

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

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

const anypost = new Anypost();

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

发送

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

await anypost.email.send({
  from: "YourCo <[email protected]>",
  to: ["[email protected]", "[email protected]"],
  cc: ["[email protected]"],
  reply_to: "[email protected]",
  subject: "Receipt #4823",
  html: "<p>Thanks for your order.</p>",
  text: "Thanks for your order.",
  tags: ["receipt"],
});

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

import { readFileSync } from "node:fs";

await anypost.email.send({
  from: "YourCo <[email protected]>",
  to: ["[email protected]"],
  subject: "Your report",
  text: "Attached.",
  attachments: [
    { filename: "report.pdf", content: readFileSync("report.pdf") },
  ],
});

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

await anypost.email.send({
  from: "YourCo <[email protected]>",
  to: ["[email protected]"],
  template_id: "template_018f2c5e-3a40-7a91-9c25-3a0b1d5e6f78",
  variables: { name: "Ada", plan: "pro" },
});

看到 发送参考 以获取完整的字段列表。

减价

在Markdown中编写正文,SDK将其呈现为电子邮件安全的HTML和纯文本替代方案 emailmd。通过 markdown 代替 html/text:

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

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

npm install emailmd

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

传递渲染选项(主题、字体、验证) markdownOptions,并直接使用 renderMarkdown 渲染一次并重用结果,或者读取提取的前页内容:

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 填充条目遗漏的任何字段。

const result = await anypost.email.sendBatch({
  defaults: { from: "YourCo <[email protected]>" },
  emails: [
    { to: ["[email protected]"], subject: "Hi A", text: "..." },
    { to: ["[email protected]"], subject: "Hi B", text: "..." },
  ],
});

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

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记录,然后验证。

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 把资源填满。看到 域 用于验证生命周期和字段参考。

API密钥

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

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密钥 用于权限模型和缓存传播。

模板

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

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 (见 发送). 看到 模板 查看完整模型。

一种

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

await anypost.suppressions.create({
  email: "[email protected]",
  topic: "marketing",
  note: "Customer requested removal",
});

await anypost.suppressions.delete("[email protected]", "marketing");

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

const result = await anypost.suppressions.createBatch([
  { email: "[email protected]", reason: "hard bounce" },
  { email: "[email protected]", topic: "marketing", reason: "unsubscribed" },
]);
// { imported: 2, already_suppressed: 0, invalid: [] }

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

get, list (与 email_contains, topic, reason, origin 过滤器), listForEmail, deleteForEmail 把资源填满。看到 一种 为范围和自动抑制规则的反弹和投诉。

人则

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

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 把资源填满。看到 人则 对于事件目录、状态转换和秘密旋转宽限期窗口。

确认交付

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

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,然后使用你的框架的解析对象,一旦它通过:

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.

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端点被禁用后如何填补空白的方法:在中断期间,一旦它恢复正常,就会记录发生的事件。看到 事件 供现场参考。

分页

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

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状态。

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状态
ValidationErrorvalidation_error400, 422
AuthenticationErrorauthentication_error401
PermissionErrorpermission_error403
NotFoundErrornot_found404
ConflictErrorconflict, idempotency_concurrent, webhook_rotation_in_progress409
IdempotencyMismatchErroridempotency_mismatch422
RateLimitErrorrate_limit_exceeded429
PayloadTooLargeErrorpayload_too_large413
APIErrorinternal_error, provisioning_error5xx
APIConnectionErrorconnection_error无

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

重试和幂等

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

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

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

配置

new Anypost("ap_your_api_key", options);
new Anypost({ apiKey: "ap_your_api_key", ...options });
选项默认的描述
apiKeyANYPOST_API_KEY无记名证书(ap_...)。
baseUrlhttps://api.anypost.com/v1API基础URL。
timeoutMs30000每个请求超时。
maxRetries2自动重试瞬态故障。
fetch全球 fetch自定义获取实现。
headers{}在每个请求上发送额外的报头。

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

许可证

麻省理工学院

版本与发布记录

anypost 共发布 7 个版本,首次发布于 2026/06/09;过去 12 个月发布了 7 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.5.0latest2026/11/03今天
1.4.02026/08/042 个月前
1.3.02026/08/042 个月前
1.2.02026/07/313 个月前
1.1.12026/06/254 个月前
1.1.02026/06/184 个月前

关于 anypost 的常见问题

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 和提交记录判断。