コンテンツにスキップ

プラットフォームモード(テナント)

プラットフォームモードを使うと、1 つの SpacePost アカウントで、お客様の多数に代わって送信できます。各お客様がテナントです。他の事業者のためにメールを送信する製品を作る場合に使います。たとえば、すべてのお客様が自分のドメインを接続する請求書管理やサポートの SaaS で、1 社の不正が全体に影響してはならない場合です。

ご自身の製品のメールだけを送信するなら、テナントは必要ありません。

分離された評判 すべてのテナントが、専用の Amazon SES テナントを持ちます。あるお客様の苦情やバウンスは、そのお客様にだけ表れます。
独自の評判ガード 同じ自動一時停止のしきい値(テナントの直近 1,000 通のうち、バウンスが 5% 超、または苦情が 0.2% 超。最小サンプル数あり)により、そのテナントだけが一時停止されます。
アカウントは清潔なまま テナントのメールは、ご自身のチームの評判チェックには含まれません。そのため、1 社の問題のあるお客様が、アカウントや他のお客様を一時停止させることはありません。
一時停止スイッチ 任意のテナントを、理由を添えてご自身で一時停止・再開できます。
1 日の上限 UTC の 1 日あたりの宛先数で、テナントごとに設定できる任意の daily_limit。
ご自身の ID SpacePost の ID を保存する代わりに、ご自身の external_id でテナントを指定できます。
帰属 メール、ドメイン、Webhook イベントに tenant_id が付きます。

テナントはご自身のプランを共有します。テナントの宛先はすべて、ご自身の月間クォータ、チームの 1 日の上限、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>)で、SpacePost の id かご自身の external_id のどちらでも渡せます。ご自身のお客様 ID を一度保存しておけば、対応表を管理する必要はありません。Webhook には常に SpacePost のテナント ID が含まれます。external_id が必要な場合は、GET /tenants/{id} で調べてください。

ドメイン: テナント所有か、チームレベルか

Section titled “ドメイン: テナント所有か、チームレベルか”
テナント所有のドメイン チームレベルのドメイン
作成方法 POST /domains に tenant_id を指定 tenant_id なしの POST /domains
典型的な所有者 お客様自身のドメイン(mail.acme-inc.com) ご自身のドメイン(yourapp.com)
帰属 自動: そこから送信されるすべてのメールがテナントに属します メールごとに tenant_id を渡す、またはご自身のメールになります
送信できる人 そのテナントのみ ご自身のメールと、すべてのテナント

メールが指定した tenant_id がドメインのテナントと異なる場合、リクエストは 422 validation_error で失敗します。from のドメインがすでにテナントに属している場合、tenant_id は省略できます。

テナント所有のドメインは、通常のドメインとまったく同じように、フィールドを 1 つ追加して追加し、その後お客様の 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(1 日は 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 に SpacePost のテナント 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. 不正を見つけたら、すぐにご自身でお客様を一時停止します。即座に反映され、元に戻せます。