# Node.js SDK

> Send email and manage contacts from Node.js with the official, fully typed Nexiom Connect SDK.

`@nexiom/connect` is the official SDK for Node.js. It is fully typed, ships ESM and CommonJS builds, has no runtime dependencies, and retries safely for you.

## Install

Node.js 22 or later is required.

```shell
npm install @nexiom/connect
```

## Initialize the client

Create one client on your server and reuse it. Load the API key from your environment; never ship it to the browser.

```typescript
import { NexiomConnect } from '@nexiom/connect';

const apiKey = process.env.NEXIOM_API_KEY;
if (!apiKey) {
  throw new Error('NEXIOM_API_KEY is required');
}

export const nexiom = new NexiomConnect({ apiKey });
```

| Option | Type | Description |
| --- | --- | --- |
| `apiKey` | string · required | Your API key, starting with `nc_`. |
| `timeout` | number · optional | Total deadline per call in milliseconds, including retries. Default `30000`. |
| `maxRetries` | number · optional | Extra attempts for reads and idempotent sends. Default `2`. |
| `baseUrl` | string · optional | API root. Default `https://api-connect.nxiom.com/api`. |
| `fetch` | function · optional | A Fetch-compatible implementation, for instrumentation or tests. |

## Send an email

```typescript
const { data, error } = await nexiom.emails.send(
  {
    from: 'hello@mail.example.com',
    fromName: 'Acme',
    to: 'reader@example.com',
    subject: 'Your receipt',
    html: '<p>Thanks for your order.</p>',
    text: 'Thanks for your order.',
  },
  { idempotencyKey: `receipt-${order.id}` },
);

if (error) {
  throw error;
}

console.log(data.messageId, data.deliveryIds);
```

Pass an `idempotencyKey` that identifies the email in your system. If you omit it, the SDK generates one per call, which protects its own retries but not a retry of your whole operation. See [Sending email](/docs/learn/sending#idempotency-keys).

To send a template, pass `templateId` and `templateVariables` instead of `subject` and content.

## Schedule an email

Scheduling needs `@nexiom/connect` 0.2.1 or later. Pass `scheduledAt` as a `Date` or an ISO 8601 timestamp with an offset, up to 30 days ahead. Keep the returned `messageId` to move or cancel the send before it goes out.

```typescript
const DAY = 24 * 60 * 60 * 1000;

const { data, error } = await nexiom.emails.send({
  from: 'hello@mail.example.com',
  to: 'reader@example.com',
  subject: 'Your trial ends soon',
  text: 'Your trial ends soon.',
  scheduledAt: new Date(Date.now() + DAY),
});

if (error) {
  throw error;
}

// Move it one day later, or cancel it before it is sent.
await nexiom.emails.reschedule(data.messageId, { scheduledAt: new Date(Date.now() + 2 * DAY) });
await nexiom.emails.cancel(data.messageId);
```

See [Reschedule](/docs/api/emails/reschedule) and [Cancel](/docs/api/emails/cancel) for the rules.

## Read email logs

`emails.list` and `emails.get` need 0.2.1 or later and a full-access key. `list` pages with a cursor; `get` takes a delivery ID, not a message ID.

```typescript
const { data: page, error } = await nexiom.emails.list({ status: 'bounced', limit: 50 });

if (error) {
  throw error;
}

const [first] = page.items;

if (first) {
  const { data: delivery, error: getError } = await nexiom.emails.get(first.id);

  if (getError) {
    throw getError;
  }

  console.log(delivery.recipient, delivery.delivery_reason);
}

// nextCursor is null on the last page.
if (page.nextCursor) {
  await nexiom.emails.list({ status: 'bounced', cursor: page.nextCursor });
}
```

See [List emails](/docs/api/emails/list) and [Retrieve an email](/docs/api/emails/retrieve) for every field.

## Manage contacts

Define a contact property before you store a value for it.

```typescript
const { error: propertyError } = await nexiom.contacts.properties.create({ name: 'plan', type: 'string' });

if (propertyError) {
  throw propertyError;
}

const { data: contact, error } = await nexiom.contacts.create({
  email: 'reader@example.com',
  firstName: 'Sam',
  properties: { plan: 'pro' },
});

await nexiom.contacts.update(contact.id, {
  email: 'reader@example.com',
  emailStatus: 'unsubscribed',
});

const { data: page } = await nexiom.contacts.list({ search: 'example.com', limit: 50 });
```

`contacts.get`, `contacts.delete`, and `contacts.properties` are available too. See the [Contacts API](/docs/api/contacts/create) for every field.

## Send a WhatsApp message

`messaging.send` sends on every messaging channel; `channel` selects the fields, and TypeScript checks them for that channel. For WhatsApp, send an approved template from your connected number, named by its phone number in `from`. Requires 0.4.0 or later and a key with the `whatsapp.send` permission.

```typescript
const { data, error } = await nexiom.messaging.send(
  {
    channel: 'whatsapp',
    from: '+213555000111',
    to: '+213555123456',
    template: {
      id: 'wat_example',
      variables: { body: { 1: 'Amina', 2: 'A-1234' } },
    },
  },
  { idempotencyKey: 'order-A-1234-shipped' },
);

if (data) {
  const message = await nexiom.messaging.get(data.id);
  console.log(message.data?.status);
}
```

See [Sending WhatsApp messages](/docs/whatsapp/sending).

## Verify webhooks

`webhooks.constructEventAsync` (0.4.0 or later) checks a webhook request's signature, with the runtime's Web Crypto API, and its timestamp, then resolves to the typed event. Pass the raw body, not parsed JSON.

```typescript
const event = await nexiom.webhooks.constructEventAsync(
  rawBody,
  request.headers,
  process.env.NEXIOM_WEBHOOK_SECRET,
);

if (event.type === 'email.bounced') {
  console.log(event.data.recipient);
}
```

It rejects with `NexiomWebhookVerificationError` when the request is not genuine. See [Verify webhook requests](/docs/webhooks/verify-webhooks-requests).

## Handle errors

Every method resolves to `{ data, error, response }` instead of throwing for API failures. Check `error` before using `data`.

```typescript
const { data, error, response } = await nexiom.emails.send(message, { idempotencyKey });

if (error) {
  // error.kind: 'api' | 'network' | 'timeout' | 'aborted' | 'protocol'
  console.error(error.status, error.code, error.message, error.requestId);
  return;
}
```

| Property | Description |
| --- | --- |
| `kind` | `api` for an error response; otherwise the transport failure type. |
| `status` | HTTP status, when the API responded. |
| `code` | Machine-readable error code, when present. |
| `requestId` | The request's correlation ID. Include it when contacting support. |

Invalid arguments — for example a missing `from` — throw a `NexiomValidationError` before any request is made.

## Retries and timeouts

The SDK retries reads, sends and cancels on `408`, `429`, and `5xx` responses and on network failures, honouring `Retry-After`. Sends are retried with the same idempotency key, so a retry never sends twice. Other writes, such as creating a contact or rescheduling an email, are not retried automatically. If `Retry-After` asks for a longer wait than the remaining timeout, the SDK returns that error straight away.

Override per call with `{ timeout, maxRetries, signal }`.

---

Source: https://connect.nxiom.com/docs/examples/nodejs
