跳转到内容

平台模式(租户)

平台模式让一个 SpacePost 账户可以为你的众多客户发信。每个客户就是一个租户。当你构建的产品要为其他企业发送邮件时,请使用它,例如开票或客服类 SaaS:每个客户连接自己的域名,且某一个恶意用户不能连累其他所有人。

如果你只发送自己产品的邮件,就不需要租户。

相互隔离的信誉 每个租户都有自己独立的 Amazon SES 租户。某个客户的投诉和退信只会体现在该客户身上。
独立的信誉防护 相同的自动暂停阈值(该租户最近 1,000 封邮件中退信率超过 5% 或投诉率超过 0.2%,并有最小样本量要求)只会暂停该租户。
你的账户保持干净 租户的邮件不会计入你自己团队的信誉检查,因此某个不良客户无法导致你的账户或其他客户被暂停。
暂停开关 你可以自行暂停和恢复任何租户,并附上原因。
每日上限 可选的 daily_limit,按每个租户、每个 UTC 日的收件人数计算。
你自己的 ID 用你自己的 external_id 来指代租户,无需保存我们的 id。
归属标识 邮件、域名和 Webhook 事件都带有 tenant_id。

租户共享你的套餐:每个租户的收件人都会计入你的每月配额、团队的每日限额和你的 API 速率限制。内容过滤同样适用于租户的邮件,即使该账户本身原本可以豁免,其租户也不例外。

POST /tenants 接受 name,以及可选的 external_id(在你的账户内唯一)和 daily_limit。所有租户端点都需要 full_access API 密钥。

create-tenant.ts
import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost(process.env.SPACEPOST_API_KEY);
// One tenant per customer. `externalId` is your own customer id and works anywhere a tenant id does.
const { data: tenant, error } = await spacepost.tenants.create({
name: "Acme Inc",
externalId: "cust_123",
dailyLimit: 500, // optional cap, in recipients per UTC day
});
if (error) throw new Error(`${error.name}: ${error.message}`);
console.log(tenant.id, tenant.status); // "active"
// Optional: give the tenant its own sending domain. Mail from it is attributed to the tenant automatically.
const { data: domain } = await spacepost.domains.create({ name: "mail.acme-inc.com", tenantId: "cust_123" });
console.log(domain?.tenant_id === tenant.id); // true
Response
{
"object": "tenant",
"id": "0192e1c4-5555-7000-8000-0000000000cc",
"name": "Acme Inc",
"external_id": "cust_123",
"daily_limit": 500,
"status": "active",
"paused_reason": null,
"created_at": "2026-10-23T14:02:09.512Z"
}

在所有指代租户的地方(/tenants/:ref、邮件和域名上的 tenant_id、spacepost tenants get <ref>),你既可以传我们的 id,也可以传你的 external_id。只需保存一次你自己的客户 id,就再也不用维护映射表。Webhook 始终携带我们的租户 id;如果你需要 external_id,请用 GET /tenants/{id} 查询。

租户自有域名 团队级域名
创建方式 在 POST /domains 中带 tenant_id 不带 tenant_id 的 POST /domains
典型所有者 客户自己的域名(mail.acme-inc.com) 你的域名(yourapp.com)
归属 自动:从它发出的每封邮件都归属于该租户 你在每封邮件上传入 tenant_id,否则邮件视为你自己的
谁可以使用它发信 仅该租户 你自己的邮件,以及任何租户

如果邮件指定的 tenant_id 与其域名所属的租户不同,请求会以 422 validation_error 失败。当 from 域名已属于某个租户时,tenant_id 可以省略。

添加租户自有域名的方式与添加其他域名完全相同,只多一个字段,然后在客户的 DNS 服务商处添加 DNS 记录。参见添加与验证。

终端窗口
curl -X POST https://api.spacepost.dev/domains \
-H "Authorization: Bearer $SPACEPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "mail.acme-inc.com", "tenant_id": "cust_123" }'
send-for-tenant.ts
import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost(process.env.SPACEPOST_API_KEY);
// From a team-level domain (yours), name the tenant on each email.
const { data, error } = await spacepost.emails.send({
from: "Acme Inc via YourApp <invoices@yourapp.com>",
to: "buyer@example.com",
subject: "Invoice #1042",
html: "<p>Your invoice is attached.</p>",
tenantId: "cust_123", // our tenant id or your external_id
});
if (error?.name === "sending_paused") {
console.warn("This customer is paused:", error.message);
} else if (error?.name === "daily_quota_exceeded") {
console.warn("This customer hit its daily cap; retry tomorrow.");
} else if (error) {
throw new Error(`${error.name}: ${error.message}`);
} else {
console.log("Queued", data.id);
}
// batch.send accepts tenantId per email.
await spacepost.batch.send([
{ from: "invoices@yourapp.com", to: "a@example.com", subject: "Hello A", text: "Hi A", tenantId: "cust_123" },
{ from: "invoices@yourapp.com", to: "b@example.com", subject: "Hello B", text: "Hi B", tenantId: "cust_456" },
]);
manage-tenants.ts
import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost(process.env.SPACEPOST_API_KEY);
const { data: page } = await spacepost.tenants.list({ limit: 20 });
for (const t of page?.data ?? []) console.log(t.name, t.external_id, t.status, t.paused_reason);
await spacepost.tenants.get("cust_123"); // by external_id or our id
await spacepost.tenants.update({ id: "cust_123", dailyLimit: 1000 }); // dailyLimit: null removes the cap
await spacepost.tenants.pause({ id: "cust_123", reason: "Investigating spam reports" });
const { error } = await spacepost.tenants.unpause("cust_123");
if (error?.statusCode === 403) console.warn("Paused by the reputation guard; contact support to review it.");
// Domains first, then the tenant (422 while it still has domains).
const { data: domains } = await spacepost.domains.list({ limit: 100 });
for (const d of domains?.data ?? []) if (d.tenant_id) console.log(d.name, "belongs to", d.tenant_id);
await spacepost.tenants.remove("cust_123");
情形 结果
为已暂停的租户发送 403 sending_paused。已入队的邮件会以 email.failed 事件失败。
租户超出其 daily_limit 429 daily_quota_exceeded(每天在 UTC 00:00 重置)。该请求中的任何邮件都不会发送。
tenant_id 与域名所属租户冲突 422 validation_error
未知的 tenant_id 404 not_found
恢复被信誉防护暂停的租户 403 invalid_access,除非你的团队是 trusted 或 internal。请在修复原因后联系 support@spacepost.dev。你自己执行的暂停随时都可以解除。
删除仍有域名的租户 422 validation_error。请先删除它的域名。

你的常规 Webhook 同样会收到租户邮件的事件,其中 data.tenant_id 为我们的租户 id。没有租户的邮件则没有 tenant_id 键。

{
"type": "email.bounced",
"created_at": "2026-10-23T14:02:12.000Z",
"data": {
"email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10",
"from": "Acme Inc via YourApp <invoices@yourapp.com>",
"to": ["nobody@example.com"],
"subject": "Invoice #1042",
"created_at": "2026-10-23T14:02:09.512Z",
"tenant_id": "0192e1c4-5555-7000-8000-0000000000cc",
"bounce": { "type": "Permanent", "subType": "General", "message": "smtp; 550 5.1.1 user unknown" }
}
}
webhook-tenant.ts
import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost(process.env.SPACEPOST_API_KEY);
export async function handle(request: Request): Promise<Response> {
const payload = await request.text();
try {
const event = await spacepost.webhooks.verify({
payload,
headers: request.headers,
secret: process.env.SPACEPOST_WEBHOOK_SECRET!,
});
// `tenant_id` is our tenant id (not your external_id). It is absent on the team's own mail.
const tenantId = event.data.tenant_id;
if (event.type === "email.bounced" && tenantId) {
console.log(`Bounce for tenant ${tenantId}: ${event.data.bounce?.subType}`);
}
return new Response("ok");
} catch {
return new Response("invalid signature", { status: 400 });
}
}

GET /emails/{id} 和 GET /domains/{id} 也会返回 tenant_id(或 null),方便你在自己的数据库中对账。

  1. 客户注册时创建一个租户,以你的客户 id 作为 external_id。
  2. 为每个客户做出选择:使用其自己的域名(POST /domains 上带 tenant_id),或使用你的共享域名(每次发送都带 tenant_id)。
  3. 为新客户设置保守的 daily_limit,待其证明自己可靠后再逐步提高。
  4. 订阅 email.bounced、email.complained 和 email.failed,并在你的管理界面中展示与 tenant_id 相关的问题。
  5. 一旦发现滥用,立即自行暂停该客户。暂停即时生效,并且可以撤销。