プラットフォームモード(テナント)
プラットフォームモードを使うと、1 つの SpacePost アカウントで、お客様の多数に代わって送信できます。各お客様がテナントです。他の事業者のためにメールを送信する製品を作る場合に使います。たとえば、すべてのお客様が自分のドメインを接続する請求書管理やサポートの SaaS で、1 社の不正が全体に影響してはならない場合です。
ご自身の製品のメールだけを送信するなら、テナントは必要ありません。
テナントで得られるもの
Section titled “テナントで得られるもの”| 分離された評判 | すべてのテナントが、専用の Amazon SES テナントを持ちます。あるお客様の苦情やバウンスは、そのお客様にだけ表れます。 |
| 独自の評判ガード | 同じ自動一時停止のしきい値(テナントの直近 1,000 通のうち、バウンスが 5% 超、または苦情が 0.2% 超。最小サンプル数あり)により、そのテナントだけが一時停止されます。 |
| アカウントは清潔なまま | テナントのメールは、ご自身のチームの評判チェックには含まれません。そのため、1 社の問題のあるお客様が、アカウントや他のお客様を一時停止させることはありません。 |
| 一時停止スイッチ | 任意のテナントを、理由を添えてご自身で一時停止・再開できます。 |
| 1 日の上限 | UTC の 1 日あたりの宛先数で、テナントごとに設定できる任意の daily_limit。 |
| ご自身の ID | SpacePost の ID を保存する代わりに、ご自身の external_id でテナントを指定できます。 |
| 帰属 | メール、ドメイン、Webhook イベントに tenant_id が付きます。 |
テナントはご自身のプランを共有します。テナントの宛先はすべて、ご自身の月間クォータ、チームの 1 日の上限、API のレート制限に加算されます。コンテンツのフィルタリングは、テナントのメールにも適用されます。他の点では免除されているアカウントのテナントも同様です。
テナントを作成する
Section titled “テナントを作成する”POST /tenants は name を受け取り、任意で external_id(アカウント内で一意)と daily_limit を指定できます。テナントのエンドポイントはすべて full_access の API キーが必要です。
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); // truecurl -X POST https://api.spacepost.dev/tenants \ -H "Authorization: Bearer $SPACEPOST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Inc", "external_id": "cust_123", "daily_limit": 500 }'spacepost tenants create --name "Acme Inc" --external-id cust_123 --daily-limit 500{ "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"}external_id
Section titled “external_id”テナントを指定するあらゆる箇所(/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" }'spacepost domains add mail.acme-inc.com --tenant cust_123テナントのために送信する
Section titled “テナントのために送信する”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" },]);curl -X POST https://api.spacepost.dev/emails \ -H "Authorization: Bearer $SPACEPOST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "Acme Inc via YourApp <invoices@yourapp.com>", "to": ["buyer@example.com"], "subject": "Invoice #1042", "html": "<p>Your invoice is attached.</p>", "tenant_id": "cust_123" }'POST /emails/batch は、各項目で tenant_id を受け付けます。
公式の resend パッケージは、SpacePost に対してそのまま動作します。RESEND_BASE_URL=https://api.spacepost.dev か new Resend(key, { baseUrl }) で SpacePost を指定してください。
最も簡単な方法: テナント所有のドメインから送信する。 from のドメインからテナントが推定されるため、既存の resend.emails.send の呼び出しは一切変更する必要がありません。
チームレベルのドメインからは、resend.post を使います。 Resend SDK は、固定されたフィールドの一覧からリクエストボディを組み立てます。そのため、resend.emails.send、resend.batch.send、resend.domains.create に追加で渡した tenant_id は、SpacePost に届く前に黙って破棄されます。resend.post は渡したボディをそのまま送信するので、snake_case のフィールドで使ってください。
import { Resend } from "resend";
const resend = new Resend(process.env.SPACEPOST_API_KEY, { baseUrl: "https://api.spacepost.dev" });
const { data, error } = await resend.post<{ id: string }>("/emails", { from: "Acme Inc via YourApp <invoices@yourapp.com>", to: ["buyer@example.com"], subject: "Invoice #1042", html: "<p>Your invoice is attached.</p>", tenant_id: "cust_123",});resend.emails.send と違い、resend.post はキャメルケースを変換しません。reply_to と scheduled_at と書いてください。resend.post("/domains", { name, tenant_id }) や resend.post("/emails/batch", [...]) でも同様に使えます。
spacepost send --from invoices@yourapp.com --to buyer@example.com \ --subject "Invoice #1042" --text "Your invoice is attached." --tenant cust_123一時停止、再開、上限
Section titled “一時停止、再開、上限”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 idawait 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");# Pause (the body is optional)curl -X POST https://api.spacepost.dev/tenants/cust_123/pause \ -H "Authorization: Bearer $SPACEPOST_API_KEY" -H "Content-Type: application/json" \ -d '{ "reason": "Investigating spam reports" }'
# Resumecurl -X POST https://api.spacepost.dev/tenants/cust_123/unpause -H "Authorization: Bearer $SPACEPOST_API_KEY"
# Change the cap (null removes it)curl -X PATCH https://api.spacepost.dev/tenants/cust_123 \ -H "Authorization: Bearer $SPACEPOST_API_KEY" -H "Content-Type: application/json" \ -d '{ "daily_limit": 1000 }'
# Delete (422 while the tenant still has domains)curl -X DELETE https://api.spacepost.dev/tenants/cust_123 -H "Authorization: Bearer $SPACEPOST_API_KEY"spacepost tenants listspacepost tenants get cust_123spacepost tenants pause cust_123 --reason "Investigating spam reports"spacepost tenants unpause cust_123spacepost tenants rm 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
Section titled “Webhook”通常の 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" } }}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)を返すので、ご自身のデータベースと照合できます。
SaaS 連携のチェックリスト
Section titled “SaaS 連携のチェックリスト”- お客様がサインアップしたときにテナントを作成し、お客様 ID を
external_idに設定します。 - お客様ごとに決めます。お客様自身のドメイン(
POST /domainsのtenant_id)か、ご自身の共有ドメイン(送信ごとのtenant_id)かです。 - 新しいお客様には控えめな
daily_limitを設定し、実績が積み上がるにつれて引き上げます。 email.bounced、email.complained、email.failedを購読し、tenant_idに関する問題を管理画面に表示します。- 不正を見つけたら、すぐにご自身でお客様を一時停止します。即座に反映され、元に戻せます。