Skip to main content

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

OptionTypeRequiredDefaultDescription
apiKeystringYes--Your Optail API key.
baseUrlstringNohttps://api.optail.ioAPI base URL. Override for self-hosted instances.
timeoutnumberNo30000Request 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

FieldTypeRequiredDescription
tostring | string[]YesRecipient email(s).
fromstringYesSender email address.
subjectstringYesEmail subject.
htmlstringConditionalHTML body.
textstringConditionalPlain text body.
templateIdstringConditionalTemplate UUID to render.
variablesRecord<string, unknown>NoTemplate variables.
replyTostringNoReply-to address.
headersRecord<string, string>NoCustom email headers.
tagsstring[]NoTags for routing and analytics.
metadataRecord<string, unknown>NoArbitrary metadata.
trackOpensbooleanNoEnable open tracking.
trackClicksbooleanNoEnable click tracking.

At least one of html, text, or templateId must be provided.

Returns: SendEmailResponse

FieldTypeDescription
messageIdstringUnique 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

FieldTypeDescription
resultsArray<{ 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[]

FieldTypeDescription
idstringTemplate ID.
namestringTemplate name.
tagsstring[]Template tags.
currentVersionIdstring | undefinedActive 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

FieldTypeDescription
suppressedbooleanWhether the email is suppressed.
groupsstring[]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

ParameterTypeDescription
payloadstring | BufferRaw request body (not parsed JSON).
signaturestringValue of the X-Optail-Signature header.
secretstringThe 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

ClassHTTP StatusCodeDescription
AuthenticationError401AUTHENTICATION_ERRORInvalid or missing API key.
ValidationError422VALIDATION_ERRORRequest body failed validation.
RateLimitError429RATE_LIMIT_EXCEEDEDToo many requests. Has retryAfter property (seconds).
NotFoundError404NOT_FOUNDRequested resource does not exist.
OptailErrorvariesvariesBase 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';