コンテンツにスキップ

Node.js SDK

@spacepost/node は、依存パッケージのない fetch ベースのクライアントで、メソッド名と { data, error } の戻り値の形式は Resend SDK と同じです。Node 18 以降、Bun、Deno、エッジランタイムで動作します。

ターミナルウィンドウ
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);
コンストラクター 環境変数によるフォールバック 既定値
new SpacePost(apiKey) SPACEPOST_API_KEY なし。ない場合は例外をスローします
{ baseUrl } SPACEPOST_BASE_URL https://api.spacepost.dev
{ fetch } なし グローバルの fetch(テストやプロキシのために差し替えられます)
{ userAgent } なし spacepost-node/<version>

すべてのメソッドは、{ data, error } の Promise を返します。どちらか一方だけが null ではありません。メソッドは、API やネットワークのエラーで例外をスローしません。

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 は例外です。署名が不正な場合は例外をスローします。)

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);
}

リクエストのフィールドは、キャメルケース(replyTo、scheduledAt、contentType、audienceId、tenantId、externalId、dailyLimit)でも snake_case でも書けます。SDK が変換します。レスポンスは、API が送ってくるとおり(snake_case)に返されます。

名前空間 メソッド
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 は、メールを送信するのすべてのフィールドを受け付けます。プラットフォームモード用の tenantId と、React Email 用の react も含まれます(オプションのピア依存関係 @react-email/render が必要です。React Emailをご覧ください)。添付ファイルの content には、base64 文字列、Buffer、Uint8Array を渡せます。

batch.send(emails, { idempotencyKey? }) は最大 100 通のメールを送信し(それぞれが独自の tenantId を持てます)、{ data: { data: [{ id }] } } に解決されます。バッチ送信をご覧ください。

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)(records 付き)、list()、verify(id)、update({ id, openTracking?, clickTracking? })、remove(id)。追加と検証をご覧ください。

create({ name, permission?, domainId? }) は { id, token } を返します。トークンが表示されるのは一度だけです。

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 でエンドポイントを管理します(Webhook)。

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 }) は、配信の署名を検証してパース済みのイベントを返すか、例外をスローします。生のボディを渡してください。headers には、Headers オブジェクトまたはプレーンなオブジェクトを渡せます。

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 });
}
}

Webhook のイベントには型が付いています。verify は、type で判別される共用体型 WebhookPayload を返すため、event.type で絞り込むと、data.bounce、data.click、data.open、data.failed、data.suppressed が得られます。メールがテナントのために送信された場合は、すべてのイベントに data.tenant_id も含まれます。イベントタイプをご覧ください。

プラットフォームモード: 送信を代行するお客様ごとに 1 つのテナントがあり、評判の分離、一時停止スイッチ、任意の 1 日の上限を備えています。プラットフォームモードのガイドをご覧ください。

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 で上限を解除)、remove(idOrExternalId)(テナントにドメインが残っている間は 422)、pause({ id, reason? })(または pause(id))、unpause(idOrExternalId)(評判による一時停止は、チームが trusted でない限り 403)。id が求められる箇所では、ご自身の 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" });

コンタクトは、audienceId と、id または email のどちらかで指定します。

create、get、list、update(id, payload)(または update({ id, ... }))、remove、send(id, { scheduledAt? })。一斉配信をご覧ください。

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

emails.send と batch.send の第 2 引数は { idempotencyKey } を受け取ります。冪等キーをご覧ください。

@spacepost/node は必須ではありません。公式の resend パッケージは、SpacePost に対してそのまま動作します。RESEND_BASE_URL=https://api.spacepost.dev を設定するか、new Resend(key, { baseUrl: "https://api.spacepost.dev" }) を渡してください。Resend からの移行をご覧ください。

tenant_id のような SpacePost の拡張機能は Resend SDK のメソッドに含まれておらず、SDK は未知のフィールドを破棄します。@spacepost/node、resend.post("/emails", { ..., tenant_id })、またはテナント所有のドメインを使ってください。プラットフォームモードをご覧ください。

リクエストとレスポンスの型はすべてエクスポートされています: import type { SendEmailOptions, Email, Domain, Tenant, WebhookPayload } from "@spacepost/node"。メールとドメインには tenant_id: string | null が含まれます。