Modalità piattaforma (tenant)
La modalità piattaforma permette a un solo account SpacePost di inviare per molti dei tuoi clienti. Ogni cliente è un tenant. Usala quando costruisci un prodotto che invia email per altre aziende, ad esempio un SaaS di fatturazione o di assistenza in cui ogni cliente collega il proprio dominio e un singolo malintenzionato non deve danneggiare tutti gli altri.
Se invii solo le email del tuo prodotto, non hai bisogno dei tenant.
Che cosa ti offre un tenant
Sezione intitolata “Che cosa ti offre un tenant”| Reputazione isolata | Ogni tenant ha il proprio tenant Amazon SES. I reclami e i rimbalzi di un cliente compaiono solo su quel cliente. |
| Una propria protezione della reputazione | Le stesse soglie della pausa automatica (oltre il 5% di rimbalzi o lo 0,2% di reclami sulle ultime 1.000 email del tenant, con dimensioni minime del campione) mettono in pausa solo quel tenant. |
| Il tuo account resta pulito | Le email dei tenant non contano per il controllo di reputazione del tuo team, quindi un cliente problematico non può mettere in pausa il tuo account né i tuoi altri clienti. |
| Interruttore di pausa | Metti in pausa e riprendi tu stesso qualsiasi tenant, indicando un motivo. |
| Tetto giornaliero | daily_limit facoltativo in destinatari per giorno UTC, per tenant. |
| I tuoi ID | Indirizza un tenant con il tuo external_id invece di salvare il nostro. |
| Attribuzione | Email, domini ed eventi webhook includono tenant_id. |
I tenant condividono il tuo piano: ogni destinatario di un tenant conta sulla tua quota mensile, sui limiti giornalieri del tuo team e sul tuo limite di frequenza dell’API. Il filtraggio dei contenuti si applica anche alle email dei tenant, compresi i tenant di un account che per il resto ne è esente.
Creare un tenant
Sezione intitolata “Creare un tenant”POST /tenants richiede un name e, facoltativamente, un external_id (univoco all’interno del tuo account) e un daily_limit. Tutti gli endpoint dei tenant richiedono una chiave 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
Sezione intitolata “external_id”Ovunque un tenant venga indirizzato (/tenants/:ref, tenant_id su email e domini, spacepost tenants get <ref>) puoi passare il nostro id oppure il tuo external_id. Salva una volta sola l’id del tuo cliente e non avrai mai bisogno di mantenere una tabella di corrispondenza. I webhook portano sempre il nostro id del tenant; cercalo con GET /tenants/{id} se ti serve l’external_id.
Domini: di proprietà del tenant o a livello di team
Sezione intitolata “Domini: di proprietà del tenant o a livello di team”| Dominio di proprietà del tenant | Dominio a livello di team | |
|---|---|---|
| Creato con | tenant_id su POST /domains |
POST /domains senza tenant_id |
| Proprietario tipico | Il dominio del cliente (mail.acme-inc.com) |
Il tuo dominio (yourapp.com) |
| Attribuzione | Automatica: ogni email inviata da esso appartiene al tenant | Passi tenant_id a ogni email, oppure l’email è tua |
| Chi può inviare da esso | Solo quel tenant | Le tue email e qualsiasi tenant |
Se un’email indica un tenant_id diverso da quello del tenant del suo dominio, la richiesta fallisce con 422 validation_error. Un tenant_id è facoltativo quando il dominio di from appartiene già a un tenant.
Aggiungi un dominio di proprietà del tenant esattamente come qualsiasi dominio, con un campo in più, poi aggiungi i record DNS presso il provider DNS del cliente. Vedi Aggiungere e verificare.
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_123Inviare per un tenant
Sezione intitolata “Inviare per un 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 accetta tenant_id su ogni elemento.
Il pacchetto ufficiale resend funziona con SpacePost senza modifiche. Puntalo su di noi con RESEND_BASE_URL=https://api.spacepost.dev oppure new Resend(key, { baseUrl }).
Il modo più semplice: invia da un dominio di proprietà del tenant. Il tenant viene dedotto dal dominio di from, quindi le tue chiamate esistenti a resend.emails.send non richiedono alcuna modifica.
Da un dominio a livello di team, usa resend.post. L’SDK di Resend costruisce il corpo della richiesta da un elenco fisso di campi, quindi un tenant_id aggiuntivo passato a resend.emails.send, resend.batch.send o resend.domains.create viene scartato silenziosamente prima di raggiungere SpacePost. resend.post invia il corpo che gli dai così com’è, quindi usalo con campi in 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",});A differenza di resend.emails.send, resend.post non converte il camelCase: scrivi reply_to e scheduled_at. Lo stesso vale per 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_123Pausa, ripresa e tetti
Sezione intitolata “Pausa, ripresa e tetti”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| Situazione | Risultato |
|---|---|
| Invio per un tenant in pausa | 403 sending_paused. Le email già in coda falliscono con un evento email.failed. |
Tenant oltre il suo daily_limit |
429 daily_quota_exceeded (il giorno si azzera alle 00:00 UTC). Nulla di quella richiesta viene inviato. |
tenant_id in conflitto con il tenant del dominio |
422 validation_error |
tenant_id sconosciuto |
404 not_found |
| Riattivare un tenant messo in pausa dalla protezione della reputazione | 403 invalid_access, a meno che il tuo team sia trusted o internal. Contatta support@spacepost.dev dopo aver risolto la causa. Le pause che hai impostato tu stesso possono sempre essere rimosse. |
| Eliminare un tenant che ha ancora dei domini | 422 validation_error. Elimina prima i suoi domini. |
Webhook
Sezione intitolata “Webhook”I tuoi webhook normali ricevono eventi anche per le email dei tenant, con data.tenant_id impostato sul nostro id del tenant. Le email senza tenant non hanno la chiave 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 }); }}Anche GET /emails/{id} e GET /domains/{id} restituiscono tenant_id (o null), così puoi riconciliare nel tuo database.
Checklist per un’integrazione SaaS
Sezione intitolata “Checklist per un’integrazione SaaS”- Crea un tenant quando un cliente si iscrive, con l’id del tuo cliente come
external_id. - Decidi per ogni cliente: il suo dominio (
tenant_idsuPOST /domains) oppure il tuo dominio condiviso (tenant_ida ogni invio). - Imposta un
daily_limitprudente per i nuovi clienti e aumentalo man mano che si dimostrano affidabili. - Iscriviti a
email.bounced,email.complainedeemail.failed, e mostra i problemi pertenant_idnella tua interfaccia di amministrazione. - Metti in pausa un cliente tu stesso nel momento in cui vedi un abuso. È istantaneo e reversibile.