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.
O que um tenant oferece
Seção intitulada “O que um tenant oferece”| 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.
Criar um tenant
Seção intitulada “Criar um tenant”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.
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
Seção intitulada “external_id”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ínios: do tenant ou da equipe
Seção intitulada “Domínios: do tenant ou da equipe”| 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.
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_123Enviar em nome de um tenant
Seção intitulada “Enviar em nome de um tenant”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 aceita tenant_id em cada item.
O pacote oficial resend funciona com o SpacePost sem alterações. Aponte-o para nós com RESEND_BASE_URL=https://api.spacepost.dev ou new Resend(key, { baseUrl }).
Mais fácil: envie de um domínio do tenant. O tenant é inferido a partir do domínio de from, então as suas chamadas existentes de resend.emails.send não precisam de nenhuma alteração.
De um domínio da equipe, use resend.post. O SDK do Resend monta o corpo da requisição a partir de uma lista fixa de campos, então um tenant_id extra passado a resend.emails.send, resend.batch.send ou resend.domains.create é descartado silenciosamente antes de chegar ao SpacePost. O resend.post envia o corpo que você informar exatamente como está, então use-o com campos em 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",});Ao contrário de resend.emails.send, o resend.post não converte camelCase: escreva reply_to e scheduled_at. O mesmo vale para resend.post("/domains", { name, tenant_id }) e resend.post("/emails/batch", [...]).
spacepost send --from invoices@yourapp.com --to buyer@example.com \ --subject "Invoice #1042" --text "Your invoice is attached." --tenant cust_123Pausar, retomar e tetos
Seção intitulada “Pausar, retomar e tetos”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| 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. |
Webhooks
Seção intitulada “Webhooks”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" } }}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.
Checklist para uma integração de SaaS
Seção intitulada “Checklist para uma integração de SaaS”- Crie um tenant quando um cliente se cadastrar, com o id do seu cliente como
external_id. - Decida por cliente: o domínio dele (
tenant_idemPOST /domains) ou o seu domínio compartilhado (tenant_idem cada envio). - Defina um
daily_limitconservador para os clientes novos e aumente-o conforme eles provarem seu valor. - Assine
email.bounced,email.complainedeemail.failed, e exiba os problemas detenant_idna sua interface de administração. - Pause um cliente você mesmo assim que perceber abuso. É instantâneo e reversível.