Ir al contenido

Modo plataforma (tenants)

El modo plataforma permite que una cuenta de SpacePost envíe en nombre de muchos de tus clientes. Cada cliente es un tenant. Úsalo cuando construyas un producto que envía correo para otras empresas, por ejemplo un SaaS de facturación o de soporte donde cada cliente conecta su propio dominio y un mal actor no debe perjudicar a todos los demás.

Si solo envías el correo de tu propio producto, no necesitas tenants.

Reputación aislada Cada tenant recibe su propio tenant de Amazon SES. Las quejas y los rebotes de un cliente solo se reflejan en ese cliente.
Su propia protección de reputación Los mismos umbrales de pausa automática (más de un 5 % de rebotes o un 0,2 % de quejas en los últimos 1.000 correos del tenant, con tamaños mínimos de muestra) pausan solo a ese tenant.
Tu cuenta se mantiene limpia El correo de los tenants no cuenta en la comprobación de reputación de tu propio equipo, así que un mal cliente no puede pausar tu cuenta ni a tus otros clientes.
Interruptor de pausa Pausa y reanuda tú mismo cualquier tenant, indicando un motivo.
Tope diario daily_limit opcional, en destinatarios por día UTC y por tenant.
Tus IDs Identifica a un tenant con tu propio external_id en lugar de guardar el nuestro.
Atribución Los correos, los dominios y los eventos de webhook llevan tenant_id.

Los tenants comparten tu plan: cada destinatario de un tenant cuenta para tu cuota mensual, los límites diarios de tu equipo y tu límite de tasa de la API. El filtrado de contenido también se aplica al correo de los tenants, incluidos los de una cuenta que por lo demás esté exenta de él.

POST /tenants recibe un name y, opcionalmente, un external_id (único dentro de tu cuenta) y un daily_limit. Todos los endpoints de tenants necesitan una clave 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"
}

En cualquier lugar donde se identifica a un tenant (/tenants/:ref, tenant_id en correos y dominios, spacepost tenants get <ref>) puedes pasar nuestro id o tu external_id. Guarda una sola vez el id de tu cliente y no necesitarás mantener una tabla de correspondencias. Los webhooks siempre llevan nuestro id de tenant; consúltalo con GET /tenants/{id} si necesitas el external_id.

Dominios: propiedad del tenant o de nivel de equipo

Sección titulada «Dominios: propiedad del tenant o de nivel de equipo»
Dominio propiedad del tenant Dominio de nivel de equipo
Se crea con tenant_id en POST /domains POST /domains sin tenant_id
Propietario habitual El dominio del propio cliente (mail.acme-inc.com) Tu dominio (yourapp.com)
Atribución Automática: todos los correos enviados desde él pertenecen al tenant Pasas tenant_id en cada correo, o el correo es tuyo
Quién puede enviar desde él Solo ese tenant Tu propio correo y cualquier tenant

Si un correo indica un tenant_id distinto del tenant de su dominio, la petición falla con 422 validation_error. El tenant_id es opcional cuando el dominio de from ya pertenece a un tenant.

Añade un dominio propiedad de un tenant igual que cualquier otro dominio, con un campo adicional, y luego añade los registros DNS en el proveedor de DNS del cliente. Consulta Añadir y verificar.

Ventana 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");
Situación Resultado
Enviar para un tenant pausado 403 sending_paused. Los correos que ya estaban en cola fallan con un evento email.failed.
Tenant que supera su daily_limit 429 daily_quota_exceeded (el día se reinicia a las 00:00 UTC). No se envía nada de esa petición.
tenant_id en conflicto con el tenant del dominio 422 validation_error
tenant_id desconocido 404 not_found
Reanudar un tenant pausado por la protección de reputación 403 invalid_access, salvo que tu equipo sea trusted o internal. Contacta con support@spacepost.dev tras corregir la causa. Las pausas que hiciste tú mismo siempre se pueden levantar.
Eliminar un tenant que aún tiene dominios 422 validation_error. Elimina primero sus dominios.

Tus webhooks habituales también reciben eventos del correo de los tenants, con data.tenant_id establecido con nuestro id de tenant. Los correos sin tenant no tienen la clave 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} y GET /domains/{id} también devuelven tenant_id (o null), para que puedas conciliar en tu propia base de datos.

Lista de comprobación para una integración SaaS

Sección titulada «Lista de comprobación para una integración SaaS»
  1. Crea un tenant cuando un cliente se registre, con el id de tu cliente como external_id.
  2. Decide por cliente: su propio dominio (tenant_id en POST /domains) o tu dominio compartido (tenant_id en cada envío).
  3. Define un daily_limit prudente para los clientes nuevos y súbelo a medida que demuestren su fiabilidad.
  4. Suscríbete a email.bounced, email.complained y email.failed, y muestra los problemas de tenant_id en tu interfaz de administración.
  5. Pausa tú mismo a un cliente en cuanto veas un abuso. Es instantáneo y reversible.