Aller au contenu

Mode plateforme (tenants)

Le mode plateforme permet à un seul compte SpacePost d’envoyer pour plusieurs de vos clients. Chaque client est un tenant. Utilisez-le lorsque vous construisez un produit qui envoie des e-mails pour d’autres entreprises, par exemple un SaaS de facturation ou de support où chaque client connecte son propre domaine et où un seul mauvais acteur ne doit pas nuire à tous les autres.

Si vous n’envoyez que les e-mails de votre propre produit, vous n’avez pas besoin de tenants.

Réputation isolée Chaque tenant reçoit son propre tenant Amazon SES. Les plaintes et les rebonds d’un client n’apparaissent que sur ce client.
Son propre garde-fou de réputation Les mêmes seuils de mise en pause automatique (plus de 5 % de rebonds ou 0,2 % de plaintes sur les 1 000 derniers e-mails du tenant, avec des tailles d’échantillon minimales) mettent en pause uniquement ce tenant.
Votre compte reste propre Les e-mails d’un tenant ne comptent pas dans la vérification de réputation de votre propre équipe : un mauvais client ne peut donc pas mettre en pause votre compte ni vos autres clients.
Interrupteur de pause Mettez en pause et reprenez n’importe quel tenant vous-même, avec une raison.
Plafond quotidien daily_limit facultatif, en destinataires par jour UTC et par tenant.
Vos ID Désignez un tenant par votre propre external_id au lieu de stocker le nôtre.
Attribution Les e-mails, les domaines et les événements de webhook portent tenant_id.

Les tenants partagent votre forfait : chaque destinataire d’un tenant compte dans votre quota mensuel, les limites quotidiennes de votre équipe et votre limite de débit d’API. Le filtrage de contenu s’applique aussi aux e-mails des tenants, y compris les tenants d’un compte qui en est par ailleurs exempté.

POST /tenants prend un name et, en option, un external_id (unique au sein de votre compte) et un daily_limit. Tous les endpoints de tenants exigent une clé API 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"
}

Partout où un tenant est désigné (/tenants/:ref, tenant_id sur les e-mails et les domaines, spacepost tenants get <ref>), vous pouvez passer soit notre id, soit votre external_id. Stockez une seule fois votre propre id client et vous n’aurez jamais besoin de table de correspondance. Les webhooks portent toujours notre id de tenant ; retrouvez-le avec GET /tenants/{id} si vous avez besoin de l’external_id.

Domaines : appartenant au tenant ou au niveau de l’équipe

Section intitulée « Domaines : appartenant au tenant ou au niveau de l’équipe »
Domaine appartenant au tenant Domaine au niveau de l’équipe
Créé avec tenant_id sur POST /domains POST /domains sans tenant_id
Propriétaire typique Le domaine propre du client (mail.acme-inc.com) Votre domaine (yourapp.com)
Attribution Automatique : chaque e-mail qui en part appartient au tenant Vous passez tenant_id sur chaque e-mail, ou l’e-mail est le vôtre
Qui peut envoyer depuis ce domaine Uniquement ce tenant Vos propres e-mails, et n’importe quel tenant

Si un e-mail indique un tenant_id différent de celui de son domaine, la requête échoue avec 422 validation_error. Un tenant_id est facultatif lorsque le domaine from appartient déjà à un tenant.

Ajoutez un domaine appartenant à un tenant exactement comme n’importe quel domaine, avec un champ supplémentaire, puis ajoutez les enregistrements DNS chez le fournisseur DNS du client. Voir Ajouter et vérifier.

Fenêtre de terminal
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 Résultat
Envoi pour un tenant en pause 403 sending_paused. Les e-mails déjà en file échouent avec un événement email.failed.
Tenant au-delà de son daily_limit 429 daily_quota_exceeded (le jour se réinitialise à 00:00 UTC). Rien de cette requête n’est envoyé.
tenant_id en conflit avec le tenant du domaine 422 validation_error
tenant_id inconnu 404 not_found
Reprise d’un tenant mis en pause par le garde-fou de réputation 403 invalid_access, sauf si votre équipe est trusted ou internal. Contactez support@spacepost.dev après avoir corrigé la cause. Les pauses que vous avez faites vous-même peuvent toujours être levées.
Suppression d’un tenant qui a encore des domaines 422 validation_error. Supprimez d’abord ses domaines.

Vos webhooks habituels reçoivent aussi les événements des e-mails de tenants, avec data.tenant_id défini sur notre id de tenant. Les e-mails sans tenant n’ont pas de clé 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} et GET /domains/{id} renvoient aussi tenant_id (ou null), ce qui vous permet de réconcilier avec votre propre base de données.

  1. Créez un tenant lorsqu’un client s’inscrit, avec votre id client comme external_id.
  2. Décidez par client : son propre domaine (tenant_id sur POST /domains) ou votre domaine partagé (tenant_id à chaque envoi).
  3. Définissez un daily_limit prudent pour les nouveaux clients et relevez-le à mesure qu’ils font leurs preuves.
  4. Abonnez-vous à email.bounced, email.complained et email.failed, et faites remonter les problèmes de tenant_id dans votre interface d’administration.
  5. Mettez un client en pause vous-même dès que vous constatez un abus. C’est instantané et réversible.