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 | 状态 |
|---|---|---|
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,客户端生成一个并在多次尝试中重用它,因此重试的发送不能传递两次。在进程重启时传递自己的密钥给重复数据删除:
await anypost.email.send(message, { idempotencyKey: orderId });
配置
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)覆盖单个请求的客户端默认值。
许可证
麻省理工学院