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.
npm install posthawkInstall 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 posthawknew 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
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);
}posthawk.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
fromstringrequiredSender email address (must be from a verified domain)
tostring | string[]requiredRecipient email address(es)
ccstring | string[]optionalCC recipient(s)
bccstring | string[]optionalBCC recipient(s)
subjectstringrequiredEmail subject line
htmlstringoptionalHTML email body
textstringoptionalPlain text email body
reactReactElementoptionalReact Email component (requires @react-email/render)
templateIdstringoptionalPosthawk template ID
variablesRecord<string, string>optionalTemplate variable substitution
headersRecord<string, string>optionalCustom email headers (e.g. List-Unsubscribe)
scheduledForstring | DateoptionalSchedule for later delivery (ISO 8601 or Date object)
timezonestringoptionalIANA timezone for scheduled time
replyTostringoptionalReply-to email address
metadataRecord<string, unknown>optionalCustom metadata attached to the email
tagsRecord<string, unknown>optionalCustom tags for filtering and search
idempotencyKeystringoptionalSent as the Idempotency-Key header for safe retries (response cached 24h). Retried POSTs are only auto-retried by the SDK when this is set.
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',
});posthawk.emails.get(jobId)Get the delivery status of a previously queued email by its job ID.
Returns
{ data, error }
Parameters
jobIdstringrequiredJob ID returned from the send method
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: '...' }
}posthawk.scheduled.list(params?)List scheduled emails with optional filtering by status and pagination.
Returns
{ data, error }
Parameters
statusstringoptionalFilter by status: scheduled, sent, cancelled, failed
limitnumberoptionalPagination limit
offsetnumberoptionalPagination offset
const { data } = await posthawk.scheduled.list({
status: 'scheduled',
limit: 10,
});
// data.data → ScheduledEmail[]
// data.total → numberposthawk.scheduled.get(id)Get the details of a specific scheduled email by its ID.
Returns
{ data, error }
Parameters
idstringrequiredScheduled email UUID
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'
}posthawk.scheduled.cancel(id)Cancel a scheduled email before it sends. Only works for emails that have not yet been processed.
Returns
{ data, error }
Parameters
idstringrequiredScheduled email UUID
const { data, error } = await posthawk.scheduled.cancel('scheduled-uuid');
// data.message → "Scheduled email cancelled successfully"posthawk.scheduled.reschedule(id, params)Change the send time of a scheduled email. Accepts a Date object or ISO 8601 string.
Returns
{ data, error }
Parameters
idstringrequiredScheduled email UUID
scheduledForstring | DaterequiredNew send time (ISO 8601 or Date object)
await posthawk.scheduled.reschedule('scheduled-uuid', {
scheduledFor: new Date('2026-04-01T10:00:00Z'),
});posthawk.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
idstringrequiredScheduled email UUID
const { data, error } = await posthawk.scheduled.sendNow('scheduled-uuid');
// data.message → "Email rescheduled to send immediately"react: 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.
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' }),
});Next.js / Express / NestJSThe SDK works with any Node.js framework. Here are examples for the most popular ones.
// ━━━ 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,
});
}
}{ 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.
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);posthawk.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 }
// 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');posthawk.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 }
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
}posthawk.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 }
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.failedposthawk.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 }
// 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'] },
]);posthawk.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 }
// 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');posthawk.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 }
// 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');posthawk.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 }
// 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.alreadyQueuedposthawk.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 }
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 | nullimport 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 {
// 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';