Pular para o conteúdo

Modo plataforma (tenants)

O modo plataforma permite que uma única conta do SpacePost envie e-mails para muitos clientes seus. Cada cliente é um tenant. Use-o quando você cria um produto que envia e-mails para outras empresas, por exemplo um SaaS de faturamento ou de suporte em que cada cliente conecta o seu próprio domínio e um mau ator não pode prejudicar todos os outros.

Se você só envia os e-mails do seu próprio produto, não precisa de tenants.

Reputação isolada Cada tenant recebe o seu próprio tenant no Amazon SES. As reclamações e rejeições de um cliente aparecem apenas nesse cliente.
Proteção de reputação própria Os mesmos limites da pausa automática (mais de 5% de rejeições ou 0,2% de reclamações nos últimos 1.000 e-mails do tenant, com tamanhos mínimos de amostra) pausam somente esse tenant.
Sua conta continua limpa Os e-mails de tenants não contam para a verificação de reputação da sua própria equipe, então um cliente ruim não pode pausar a sua conta nem os seus outros clientes.
Chave de pausa Pause e retome qualquer tenant você mesmo, informando um motivo.
Teto diário daily_limit opcional, em destinatários por dia UTC, por tenant.
Seus IDs Identifique um tenant pelo seu próprio external_id, em vez de guardar o nosso.
Atribuição E-mails, domínios e eventos de webhook trazem tenant_id.

Os tenants compartilham o seu plano: cada destinatário de um tenant conta para a sua cota mensal, para os limites diários da sua equipe e para o seu limite de taxa da API. A filtragem de conteúdo também se aplica aos e-mails de tenants, inclusive aos tenants de uma conta que, fora isso, esteja isenta dela.

POST /tenants recebe um name e, opcionalmente, um external_id (único na sua conta) e um daily_limit. Todos os endpoints de tenants exigem uma chave de 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"
}

Em todo lugar onde um tenant é identificado (/tenants/:ref, tenant_id em e-mails e domínios, spacepost tenants get <ref>), você pode passar o nosso id ou o seu external_id. Guarde o id do seu cliente uma única vez e você nunca precisará manter uma tabela de mapeamento. Os webhooks sempre trazem o id de tenant nosso; consulte-o com GET /tenants/{id} se precisar do external_id.

Domínio do tenant Domínio da equipe
Criado com tenant_id em POST /domains POST /domains sem tenant_id
Dono típico O domínio do próprio cliente (mail.acme-inc.com) O seu domínio (yourapp.com)
Atribuição Automática: todo e-mail enviado dele pertence ao tenant Você passa tenant_id em cada e-mail, ou o e-mail é seu
Quem pode enviar dele Somente esse tenant Os seus próprios e-mails e qualquer tenant

Se um e-mail indica um tenant_id diferente do tenant do seu domínio, a requisição falha com 422 validation_error. O tenant_id é opcional quando o domínio de from já pertence a um tenant.

Adicione um domínio do tenant exatamente como qualquer domínio, com um campo extra, e depois adicione os registros DNS no provedor de DNS do cliente. Veja Adicionar e verificar.

Janela do 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");
Situação Resultado
Enviar para um tenant pausado 403 sending_paused. Os e-mails que já estavam na fila falham com um evento email.failed.
Tenant acima do seu daily_limit 429 daily_quota_exceeded (o dia é reiniciado às 00:00 UTC). Nada dessa requisição é enviado.
tenant_id em conflito com o tenant do domínio 422 validation_error
tenant_id desconhecido 404 not_found
Retomar um tenant pausado pela proteção de reputação 403 invalid_access, a menos que a sua equipe seja trusted ou internal. Entre em contato com support@spacepost.dev depois de corrigir a causa. As pausas que você mesmo fez sempre podem ser desfeitas.
Excluir um tenant que ainda tem domínios 422 validation_error. Exclua primeiro os domínios dele.

Os seus webhooks normais também recebem eventos dos e-mails de tenants, com data.tenant_id definido com o id do nosso tenant. Os e-mails sem tenant não têm a chave 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} e GET /domains/{id} também retornam tenant_id (ou null), para que você possa reconciliar no seu próprio banco de dados.

  1. Crie um tenant quando um cliente se cadastrar, com o id do seu cliente como external_id.
  2. Decida por cliente: o domínio dele (tenant_id em POST /domains) ou o seu domínio compartilhado (tenant_id em cada envio).
  3. Defina um daily_limit conservador para os clientes novos e aumente-o conforme eles provarem seu valor.
  4. Assine email.bounced, email.complained e email.failed, e exiba os problemas de tenant_id na sua interface de administração.
  5. Pause um cliente você mesmo assim que perceber abuso. É instantâneo e reversível.