Aller au contenu

SDK Node.js

@spacepost/node est un client sans dépendance, basé sur fetch, avec les mêmes noms de méthodes et le même format de retour { data, error } que le SDK Resend. Il fonctionne sur Node 18+, Bun, Deno et les runtimes edge.

Fenêtre de terminal
npm install @spacepost/node
# or: bun add @spacepost/node | pnpm add @spacepost/node | yarn add @spacepost/node
client.ts
import { SpacePost } from "@spacepost/node";
// Reads SPACEPOST_API_KEY (and SPACEPOST_BASE_URL) from the environment when no key is passed.
const spacepost = new SpacePost();
// Or configure explicitly, for example in tests or against a self-hosted deployment.
const custom = new SpacePost("sp_live_YOUR_API_KEY", { baseUrl: "https://api.spacepost.dev" });
console.log(spacepost.baseUrl, custom.baseUrl);
Constructeur Repli sur l’environnement Par défaut
new SpacePost(apiKey) SPACEPOST_API_KEY aucune ; lève une exception si absente
{ baseUrl } SPACEPOST_BASE_URL https://api.spacepost.dev
{ fetch } n/a fetch global (à injecter pour les tests ou les proxys)
{ userAgent } n/a spacepost-node/<version>

Chaque méthode renvoie une promesse de { data, error }. Exactement l’un des deux est non nul. Les méthodes ne lèvent jamais d’exception sur les erreurs d’API ou réseau.

const { data, error } = await spacepost.emails.get(id);
if (error) {
// error.name "not_found" | "validation_error" | ...
// error.message human readable
// error.statusCode HTTP status, or null when the request never got a response
return;
}
console.log(data.last_event);

(webhooks.verify fait exception : elle lève une exception si la signature est invalide.)

errors.ts
import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost();
const { data, error } = await spacepost.emails.send({
from: "Acme <hello@acme.com>",
to: "user@example.com",
subject: "Hello",
html: "<p>Hello</p>",
});
if (error) {
switch (error.name) {
case "rate_limit_exceeded":
// wait about a second, then retry with the same idempotency key
break;
case "daily_quota_exceeded":
case "monthly_quota_exceeded":
// alert someone: you hit a limit
break;
case "validation_error":
case "invalid_from_address":
// a bug in your request: do not retry
throw new Error(`${error.name}: ${error.message}`);
default:
console.error(error.statusCode, error.name, error.message);
}
} else {
console.log(data.id);
}

Les champs de requête peuvent s’écrire en camelCase (replyTo, scheduledAt, contentType, audienceId, tenantId, externalId, dailyLimit) ou en snake_case ; le SDK convertit. Les réponses sont renvoyées exactement comme l’API les envoie (snake_case).

Espace de noms Méthodes
emails send, get, list, update, cancel
batch send
domains create, get, list, verify, update, remove
apiKeys create, list, remove
webhooks create, get, list, update, remove, verify
audiences create, get, list, remove
contacts create, get, list, update, remove
broadcasts create, get, list, update, remove, send
suppressions list, create, remove
tenants create, get, list, update, remove, pause, unpause

send(payload, { idempotencyKey? }), get(id), list({ limit, after, before }), update({ id, scheduledAt }), cancel(id).

emails.ts
import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost();
// send
const { data: sent } = await spacepost.emails.send({
from: "Acme <hello@acme.com>",
to: "user@example.com",
subject: "Hello",
html: "<p>Hello</p>",
scheduledAt: "in 1 hour",
});
if (sent) {
// get: includes html/text and last_event
const { data: email } = await spacepost.emails.get(sent.id);
console.log(email?.last_event); // "scheduled"
// update: reschedule
await spacepost.emails.update({ id: sent.id, scheduledAt: "in 3 hours" });
// cancel
await spacepost.emails.cancel(sent.id);
}
// list: cursor pagination, newest first, bodies omitted
const { data: page } = await spacepost.emails.list({ limit: 10 });
if (page?.has_more) {
const last = page.data[page.data.length - 1]!;
const { data: older } = await spacepost.emails.list({ limit: 10, after: last.id });
console.log(older?.data.length);
}

send accepte tous les champs de Envoyer un e-mail, y compris tenantId pour le mode plateforme, plus react pour React Email (nécessite la dépendance peer facultative @react-email/render ; voir React Email). Le content d’une pièce jointe peut être une chaîne base64, un Buffer ou un Uint8Array.

batch.send(emails, { idempotencyKey? }) envoie jusqu’à 100 e-mails (chacun peut porter son propre tenantId) et se résout en { data: { data: [{ id }] } }. Voir Envoi par lot.

batch.ts
import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost(process.env.SPACEPOST_API_KEY);
const { data, error } = await spacepost.batch.send(
[
{ from: "Acme <hello@acme.com>", to: ["a@example.com"], subject: "Hi Ada", html: "<p>Hi Ada</p>" },
{ from: "Acme <hello@acme.com>", to: ["b@example.com"], subject: "Hi Bob", html: "<p>Hi Bob</p>" },
],
{ idempotencyKey: "weekly-digest-2026-w43" },
);
if (error) console.error(error);
else console.log(data.data.map((e) => e.id)); // ids in input order
domains.ts
import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost();
const { data: domain } = await spacepost.domains.create({ name: "acme.com", customReturnPath: "send" });
if (domain) {
await spacepost.domains.verify(domain.id);
const { data: fresh } = await spacepost.domains.get(domain.id); // includes records
console.log(fresh?.status, fresh?.records);
await spacepost.domains.update({ id: domain.id, openTracking: true, clickTracking: true });
await spacepost.domains.remove(domain.id);
}
const { data: all } = await spacepost.domains.list(); // no records in lists
console.log(all?.data.map((d) => d.name));

create({ name, region?, customReturnPath?, tenantId? }), get(id) (avec records), list(), verify(id), update({ id, openTracking?, clickTracking? }), remove(id). Voir Ajouter et vérifier.

create({ name, permission?, domainId? }) renvoie { id, token } ; le jeton n’est affiché qu’une fois.

api-keys.ts
import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost();
// A key that can only send, and only from one domain.
const { data: key, error } = await spacepost.apiKeys.create({
name: "worker-prod",
permission: "sending_access",
domainId: "0192e1c4-0000-7000-8000-000000000001",
});
if (error) throw new Error(error.message);
console.log(key.token); // sp_live_..., shown only once
const { data: keys } = await spacepost.apiKeys.list();
console.log(keys?.data.map((k) => `${k.name} ${k.prefix}... ${k.permission}`));
await spacepost.apiKeys.remove(key.id);

create, get, list, update, remove gèrent les endpoints (Webhooks).

create-webhook.ts
import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost(process.env.SPACEPOST_API_KEY);
const { data: webhook, error } = await spacepost.webhooks.create({
endpoint: "https://acme.com/webhooks/spacepost",
events: ["email.delivered", "email.bounced", "email.complained"],
});
if (error) throw new Error(`${error.name}: ${error.message}`);
// Store this secret: you need it to verify signatures. It starts with "whsec_".
console.log(webhook.id, webhook.signing_secret);

verify({ payload, headers, secret }) vérifie la signature d’une distribution et renvoie l’événement analysé, ou lève une exception. Passez le corps brut ; headers peut être un objet Headers ou un objet simple.

webhook-handler.ts
import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost(process.env.SPACEPOST_API_KEY);
/** Works as a Bun, Deno, Cloudflare Workers or Next.js route handler. */
export async function POST(request: Request): Promise<Response> {
const payload = await request.text(); // the RAW body: do not parse it first
try {
const event = await spacepost.webhooks.verify({
payload,
headers: request.headers,
secret: process.env.SPACEPOST_WEBHOOK_SECRET!,
});
switch (event.type) {
case "email.bounced":
case "email.complained":
console.warn("Stop mailing", event.data.to);
break;
case "email.delivered":
console.log("Delivered", event.data.email_id);
break;
}
return new Response("ok");
} catch {
return new Response("invalid signature", { status: 400 });
}
}

Les événements de webhook sont typés. verify renvoie un WebhookPayload, une union discriminée par type : en affinant sur event.type, vous obtenez data.bounce, data.click, data.open, data.failed ou data.suppressed. Chaque événement a aussi data.tenant_id lorsque l’e-mail a été envoyé pour un tenant. Voir Types d’événements.

Mode plateforme : un tenant par client pour lequel vous envoyez, avec une réputation isolée, un interrupteur de pause et un plafond quotidien facultatif. Voir le guide du mode plateforme.

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

create({ name, externalId?, dailyLimit? }), get(idOrExternalId), list({ limit, after, before }), update({ id, name?, externalId?, dailyLimit? }) (dailyLimit: null supprime le plafond), remove(idOrExternalId) (422 tant que le tenant a des domaines), pause({ id, reason? }) (ou pause(id)) et unpause(idOrExternalId) (403 pour les pauses de réputation, sauf si votre équipe est trusted). Partout où un id est attendu, vous pouvez passer votre external_id.

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");
contacts.ts
import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost(process.env.SPACEPOST_API_KEY);
const { data: audience, error } = await spacepost.audiences.create({ name: "Newsletter" });
if (error) throw new Error(error.message);
// Create
await spacepost.contacts.create({
audienceId: audience.id,
email: "ada@example.com",
firstName: "Ada",
lastName: "Lovelace",
});
// Read by email (or by id)
const { data: contact } = await spacepost.contacts.get({ audienceId: audience.id, email: "ada@example.com" });
console.log(contact?.unsubscribed);
// Update
await spacepost.contacts.update({ audienceId: audience.id, email: "ada@example.com", firstName: "Augusta" });
// List, newest first
const { data: page } = await spacepost.contacts.list({ audienceId: audience.id, limit: 50 });
console.log(page?.data.length, page?.has_more);
// Unsubscribe manually, then delete
await spacepost.contacts.update({ audienceId: audience.id, email: "ada@example.com", unsubscribed: true });
await spacepost.contacts.remove({ audienceId: audience.id, email: "ada@example.com" });

Les contacts sont désignés par audienceId plus id ou email.

create, get, list, update(id, payload) (ou update({ id, ... })), remove, send(id, { scheduledAt? }). Voir Campagnes.

broadcast.ts
import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost(process.env.SPACEPOST_API_KEY);
const audienceId = "0192e1c4-0000-7000-8000-0000000000a1";
// 1. Create a draft
const { data: draft, error } = await spacepost.broadcasts.create({
audienceId,
name: "October newsletter",
from: "Acme <news@acme.com>",
replyTo: "support@acme.com",
subject: "What's new at Acme, {{{FIRST_NAME|friend}}}",
previewText: "Three things we shipped this month",
html: `
<h1>Hi {{{FIRST_NAME|there}}},</h1>
<p>Here is what is new.</p>
<p><a href="{{{SPACEPOST_UNSUBSCRIBE_URL}}}">Unsubscribe</a></p>
`,
});
if (error) throw new Error(error.message);
// 2. Send it now, or schedule it
const { error: sendError } = await spacepost.broadcasts.send(draft.id, { scheduledAt: "in 1 hour" });
if (sendError) throw new Error(sendError.message);
// 3. Check progress
const { data: broadcast } = await spacepost.broadcasts.get(draft.id);
console.log(broadcast?.status); // "queued" | "sending" | "sent" | "failed"
suppressions.ts
import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost();
await spacepost.suppressions.create({ email: "do-not-mail@example.com" });
const { data } = await spacepost.suppressions.list({ limit: 100 });
for (const s of data?.data ?? []) console.log(s.email, s.reason);
await spacepost.suppressions.remove("do-not-mail@example.com"); // by email or id

Le second argument de emails.send et de batch.send accepte { idempotencyKey }. Voir Clés d’idempotence.

Vous n’avez pas besoin de @spacepost/node : le paquet officiel resend fonctionne sans modification avec SpacePost. Définissez RESEND_BASE_URL=https://api.spacepost.dev ou passez new Resend(key, { baseUrl: "https://api.spacepost.dev" }). Voir Migrer depuis Resend.

Les extras de SpacePost comme tenant_id ne font pas partie des méthodes du SDK Resend, qui supprime les champs inconnus. Utilisez @spacepost/node, resend.post("/emails", { ..., tenant_id }) ou des domaines appartenant à un tenant ; voir Mode plateforme.

Tous les types de requête et de réponse sont exportés : import type { SendEmailOptions, Email, Domain, Tenant, WebhookPayload } from "@spacepost/node". Les e-mails et les domaines incluent tenant_id: string | null.