Zum Inhalt springen

Plattform-Modus (Tenants)

Der Plattform-Modus lässt ein SpacePost-Konto für viele deiner Kunden senden. Jeder Kunde ist ein Tenant. Nutze ihn, wenn du ein Produkt baust, das E-Mails für andere Unternehmen sendet, zum Beispiel ein SaaS für Rechnungen oder Support, bei dem jeder Kunde seine eigene Domain verbindet und ein einzelner Missbrauchsfall nicht allen anderen schaden darf.

Wenn du nur die E-Mails deines eigenen Produkts sendest, brauchst du keine Tenants.

Isolierte Reputation Jeder Tenant erhält einen eigenen Amazon-SES-Tenant. Beschwerden und Bounces eines Kunden erscheinen nur bei diesem Kunden.
Eigene Reputationsüberwachung Dieselben Schwellenwerte des automatischen Pausierens (über 5 % Bounces oder 0,2 % Beschwerden bei den letzten 1.000 E-Mails des Tenants, mit Mindest-Stichprobengrößen) pausieren nur diesen Tenant.
Dein Konto bleibt sauber E-Mails von Tenants zählen nicht für die Reputationsprüfung deines eigenen Teams, sodass ein schlechter Kunde weder dein Konto noch deine anderen Kunden pausieren kann.
Pausenschalter Pausiere und aktiviere jeden Tenant selbst, mit Begründung.
Tageslimit Optionales daily_limit in Empfängern pro UTC-Tag und Tenant.
Deine IDs Sprich einen Tenant über deine eigene external_id an, statt unsere zu speichern.
Zuordnung E-Mails, Domains und Webhook-Events tragen tenant_id.

Tenants teilen sich deinen Plan: Jeder Empfänger eines Tenants zählt auf dein Monatskontingent, die Tageslimits deines Teams und dein API-Rate-Limit. Die Inhaltsfilterung gilt auch für E-Mails von Tenants, einschließlich Tenants eines Kontos, das davon sonst ausgenommen ist.

POST /tenants nimmt einen name und optional eine external_id (eindeutig innerhalb deines Kontos) und ein daily_limit entgegen. Alle Tenant-Endpoints benötigen einen API-Schlüssel mit full_access.

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"
}

Überall, wo ein Tenant angesprochen wird (/tenants/:ref, tenant_id bei E-Mails und Domains, spacepost tenants get <ref>), kannst du entweder unsere id oder deine external_id übergeben. Speichere deine eigene Kunden-ID einmal, und du brauchst nie eine Zuordnungstabelle zu pflegen. Webhooks enthalten immer unsere Tenant-ID; schlage sie mit GET /tenants/{id} nach, wenn du die external_id brauchst.

Domains: im Besitz eines Tenants oder auf Team-Ebene

Abschnitt betitelt „Domains: im Besitz eines Tenants oder auf Team-Ebene“
Domain im Besitz eines Tenants Domain auf Team-Ebene
Erstellt mit tenant_id bei POST /domains POST /domains ohne tenant_id
Typischer Besitzer Die eigene Domain des Kunden (mail.acme-inc.com) Deine Domain (yourapp.com)
Zuordnung Automatisch: Jede E-Mail von ihr gehört dem Tenant Du übergibst tenant_id bei jeder E-Mail, oder die E-Mail ist deine eigene
Wer von ihr senden darf Nur dieser Tenant Deine eigenen E-Mails und jeder Tenant

Nennt eine E-Mail eine tenant_id, die vom Tenant ihrer Domain abweicht, schlägt die Anfrage mit 422 validation_error fehl. Eine tenant_id ist optional, wenn die from-Domain bereits zu einem Tenant gehört.

Füge eine Domain im Besitz eines Tenants genauso hinzu wie jede Domain, mit einem zusätzlichen Feld, und lege dann die DNS-Einträge beim DNS-Anbieter des Kunden an. Siehe Hinzufügen und verifizieren.

Terminal-Fenster
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");
Situation Ergebnis
Senden für einen pausierten Tenant 403 sending_paused. Bereits eingereihte E-Mails schlagen mit einem email.failed-Event fehl.
Tenant über seinem daily_limit 429 daily_quota_exceeded (der Tag wird um 00:00 UTC zurückgesetzt). Aus dieser Anfrage wird nichts gesendet.
tenant_id kollidiert mit dem Tenant der Domain 422 validation_error
Unbekannte tenant_id 404 not_found
Fortsetzen eines Tenants, der von der Reputationsüberwachung pausiert wurde 403 invalid_access, es sei denn, dein Team ist trusted oder internal. Wende dich nach der Behebung der Ursache an support@spacepost.dev. Pausen, die du selbst gesetzt hast, kannst du immer aufheben.
Löschen eines Tenants, der noch Domains hat 422 validation_error. Lösche zuerst seine Domains.

Deine normalen Webhooks erhalten auch Events für E-Mails von Tenants, wobei data.tenant_id auf unsere Tenant-ID gesetzt ist. E-Mails ohne Tenant haben keinen Schlüssel 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} und GET /domains/{id} geben ebenfalls tenant_id zurück (oder null), sodass du in deiner eigenen Datenbank abgleichen kannst.

  1. Erstelle einen Tenant, wenn sich ein Kunde registriert, mit deiner Kunden-ID als external_id.
  2. Entscheide pro Kunde: seine eigene Domain (tenant_id bei POST /domains) oder deine gemeinsame Domain (tenant_id bei jedem Versand).
  3. Setze für neue Kunden ein vorsichtiges daily_limit und erhöhe es, sobald sie sich bewähren.
  4. Abonniere email.bounced, email.complained und email.failed und zeige Probleme pro tenant_id in deiner Admin-Oberfläche an.
  5. Pausiere einen Kunden selbst, sobald du Missbrauch siehst. Das wirkt sofort und ist umkehrbar.