Node.js SDK
Official Node.js client for the Optail API. Works with Node.js 18+ and any runtime that supports the Fetch API (Bun, Deno, Cloudflare Workers).
Installation
npm install @optail/node
Configuration
import { Optail } from '@optail/node';
const optail = new Optail({
apiKey: process.env.OPTAIL_API_KEY, // Required
baseUrl: 'https://api.optail.io', // Optional, default shown
timeout: 30000, // Optional, ms, default: 30000
});
Config Options
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
apiKey | string | Yes | -- | Your Optail API key. |
baseUrl | string | No | https://api.optail.io | API base URL. Override for self-hosted instances. |
timeout | number | No | 30000 | Request timeout in milliseconds. |
Methods
send(params)
Send a single email. Returns immediately with a message ID (the email is queued for async delivery).
const result = await optail.send({
to: 'user@example.com',
from: 'hello@yourdomain.com',
subject: 'Welcome!',
html: '<h1>Hello World</h1>',
tags: ['welcome'],
});
console.log(result.messageId); // "a1b2c3d4-..."
console.log(result.status); // "queued" | "suppressed"
Parameters: SendEmailParams
| Field | Type | Required | Description |
|---|---|---|---|
to | string | string[] | Yes | Recipient email(s). |
from | string | Yes | Sender email address. |
subject | string | Yes | Email subject. |
html | string | Conditional | HTML body. |
text | string | Conditional | Plain text body. |
templateId | string | Conditional | Template UUID to render. |
variables | Record<string, unknown> | No | Template variables. |
replyTo | string | No | Reply-to address. |
headers | Record<string, string> | No | Custom email headers. |
tags | string[] | No | Tags for routing and analytics. |
metadata | Record<string, unknown> | No | Arbitrary metadata. |
trackOpens | boolean | No | Enable open tracking. |
trackClicks | boolean | No | Enable click tracking. |
At least one of html, text, or templateId must be provided.
Returns: SendEmailResponse
| Field | Type | Description |
|---|---|---|
messageId | string | Unique message ID. |
status | "queued" | "suppressed" | Whether the email was queued or suppressed. |
sendBatch(params)
Send multiple emails in a single request.
const result = await optail.sendBatch({
messages: [
{
to: 'alice@example.com',
from: 'hello@yourdomain.com',
subject: 'Hello Alice',
html: '<p>Hi Alice!</p>',
},
{
to: 'bob@example.com',
from: 'hello@yourdomain.com',
subject: 'Hello Bob',
html: '<p>Hi Bob!</p>',
},
],
});
for (const item of result.results) {
if ('messageId' in item) {
console.log(`Queued: ${item.messageId}`);
} else {
console.error(`Error: ${item.error}`);
}
}
Returns: BatchSendResponse
| Field | Type | Description |
|---|---|---|
results | Array<{ messageId: string; status: string } | { error: string }> | Result for each message. |
listTemplates()
List all templates for the organization.
const templates = await optail.listTemplates();
for (const tpl of templates) {
console.log(`${tpl.name} (${tpl.id})`);
}
Returns: TemplateResponse[]
| Field | Type | Description |
|---|---|---|
id | string | Template ID. |
name | string | Template name. |
tags | string[] | Template tags. |
currentVersionId | string | undefined | Active version ID. |
getTemplate(templateId)
Get a single template by ID.
const template = await optail.getTemplate('tpl-uuid');
console.log(template.name);
checkSuppression(email)
Check if an email address is suppressed.
const result = await optail.checkSuppression('user@example.com');
if (result.suppressed) {
console.log(`Suppressed in groups: ${result.groups.join(', ')}`);
}
Returns: SuppressionCheckResponse
| Field | Type | Description |
|---|---|---|
suppressed | boolean | Whether the email is suppressed. |
groups | string[] | Unsubscribe group IDs where the email is suppressed. |
addSuppression(email, groupId)
Add a suppression for an email address in a specific unsubscribe group.
await optail.addSuppression('user@example.com', 'grp-uuid');
Returns void on success.
removeSuppression(email, groupId)
Remove a suppression, allowing emails to that address again.
await optail.removeSuppression('user@example.com', 'grp-uuid');
Returns void on success.
Webhook Verification
verifyWebhookSignature(payload, signature, secret)
Verify the HMAC-SHA256 signature of an incoming webhook request. Uses timing-safe comparison to prevent timing attacks.
import { verifyWebhookSignature } from '@optail/node';
const isValid = verifyWebhookSignature(
rawBody, // string | Buffer -- the raw request body
signature, // string -- X-Optail-Signature header
webhookSecret, // string -- the secret from webhook creation
);
Parameters
| Parameter | Type | Description |
|---|---|---|
payload | string | Buffer | Raw request body (not parsed JSON). |
signature | string | Value of the X-Optail-Signature header. |
secret | string | The webhook signing secret (whsec_...). |
Returns
boolean -- true if the signature is valid.
Express Example
import express from 'express';
import { verifyWebhookSignature } from '@optail/node';
import type { WebhookEvent } from '@optail/node';
const app = express();
app.post(
'/webhooks/optail',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-optail-signature'] as string;
const isValid = verifyWebhookSignature(
req.body,
signature,
process.env.OPTAIL_WEBHOOK_SECRET!,
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const event: WebhookEvent = JSON.parse(req.body.toString());
console.log(`Event: ${event.type}, Message: ${event.messageId}`);
res.sendStatus(200);
},
);
Error Handling
The SDK throws typed errors that you can catch and handle:
import {
Optail,
OptailError,
AuthenticationError,
ValidationError,
RateLimitError,
NotFoundError,
} from '@optail/node';
try {
await optail.send({ ... });
} catch (error) {
if (error instanceof AuthenticationError) {
// 401 -- Invalid or missing API key
console.error('Auth failed:', error.message);
} else if (error instanceof ValidationError) {
// 422 -- Request validation failed
console.error('Invalid request:', error.message);
} else if (error instanceof RateLimitError) {
// 429 -- Too many requests
console.error('Rate limited, retry after:', error.retryAfter, 'seconds');
} else if (error instanceof NotFoundError) {
// 404 -- Resource not found
console.error('Not found:', error.message);
} else if (error instanceof OptailError) {
// Any other API error
console.error(`Error ${error.status}: ${error.message} (${error.code})`);
}
}
Error Classes
| Class | HTTP Status | Code | Description |
|---|---|---|---|
AuthenticationError | 401 | AUTHENTICATION_ERROR | Invalid or missing API key. |
ValidationError | 422 | VALIDATION_ERROR | Request body failed validation. |
RateLimitError | 429 | RATE_LIMIT_EXCEEDED | Too many requests. Has retryAfter property (seconds). |
NotFoundError | 404 | NOT_FOUND | Requested resource does not exist. |
OptailError | varies | varies | Base class for all SDK errors. Has status, code, and message. |
TypeScript Types
All types are exported for use in your application:
import type {
OptailConfig,
SendEmailParams,
SendEmailResponse,
BatchSendParams,
BatchSendResponse,
TemplateResponse,
SuppressionCheckResponse,
WebhookEvent,
} from '@optail/node';