Node.js SDK
@spacepost/node ist ein fetch-basierter Client ohne Abhängigkeiten, mit denselben Methodennamen und demselben Rückgabeformat { data, error } wie das Resend SDK. Er läuft unter Node 18+, Bun, Deno und Edge-Runtimes.
npm install @spacepost/node# or: bun add @spacepost/node | pnpm add @spacepost/node | yarn add @spacepost/nodeEinrichtung
Abschnitt betitelt „Einrichtung“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);| Konstruktor | Fallback aus der Umgebung | Standard |
|---|---|---|
new SpacePost(apiKey) |
SPACEPOST_API_KEY |
keiner; wirft eine Exception, wenn er fehlt |
{ baseUrl } |
SPACEPOST_BASE_URL |
https://api.spacepost.dev |
{ fetch } |
n. z. | globales fetch (für Tests oder Proxys injizierbar) |
{ userAgent } |
n. z. | spacepost-node/<version> |
Ergebnisse und Fehler
Abschnitt betitelt „Ergebnisse und Fehler“Jede Methode gibt ein Promise von { data, error } zurück. Genau eines von beiden ist nicht null. Methoden werfen bei API- oder Netzwerkfehlern nie eine Exception.
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 ist die Ausnahme: Es wirft bei einer ungültigen Signatur eine Exception.)
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);}Felder in Anfragen können in camelCase (replyTo, scheduledAt, contentType, audienceId, tenantId, externalId, dailyLimit) oder snake_case geschrieben werden; das SDK wandelt um. Antworten werden genau so zurückgegeben, wie die API sie sendet (snake_case).
Methodenübersicht
Abschnitt betitelt „Methodenübersicht“| Namespace | Methoden |
|---|---|
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).
import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost();
// sendconst { 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 omittedconst { 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 akzeptiert jedes Feld aus E-Mail senden, einschließlich tenantId für den Plattform-Modus, sowie react für React Email (benötigt die optionale Peer-Dependency @react-email/render; siehe React Email). Der content eines Anhangs darf ein Base64-String, ein Buffer oder ein Uint8Array sein.
batch.send(emails, { idempotencyKey? }) sendet bis zu 100 E-Mails (jede kann ihre eigene tenantId tragen) und liefert { data: { data: [{ id }] } }. Siehe Batch-Versand.
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 orderdomains
Abschnitt betitelt „domains“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 listsconsole.log(all?.data.map((d) => d.name));create({ name, region?, customReturnPath?, tenantId? }), get(id) (mit records), list(), verify(id), update({ id, openTracking?, clickTracking? }), remove(id). Siehe Hinzufügen und verifizieren.
apiKeys
Abschnitt betitelt „apiKeys“create({ name, permission?, domainId? }) gibt { id, token } zurück; das Token wird einmal angezeigt.
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);webhooks
Abschnitt betitelt „webhooks“create, get, list, update, remove verwalten Endpoints (Webhooks).
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 }) prüft die Signatur einer Zustellung und gibt das geparste Event zurück oder wirft eine Exception. Übergib den rohen Body; headers kann ein Headers-Objekt oder ein einfaches Objekt sein.
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 }); }}Webhook-Events sind typisiert. verify gibt einen WebhookPayload zurück, eine über type unterschiedene Union; wenn du auf event.type einengst, erhältst du data.bounce, data.click, data.open, data.failed oder data.suppressed. Jedes Event hat außerdem data.tenant_id, wenn die E-Mail für einen Tenant gesendet wurde. Siehe Event-Typen.
tenants
Abschnitt betitelt „tenants“Plattform-Modus: ein Tenant pro Kunde, für den du sendest, mit isolierter Reputation, einem Pausenschalter und einem optionalen Tageslimit. Siehe die Anleitung zum Plattform-Modus.
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); // truecreate({ name, externalId?, dailyLimit? }), get(idOrExternalId), list({ limit, after, before }), update({ id, name?, externalId?, dailyLimit? }) (dailyLimit: null entfernt das Limit), remove(idOrExternalId) (422, solange der Tenant Domains hat), pause({ id, reason? }) (oder pause(id)) und unpause(idOrExternalId) (403 bei Reputationspausen, es sei denn, dein Team ist trusted). Überall, wo eine ID erwartet wird, kannst du deine external_id übergeben.
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");audiences und contacts
Abschnitt betitelt „audiences und contacts“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);
// Createawait 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);
// Updateawait spacepost.contacts.update({ audienceId: audience.id, email: "ada@example.com", firstName: "Augusta" });
// List, newest firstconst { data: page } = await spacepost.contacts.list({ audienceId: audience.id, limit: 50 });console.log(page?.data.length, page?.has_more);
// Unsubscribe manually, then deleteawait spacepost.contacts.update({ audienceId: audience.id, email: "ada@example.com", unsubscribed: true });await spacepost.contacts.remove({ audienceId: audience.id, email: "ada@example.com" });Kontakte werden mit audienceId plus entweder id oder email angesprochen.
broadcasts
Abschnitt betitelt „broadcasts“create, get, list, update(id, payload) (oder update({ id, ... })), remove, send(id, { scheduledAt? }). Siehe Kampagnen.
import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost(process.env.SPACEPOST_API_KEY);const audienceId = "0192e1c4-0000-7000-8000-0000000000a1";
// 1. Create a draftconst { 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 itconst { error: sendError } = await spacepost.broadcasts.send(draft.id, { scheduledAt: "in 1 hour" });if (sendError) throw new Error(sendError.message);
// 3. Check progressconst { data: broadcast } = await spacepost.broadcasts.get(draft.id);console.log(broadcast?.status); // "queued" | "sending" | "sent" | "failed"suppressions
Abschnitt betitelt „suppressions“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 idIdempotenz
Abschnitt betitelt „Idempotenz“Das zweite Argument von emails.send und batch.send nimmt { idempotencyKey } entgegen. Siehe Idempotenz-Schlüssel.
Stattdessen das Resend SDK verwenden
Abschnitt betitelt „Stattdessen das Resend SDK verwenden“Du brauchst @spacepost/node nicht: Das offizielle Paket resend funktioniert unverändert mit SpacePost. Setze RESEND_BASE_URL=https://api.spacepost.dev oder übergib new Resend(key, { baseUrl: "https://api.spacepost.dev" }). Siehe Migration von Resend.
Zusatzfunktionen von SpacePost wie tenant_id sind nicht Teil der Methoden des Resend SDK, die unbekannte Felder verwerfen. Nutze @spacepost/node, resend.post("/emails", { ..., tenant_id }) oder Domains im Besitz eines Tenants; siehe Plattform-Modus.
TypeScript
Abschnitt betitelt „TypeScript“Alle Typen für Anfragen und Antworten werden exportiert: import type { SendEmailOptions, Email, Domain, Tenant, WebhookPayload } from "@spacepost/node". E-Mails und Domains enthalten tenant_id: string | null.