Skip to main content
Requirements: Node.js 20 LTS or later

Installation

npm install ark-email
Or with your preferred package manager:
yarn add ark-email
pnpm add ark-email
bun add ark-email

Quick Start

import Ark from 'ark-email';

const client = new Ark({
  apiKey: process.env.ARK_API_KEY,
});

const email = await client.emails.send({
  from: 'Security <[email protected]>',
  to: ['[email protected]'],
  subject: 'Reset your password',
  html: '<h1>Password Reset</h1><p>Click the link below to reset your password.</p>',
  text: 'Password Reset\n\nClick the link below to reset your password.',
});

console.log(`Email ID: ${email.data.id}`);
console.log(`Status: ${email.data.status}`);

Configuration

Client Options

import Ark from 'ark-email';

const client = new Ark({
  apiKey: 'ark_...',                    // Required (or set ARK_API_KEY env var)
  timeout: 60000,                        // Request timeout in ms (default: 60000)
  maxRetries: 2,                         // Number of retry attempts (default: 2)
  baseURL: 'https://api.arkhq.io/v1',   // API base URL (rarely needed)
  logLevel: 'info',                      // 'off' | 'error' | 'warn' | 'info' | 'debug'
});

Environment Variables

export ARK_API_KEY="ark_live_..."
export ARK_LOG="debug"  # Enable debug logging

TypeScript Support

The SDK is written in TypeScript and exports all types:
import Ark from 'ark-email';
import type {
  EmailSendParams,
  SendEmailResponse,
  Domain,
  Webhook,
} from 'ark-email';

const params: EmailSendParams = {
  from: '[email protected]',
  to: ['[email protected]'],
  subject: 'Hello!',
  html: '<p>Hello</p>',
};

const response: SendEmailResponse = await client.emails.send(params);

API Reference

Emails

Send a single email.
const email = await client.emails.send({
  from: '[email protected]',
  to: ['[email protected]'],
  subject: 'Welcome!',
  html: '<h1>Hello</h1>',
  text: 'Hello',  // Optional but recommended
  cc: ['[email protected]'],  // Optional
  bcc: ['[email protected]'],  // Optional
  replyTo: '[email protected]',  // Optional
  tags: ['welcome', 'onboarding'],  // Optional
  metadata: { userId: '123' },  // Optional
  trackOpens: true,  // Optional
  trackClicks: true,  // Optional
  scheduledAt: '2024-01-20T09:00:00Z',  // Optional - ISO 8601
  attachments: [  // Optional
    {
      filename: 'invoice.pdf',
      content: base64EncodedContent,
      contentType: 'application/pdf',
    },
  ],
  headers: {  // Optional custom headers
    'X-Custom-Header': 'value',
  },
});
Returns: SendEmailResponse with data.id, data.status, etc.
Send multiple emails in a single request.
const result = await client.emails.sendBatch({
  emails: [
    {
      from: '[email protected]',
      to: ['[email protected]'],
      subject: 'Hello User 1',
      html: '<p>Hello!</p>',
    },
    {
      from: '[email protected]',
      to: ['[email protected]'],
      subject: 'Hello User 2',
      html: '<p>Hello!</p>',
    },
  ],
});

for (const email of result.data) {
  console.log(`${email.id}: ${email.status}`);
}
Send a raw MIME message.
const rawMessage = `From: [email protected]
To: [email protected]
Subject: Raw email
Content-Type: text/plain

This is a raw MIME message.`;

const email = await client.emails.sendRaw({
  from: '[email protected]',
  to: ['[email protected]'],
  rawMessage,
});
List emails with filtering and pagination.
const emails = await client.emails.list({
  page: 1,
  perPage: 25,
  status: 'sent',       // Optional filter
  tag: 'welcome',       // Optional filter
});

for (const email of emails.data) {
  console.log(`${email.id}: ${email.subject} - ${email.status}`);
}

// Pagination info
console.log(`Page ${emails.page} of ${emails.totalPages}`);
Get a single email by ID.
const email = await client.emails.retrieve('msg_abc123xyz');

console.log(`Subject: ${email.data.subject}`);
console.log(`Status: ${email.data.status}`);
console.log(`Created: ${email.data.createdAt}`);
Get delivery attempts for an email.
const deliveries = await client.emails.getDeliveries('msg_abc123xyz');

for (const delivery of deliveries.data) {
  console.log(`Attempt at ${delivery.timestamp}: ${delivery.status}`);
  if (delivery.error) {
    console.log(`  Error: ${delivery.error}`);
  }
}
Retry a failed email.
const result = await client.emails.retry('msg_abc123xyz');
console.log(`Retry scheduled: ${result.data.id}`);

Domains

Register a new sending domain.
const domain = await client.domains.create({
  name: 'mail.yourdomain.com',
});

console.log(`Domain ID: ${domain.data.id}`);
console.log('DNS Records to configure:');
for (const record of domain.data.dnsRecords) {
  console.log(`  ${record.type} ${record.name} -> ${record.value}`);
}
List all domains.
const domains = await client.domains.list();

for (const domain of domains.data) {
  const status = domain.verified ? 'Verified' : 'Pending';
  console.log(`${domain.name}: ${status}`);
}
Get domain details.
const domain = await client.domains.retrieve('dom_abc123');
console.log(`Domain: ${domain.data.name}`);
console.log(`Verified: ${domain.data.verified}`);
Trigger DNS verification.
const result = await client.domains.verify('dom_abc123');

if (result.data.verified) {
  console.log('Domain verified successfully!');
} else {
  console.log('DNS records not found. Please check your configuration.');
}
Remove a domain.
await client.domains.delete('dom_abc123');
console.log('Domain deleted');

Suppressions

Add an email to the suppression list.
const suppression = await client.suppressions.create({
  email: '[email protected]',
  reason: 'hard_bounce',
});
Add multiple emails to the suppression list.
const result = await client.suppressions.bulkCreate({
  suppressions: [
    { email: '[email protected]', reason: 'hard_bounce' },
    { email: '[email protected]', reason: 'complaint' },
  ],
});
console.log(`Added ${result.data.createdCount} suppressions`);
List suppressed emails.
const suppressions = await client.suppressions.list({
  page: 1,
  perPage: 50,
  reason: 'hard_bounce',  // Optional filter
});

for (const s of suppressions.data) {
  console.log(`${s.email}: ${s.reason} (added ${s.createdAt})`);
}
Check if an email is suppressed.
try {
  const suppression = await client.suppressions.retrieve('[email protected]');
  console.log(`Suppressed: ${suppression.data.reason}`);
} catch (error) {
  if (error instanceof Ark.NotFoundError) {
    console.log('Email is not suppressed');
  }
  throw error;
}
Remove from suppression list.
await client.suppressions.delete('[email protected]');
console.log('Removed from suppression list');

Webhooks

Create a webhook endpoint.
const webhook = await client.webhooks.create({
  url: 'https://yourapp.com/webhooks/ark',
  events: ['MessageSent', 'MessageBounced', 'MessageLoaded'],
});

console.log(`Webhook ID: ${webhook.data.id}`);
console.log(`Signing Secret: ${webhook.data.signingSecret}`);  // Store this!
List all webhooks.
const webhooks = await client.webhooks.list();

for (const wh of webhooks.data) {
  console.log(`${wh.id}: ${wh.url}`);
  console.log(`  Events: ${wh.events.join(', ')}`);
}
Update webhook configuration.
const webhook = await client.webhooks.update('wh_abc123', {
  events: ['MessageSent', 'MessageBounced'],  // Updated events
});
Send a test event to your webhook.
const result = await client.webhooks.test('wh_abc123', {
  event: 'MessageSent',
});
console.log(`Test result: ${result.data.status}`);
Delete a webhook.
await client.webhooks.delete('wh_abc123');

Tracking

Configure a custom tracking domain.
const tracking = await client.tracking.create({
  domain: 'track.yourdomain.com',
});

console.log('Add this CNAME record:');
console.log(`  ${tracking.data.cnameTarget}`);
List tracking domains.
const trackingDomains = await client.tracking.list();

for (const t of trackingDomains.data) {
  const status = t.verified ? 'Verified' : 'Pending';
  console.log(`${t.domain}: ${status}`);
}
Verify tracking domain DNS.
const result = await client.tracking.verify('trk_abc123');
console.log(`Verified: ${result.data.verified}`);
Remove a tracking domain.
await client.tracking.delete('trk_abc123');

Error Handling

The SDK throws typed errors for different scenarios:
import Ark from 'ark-email';

const client = new Ark();

try {
  const email = await client.emails.send({
    from: '[email protected]',
    to: ['invalid'],
    subject: 'Test',
    html: '<p>Test</p>',
  });
} catch (error) {
  if (error instanceof Ark.BadRequestError) {
    console.log(`Invalid request: ${error.message}`);
    console.log(`Error code: ${error.code}`);
  } else if (error instanceof Ark.AuthenticationError) {
    console.log('Invalid API key');
  } else if (error instanceof Ark.RateLimitError) {
    console.log(`Rate limited. Retry after: ${error.headers?.['retry-after']}`);
  } else if (error instanceof Ark.APIConnectionError) {
    console.log('Network error - check your connection');
  } else if (error instanceof Ark.APIError) {
    console.log(`API error ${error.status}: ${error.message}`);
  }
  throw error;
}

Error Types

Error ClassHTTP StatusDescription
BadRequestError400Invalid request parameters
AuthenticationError401Invalid or missing API key
PermissionDeniedError403Insufficient permissions
NotFoundError404Resource not found
UnprocessableEntityError422Validation error
RateLimitError429Too many requests
InternalServerError5xxServer error
APIConnectionErrorNetwork/connection issue

Advanced Usage

Raw Response Access

Access HTTP headers, status codes, and raw response data:
// Method 1: asResponse() - returns raw Response
const response = await client.emails.send({...}).asResponse();
console.log(response.status);
console.log(response.headers);

// Method 2: withResponse() - returns both parsed data and response
const { data: email, response: raw } = await client.emails.send({...}).withResponse();
console.log(email.data.id);
console.log(raw.headers.get('x-request-id'));

Custom Fetch

Provide your own fetch implementation for proxies or custom behavior:
import Ark from 'ark-email';
import { fetch as undiciFetch } from 'undici';

const client = new Ark({
  fetch: undiciFetch,
});

Request Options

Pass per-request options:
const email = await client.emails.send(
  {
    from: '[email protected]',
    to: ['[email protected]'],
    subject: 'Test',
    html: '<p>Test</p>',
  },
  {
    timeout: 10000,  // Override timeout for this request
    maxRetries: 0,   // Disable retries for this request
    headers: {
      'X-Custom-Header': 'value',
    },
  }
);

Idempotency

Use idempotency keys to safely retry requests:
import { randomUUID } from 'crypto';

const idempotencyKey = randomUUID();

// This request can be safely retried with the same key
const email = await client.emails.send({
  from: '[email protected]',
  to: ['[email protected]'],
  subject: 'Order Confirmation',
  html: '<p>Your order is confirmed.</p>',
  idempotencyKey,
});

Custom Logger

Use your preferred logging library:
import Ark from 'ark-email';
import pino from 'pino';

const logger = pino();

const client = new Ark({
  logger: logger.child({ service: 'ark' }),
});
Supported loggers: pino, winston, bunyan, consola, signale, @std/log.

Accessing Undocumented Endpoints

Make requests to endpoints not yet in the SDK:
const result = await client.post('/some/new/endpoint', {
  body: { key: 'value' },
  query: { param: 'value' },
});

Framework Integration

Express

import express from 'express';
import Ark from 'ark-email';

const app = express();
const ark = new Ark();

app.post('/api/send-email', async (req, res) => {
  try {
    const email = await ark.emails.send({
      from: '[email protected]',
      to: [req.body.to],
      subject: req.body.subject,
      html: req.body.html,
    });
    res.json({ emailId: email.data.id });
  } catch (error) {
    if (error instanceof Ark.APIError) {
      res.status(error.status ?? 500).json({ error: error.message });
    }
    throw error;
  }
});

Next.js (App Router)

// app/api/send-email/route.ts
import { NextResponse } from 'next/server';
import Ark from 'ark-email';

const ark = new Ark();

export async function POST(request: Request) {
  const { to, subject, html } = await request.json();

  try {
    const email = await ark.emails.send({
      from: '[email protected]',
      to: [to],
      subject,
      html,
    });
    return NextResponse.json({ emailId: email.data.id });
  } catch (error) {
    if (error instanceof Ark.APIError) {
      return NextResponse.json(
        { error: error.message },
        { status: error.status ?? 500 }
      );
    }
    throw error;
  }
}

Hono

import { Hono } from 'hono';
import Ark from 'ark-email';

const app = new Hono();
const ark = new Ark();

app.post('/send-email', async (c) => {
  const { to, subject, html } = await c.req.json();

  const email = await ark.emails.send({
    from: '[email protected]',
    to: [to],
    subject,
    html,
  });

  return c.json({ emailId: email.data.id });
});

export default app;

Fastify

import Fastify from 'fastify';
import Ark from 'ark-email';

const fastify = Fastify();
const ark = new Ark();

fastify.post('/send-email', async (request, reply) => {
  const { to, subject, html } = request.body as any;

  const email = await ark.emails.send({
    from: '[email protected]',
    to: [to],
    subject,
    html,
  });

  return { emailId: email.data.id };
});

fastify.listen({ port: 3000 });

NestJS

// ark.service.ts
import { Injectable } from '@nestjs/common';
import Ark from 'ark-email';

@Injectable()
export class ArkService {
  private client: Ark;

  constructor() {
    this.client = new Ark();
  }

  async sendEmail(to: string, subject: string, html: string) {
    return this.client.emails.send({
      from: '[email protected]',
      to: [to],
      subject,
      html,
    });
  }
}

// email.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { ArkService } from './ark.service';

@Controller('email')
export class EmailController {
  constructor(private arkService: ArkService) {}

  @Post('send')
  async send(@Body() body: { to: string; subject: string; html: string }) {
    const email = await this.arkService.sendEmail(body.to, body.subject, body.html);
    return { emailId: email.data.id };
  }
}

Resources