Node.js SDK

Official Node.js SDK for Posthawk. Works with Node.js, Next.js, React, and Express. Full TypeScript support, zero dependencies, and built-in React Email integration.

SDKnpm install posthawk

Install the SDK from npm. Ships as both ESM and CommonJS with full TypeScript declarations. To use React Email components with the react prop, also install the optional peer dependency: npm install @react-email/render

npm install posthawk
npm install posthawk
SDKnew Posthawk(key)

Initialize the client and send your first email. The constructor accepts a plain API key string (uses the default cloud URL) or a config object for self-hosted instances.

Returns

Posthawk

new Posthawk(key)
import { Posthawk } from 'posthawk';

// Cloud (default base URL: https://api.posthawk.dev)
const posthawk = new Posthawk('ck_live_...');

// Self-hosted
const posthawk = new Posthawk({
  apiKey: 'ck_live_...',
  baseUrl: 'https://api.yourdomain.com',
});

const { data, error } = await posthawk.emails.send({
  from: 'hello@yourdomain.com',
  to: 'user@example.com',
  subject: 'Welcome!',
  html: '<h1>Hello!</h1>',
});

if (error) {
  console.error(error.message);
} else {
  console.log('Sent!', data.jobId);
}
SDKposthawk.emails.send(params)

Send an email immediately, or schedule it for later by including scheduledFor. Returns { data, error }. At least one of html, text, react, or templateId is required.

Returns

{ data, error }

Parameters

fromstringrequired

Sender email address (must be from a verified domain)

tostring | string[]required

Recipient email address(es)

ccstring | string[]optional

CC recipient(s)

bccstring | string[]optional

BCC recipient(s)

subjectstringrequired

Email subject line

htmlstringoptional

HTML email body

textstringoptional

Plain text email body

reactReactElementoptional

React Email component (requires @react-email/render)

templateIdstringoptional

Posthawk template ID

variablesRecord<string, string>optional

Template variable substitution

headersRecord<string, string>optional

Custom email headers (e.g. List-Unsubscribe)

scheduledForstring | Dateoptional

Schedule for later delivery (ISO 8601 or Date object)

timezonestringoptional

IANA timezone for scheduled time

replyTostringoptional

Reply-to email address

metadataRecord<string, unknown>optional

Custom metadata attached to the email

tagsRecord<string, unknown>optional

Custom tags for filtering and search

idempotencyKeystringoptional

Sent as the Idempotency-Key header for safe retries (response cached 24h). Retried POSTs are only auto-retried by the SDK when this is set.

posthawk.emails.send(params)
const { data, error } = await posthawk.emails.send({
  from: 'hello@yourdomain.com',
  to: ['user@example.com', 'other@example.com'],
  subject: 'Welcome to Acme',
  html: '<h1>Welcome!</h1><p>Thanks for signing up.</p>',
  metadata: { userId: 'usr_123' },
  tags: { campaign: 'onboarding' },
  idempotencyKey: '550e8400-e29b-41d4-a716-446655440000',
});

// Schedule for later
const { data, error } = await posthawk.emails.send({
  from: 'hello@yourdomain.com',
  to: 'user@example.com',
  subject: 'Reminder',
  text: "Don't forget your meeting!",
  scheduledFor: '2026-03-15T09:00:00Z',
  timezone: 'America/New_York',
});
SDKposthawk.emails.get(jobId)

Get the delivery status of a previously queued email by its job ID.

Returns

{ data, error }

Parameters

jobIdstringrequired

Job ID returned from the send method

posthawk.emails.get(jobId)
const { data, error } = await posthawk.emails.get('abc-123-def');

if (data) {
  console.log(data.status);     // 'pending' | 'processing' | 'completed' | 'failed'
  console.log(data.result);     // { messageId: '...', emailLogId: '...' }
}
SDKposthawk.scheduled.list(params?)

List scheduled emails with optional filtering by status and pagination.

Returns

{ data, error }

Parameters

statusstringoptional

Filter by status: scheduled, sent, cancelled, failed

limitnumberoptional

Pagination limit

offsetnumberoptional

Pagination offset

posthawk.scheduled.list(params?)
const { data } = await posthawk.scheduled.list({
  status: 'scheduled',
  limit: 10,
});

// data.data → ScheduledEmail[]
// data.total → number
SDKposthawk.scheduled.get(id)

Get the details of a specific scheduled email by its ID.

Returns

{ data, error }

Parameters

idstringrequired

Scheduled email UUID

posthawk.scheduled.get(id)
const { data } = await posthawk.scheduled.get('scheduled-uuid');

if (data) {
  console.log(data.data.scheduled_for); // ISO 8601 datetime
  console.log(data.data.status);        // 'scheduled' | 'sent' | 'cancelled'
}
SDKposthawk.scheduled.cancel(id)

Cancel a scheduled email before it sends. Only works for emails that have not yet been processed.

Returns

{ data, error }

Parameters

idstringrequired

Scheduled email UUID

posthawk.scheduled.cancel(id)
const { data, error } = await posthawk.scheduled.cancel('scheduled-uuid');
// data.message → "Scheduled email cancelled successfully"
SDKposthawk.scheduled.reschedule(id, params)

Change the send time of a scheduled email. Accepts a Date object or ISO 8601 string.

Returns

{ data, error }

Parameters

idstringrequired

Scheduled email UUID

scheduledForstring | Daterequired

New send time (ISO 8601 or Date object)

posthawk.scheduled.reschedule(id, params)
await posthawk.scheduled.reschedule('scheduled-uuid', {
  scheduledFor: new Date('2026-04-01T10:00:00Z'),
});
SDKposthawk.scheduled.sendNow(id)

Send a scheduled email immediately, before its scheduled time. Internally reschedules to now + 1s so the full audit trail is preserved.

Returns

{ data, error }

Parameters

idstringrequired

Scheduled email UUID

posthawk.scheduled.sendNow(id)
const { data, error } = await posthawk.scheduled.sendNow('scheduled-uuid');
// data.message → "Email rescheduled to send immediately"
SDKposthawk.newsletters.*

Manage newsletters, subscribers, and issues. Create newsletters, add subscribers, draft issues, and send them to all active subscribers.

Returns

{ data, error }

posthawk.newsletters.*
// Create a newsletter
const { data: nl } = await posthawk.newsletters.create({
  slug: 'weekly-digest',
  name: 'Weekly Digest',
  double_opt_in: true,
});

// List all newsletters
const { data: list } = await posthawk.newsletters.list();

// Get a specific newsletter
const { data: newsletter } = await posthawk.newsletters.get('newsletter-id');

// Update settings
await posthawk.newsletters.update('newsletter-id', { status: 'paused' });
SDKposthawk.newsletters.addSubscriber()

Add, list, and manage subscribers for a newsletter. Honors the newsletter's double_opt_in setting. Re-subscribing an existing email is idempotent.

Returns

{ data, error }

posthawk.newsletters.addSubscriber()
// Add a subscriber
const { data: sub } = await posthawk.newsletters.addSubscriber('newsletter-id', {
  email: 'user@example.com',
  name: 'Alex',
  metadata: { source: 'landing-page' },
});

// List subscribers (filterable by status)
const { data } = await posthawk.newsletters.listSubscribers('newsletter-id', {
  status: 'active',
  limit: 50,
});

// Unsubscribe a subscriber
await posthawk.newsletters.unsubscribe('newsletter-id', 'subscriber-id');

// Resend confirmation email
await posthawk.newsletters.resendConfirmation('newsletter-id', 'subscriber-id');

// Permanently delete a subscriber
await posthawk.newsletters.deleteSubscriber('newsletter-id', 'subscriber-id');
SDKposthawk.newsletters.createIssue()

Draft and send newsletter issues. Issues are saved as drafts until you call sendIssue. Schedule an issue with scheduleIssue, cancel a scheduled/in-flight send with cancelIssue, preview with sendTestIssue, and pull delivery + engagement stats with getIssueStats.

Returns

{ data, error }

posthawk.newsletters.createIssue()
// Create a draft issue (body fields are html_body / text_body)
const { data: issue } = await posthawk.newsletters.createIssue('newsletter-id', {
  subject: 'Issue #5: New features',
  preheader: 'Everything shipped this month',
  html_body: '<h1>What shipped</h1><p>...</p>',
  text_body: 'What shipped\n...',
});

// Update the draft before sending
await posthawk.newsletters.updateIssue('newsletter-id', issue.id, {
  subject: 'Issue #5: New features this month',
});

// Send a test render to yourself first
await posthawk.newsletters.sendTestIssue('newsletter-id', issue.id, 'you@yourdomain.com');

// Send to all active subscribers
const { data: sendResult } = await posthawk.newsletters.sendIssue('newsletter-id', issue.id);
// sendResult.recipient_count → number of emails dispatched

// Or schedule it for later (ISO 8601 string)
await posthawk.newsletters.scheduleIssue('newsletter-id', issue.id, '2026-06-20T14:00:00Z');

// Cancel a scheduled or in-flight send
await posthawk.newsletters.cancelIssue('newsletter-id', issue.id);
// → { success, status, alreadyQueued }

// Get the issue (recipient_count, open_count, click_count live on the issue)
const { data: full } = await posthawk.newsletters.getIssue('newsletter-id', issue.id);
// full.recipient_count, full.open_count, full.click_count

// Get detailed per-issue delivery + engagement stats
const { data: stats } = await posthawk.newsletters.getIssueStats('newsletter-id', issue.id);
// stats.delivered, stats.opened, stats.clicked, stats.bounced, ...

// List all issues
const { data: issues } = await posthawk.newsletters.listIssues('newsletter-id', {
  status: 'sent',
});
SDKreact: WelcomeEmail({ name })

Use React Email components directly — the SDK renders them to HTML for you. Pass any React element via the react prop. The SDK dynamically imports @react-email/render at runtime, so the peer dependency is only needed if you use this feature.

react: WelcomeEmail({ name })
import { Posthawk } from 'posthawk';
import { WelcomeEmail } from './emails/welcome';

const posthawk = new Posthawk('ck_live_...');

const { data, error } = await posthawk.emails.send({
  from: 'hello@yourdomain.com',
  to: 'user@example.com',
  subject: 'Welcome aboard!',
  react: WelcomeEmail({ name: 'Alex' }),
});
SDKNext.js / Express / NestJS

The SDK works with any Node.js framework. Here are examples for the most popular ones.

Next.js / Express / NestJS
// ━━━ Next.js — Server Action ━━━━━━━━━━━━━━━━━━━━━━━━━━━
// app/actions/send-email.ts
'use server';
import { Posthawk } from 'posthawk';

const posthawk = new Posthawk(process.env.POSTHAWK_API_KEY!);

export async function sendWelcomeEmail(email: string, name: string) {
  const { data, error } = await posthawk.emails.send({
    from: 'hello@yourdomain.com',
    to: email,
    subject: `Welcome, ${name}!`,
    html: `<h1>Welcome, ${name}!</h1>`,
  });
  if (error) throw new Error(error.message);
  return data;
}

// ━━━ Next.js — API Route ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// app/api/send/route.ts
import { Posthawk } from 'posthawk';
import { NextRequest, NextResponse } from 'next/server';

const posthawk = new Posthawk(process.env.POSTHAWK_API_KEY!);

export async function POST(request: NextRequest) {
  const { to, subject, html } = await request.json();
  const { data, error } = await posthawk.emails.send({
    from: 'hello@yourdomain.com', to, subject, html,
  });
  if (error) return NextResponse.json({ error: error.message }, { status: error.statusCode });
  return NextResponse.json(data);
}

// ━━━ Express ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
import express from 'express';
import { Posthawk } from 'posthawk';

const app = express();
app.use(express.json());
const posthawk = new Posthawk(process.env.POSTHAWK_API_KEY!);

app.post('/send', async (req, res) => {
  const { data, error } = await posthawk.emails.send({
    from: 'hello@yourdomain.com', ...req.body,
  });
  if (error) return res.status(error.statusCode || 500).json({ error: error.message });
  res.json(data);
});

// ━━━ NestJS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
import { Injectable } from '@nestjs/common';
import { Posthawk } from 'posthawk';

@Injectable()
export class EmailService {
  private posthawk = new Posthawk(process.env.POSTHAWK_API_KEY!);

  async send(to: string, subject: string, html: string) {
    return this.posthawk.emails.send({
      from: 'hello@yourdomain.com', to, subject, html,
    });
  }
}
SDK{ data, error }

SDK methods never throw for API errors. Every call returns a discriminated union of { data: T, error: null } or { data: null, error: PosthawkError }. The only case where the SDK throws is a missing API key in the constructor.

{ data, error }
const { data, error } = await posthawk.emails.send({ ... });

if (error) {
  console.error(error.message);    // Human-readable error message
  console.error(error.statusCode); // HTTP status code (e.g. 400, 429)
  return;
}

// data is guaranteed non-null here
console.log(data.jobId);
SDKposthawk.domains.*

Manage sending domains programmatically — add, verify, enable inbound receiving, configure webhooks, and set sending limits. Ideal for SaaS platforms that let their users add custom sending domains.

Returns

{ data, error }

posthawk.domains.*
// Add a domain
const { data, error } = await posthawk.domains.create({
  domain: 'mail.customer.com',
  region: 'us-east-1',
});
// data.dns_records → DNS records to configure

// List all domains
const { data: domains } = await posthawk.domains.list();

// Trigger verification check
const { data: status } = await posthawk.domains.verify('domain-uuid');
// status.sending_enabled → true when verified

// Enable inbound receiving
await posthawk.domains.enableReceiving('domain-uuid');

// Set a webhook for inbound emails
await posthawk.domains.updateWebhook('domain-uuid', {
  webhookUrl: 'https://yourapp.com/api/incoming-email',
});

// Test the webhook
await posthawk.domains.testWebhook('domain-uuid');

// Set per-domain sending limits
await posthawk.domains.updateLimits('domain-uuid', {
  maxDailyEmails: 1000,
  maxMonthlyEmails: 25000,
});

// Remove limits (set to null for unlimited)
await posthawk.domains.updateLimits('domain-uuid', {
  maxDailyEmails: null,
  maxMonthlyEmails: null,
});

// Disable inbound receiving
await posthawk.domains.disableReceiving('domain-uuid');

// Delete a domain (method is named "remove", not "delete")
await posthawk.domains.remove('domain-uuid');
SDKposthawk.validation.validate(email)

Validate a single email address before sending. Pass the address as a plain string. Returns a deliverability decision, a confidence score, and a per-signal checks breakdown (checks may be null on a cached result).

Returns

{ data, error }

posthawk.validation.validate(email)
const { data, error } = await posthawk.validation.validate('user@example.com');

if (data) {
  console.log(data.decision);    // 'deliverable' | 'risky' | 'undeliverable' | 'unknown'
  console.log(data.confidence);  // 'HIGH' | 'MEDIUM' | 'LOW' | 'UNKNOWN'
  console.log(data.checks);      // { validSyntax, validDns, mailboxExists, ... } | null
}
SDKposthawk.emails.batch(messages, options?)

Send up to 100 emails in one call. Pass an array of message objects (same fields as send, minus scheduling/react/idempotency). Each message is validated independently. An optional second argument sets a batch-wide Idempotency-Key.

Returns

{ data, error }

posthawk.emails.batch(messages, options?)
const { data, error } = await posthawk.emails.batch(
  [
    { from: 'hello@yourdomain.com', to: 'a@example.com', subject: 'Hi A', html: '<p>A</p>' },
    { from: 'hello@yourdomain.com', to: 'b@example.com', subject: 'Hi B', templateId: 'welcome', variables: { name: 'B' } },
  ],
  { idempotencyKey: '550e8400-e29b-41d4-a716-446655440000' },
);

// data.results → [{ index, success, jobId?, error? }]
// data.total, data.queued, data.failed
SDKposthawk.contacts.*

Manage workspace contacts: create/upsert, list with filters, get, update, delete, and bulk import. The delete method is named delete (note: domains uses remove).

Returns

{ data, error }

posthawk.contacts.*
// Create or upsert a contact
const { data: contact } = await posthawk.contacts.create({
  email: 'user@example.com',
  name: 'Jane Doe',
  tags: ['newsletter', 'beta'],
  metadata: { source: 'signup-page' },
});

// List contacts (paginated, optional tag + search filters)
const { data } = await posthawk.contacts.list({ tag: 'newsletter', page: 1 });

// Get a single contact
const { data: one } = await posthawk.contacts.get('contact-uuid');

// Update
await posthawk.contacts.update('contact-uuid', { tags: ['newsletter', 'premium'] });

// Unsubscribe
await posthawk.contacts.update('contact-uuid', { unsubscribed: true });

// Delete
await posthawk.contacts.delete('contact-uuid');

// Bulk import (up to 1000) — pass the array positionally
await posthawk.contacts.import([
  { email: 'a@x.com', name: 'A', tags: ['import'] },
  { email: 'b@x.com', name: 'B', tags: ['import'] },
]);
SDKposthawk.suppressions.*

Manage the suppression list. Add an entry with create (reason "manual"), list all entries, and remove with delete. Removing an entry also wipes the soft-bounce counter.

Returns

{ data, error }

posthawk.suppressions.*
// Add a manual suppression
const { data } = await posthawk.suppressions.create({
  email: 'user@example.com',
  notes: 'Requested removal',
});

// List all suppressions (data.entries → Suppression[])
const { data: list } = await posthawk.suppressions.list();

// Remove a suppression by id
await posthawk.suppressions.delete('suppression-uuid');
SDKposthawk.webhooks.*

Manage webhook endpoints for real-time email + newsletter + broadcast events. list() returns a bare array of endpoints. The signing secret is returned on create — store it for signature verification.

Returns

{ data, error }

posthawk.webhooks.*
// Create an endpoint
const { data: hook } = await posthawk.webhooks.create({
  url: 'https://yourapp.com/api/webhooks/posthawk',
  events: ['delivery', 'bounce', 'complaint', 'open', 'click'],
  description: 'Production webhook',
});
// hook.secret → store this for signature verification

// List (returns WebhookEndpoint[])
const { data: endpoints } = await posthawk.webhooks.list();

// Update (e.g. add event types or toggle enabled)
await posthawk.webhooks.update('webhook-uuid', {
  events: ['delivery', 'bounce', 'complaint', 'open', 'click', 'newsletter.issue.sent'],
});

// Send a test event
await posthawk.webhooks.test('webhook-uuid');

// Delete
await posthawk.webhooks.delete('webhook-uuid');
SDKposthawk.broadcasts.*

Create, list, get, send, and cancel one-off broadcasts. create() and get() return a { broadcast } wrapper. Body fields use snake_case (from_email, html_body, recipient_type).

Returns

{ data, error }

posthawk.broadcasts.*
// Create a draft broadcast
const { data } = await posthawk.broadcasts.create({
  name: 'March Update',
  from_email: 'hello@yourdomain.com',
  subject: 'What shipped in March',
  html_body: '<h1>This month at Acme</h1>...',
  recipient_type: 'tag',
  recipient_tag: 'newsletter',
});
const broadcastId = data.broadcast.id;

// List broadcasts (optional status filter + page)
const { data: list } = await posthawk.broadcasts.list({ status: 'draft' });

// Get one
const { data: one } = await posthawk.broadcasts.get(broadcastId);

// Send
const { data: sent } = await posthawk.broadcasts.send(broadcastId);
// sent.queued, sent.message

// Cancel
const { data: cancelled } = await posthawk.broadcasts.cancel(broadcastId);
// cancelled.status, cancelled.alreadyQueued
SDKposthawk.templates.render(params)

Render a template server-side with variable substitution, without sending. Useful for previews or generating HTML for other channels.

Returns

{ data, error }

posthawk.templates.render(params)
const { data } = await posthawk.templates.render({
  templateId: 'welcome-template',
  variables: { firstName: 'John', company: 'Acme' },
});

console.log(data.subject);
console.log(data.html);   // string | null
console.log(data.text);   // string | null
SDKimport type { ... } from 'posthawk'

All types are exported from the package for full type safety in your application. The value exports are Posthawk, PosthawkError, and Validation.

import type { ... } from 'posthawk'
import type {
  // Core
  PosthawkConfig,
  PosthawkResponse,
  // Email
  SendEmailRequest,
  SendEmailResponse,
  EmailJobStatus,
  // Batch
  BatchEmailMessage,
  BatchResultItem,
  BatchResponse,
  SendBatchOptions,
  // Validation
  ValidationConfidence,
  ValidationDecision,
  ValidationChecks,
  ValidateEmailResponse,
  // Scheduled
  ScheduledEmail,
  ScheduledListParams,
  ScheduledListResponse,
  RescheduleRequest,
  // Templates
  RenderTemplateRequest,
  RenderTemplateResponse,
  // Webhooks
  WebhookEventType,
  WebhookEndpoint,
  CreateWebhookRequest,
  UpdateWebhookRequest,
  WebhookEndpointTestResponse,
  // Domains
  CreateDomainRequest,
  Domain,
  DnsRecord,
  SesRegion,
  DomainResponse,
  DomainListResponse,
  DomainDeleteResponse,
  UpdateDomainWebhookRequest,
  UpdateDomainLimitsRequest,
  WebhookTestResponse,
  // Contacts
  Contact,
  ContactListResponse,
  CreateContactRequest,
  UpdateContactRequest,
  ContactImportResponse,
  // Suppressions
  SuppressionReason,
  Suppression,
  SuppressionListResponse,
  CreateSuppressionRequest,
  CreateSuppressionResponse,
  // Newsletters
  Newsletter,
  NewsletterSubscriber,
  NewsletterIssue,
  NewsletterIssueStatus,
  NewsletterListResponse,
  NewsletterSubscriberListResponse,
  NewsletterIssueListResponse,
  CreateNewsletterRequest,
  UpdateNewsletterRequest,
  AddSubscriberRequest,
  CreateIssueRequest,
  UpdateIssueRequest,
  SendIssueResponse,
  CancelIssueResponse,
  SendTestIssueResponse,
  NewsletterIssueStats,
  // Broadcasts
  Broadcast,
  BroadcastStatus,
  CreateBroadcastRequest,
  BroadcastListParams,
  BroadcastListResponse,
  BroadcastSendResponse,
  BroadcastCancelResponse,
} from 'posthawk';