> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brudcast.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Node.js library

> Manage contacts, segments, campaigns, sending domains, forms and webhooks from Node.js with @brudcast/node, and send email.

`@brudcast/node` is the official Node.js library for the Brudcast API. Anything your
[API key](/developers/api-keys.md) can do in the platform API has a method: manage contacts, lists and
segments, import contacts, run campaigns, read delivery logs and reports, set up sending domains,
handle form submissions and manage webhooks. It also sends email through the
[email send API](/developers/sending/http-api.md) and verifies
[webhook signatures](/developers/webhooks/verifying-signatures.md).

It runs on Node.js 20 or later, Bun and Deno, and has no dependencies.

## Install

```bash theme={"system"}
npm install @brudcast/node
```

## Create a client

```javascript theme={"system"}
import Brudcast from "@brudcast/node";

const brudcast = new Brudcast({
  apiKey: process.env.BRUDCAST_API_KEY,
  sendingKey: process.env.BRUDCAST_SENDING_KEY,
});
```

`apiKey` is your organization API key, used for every call except sending email. `sendingKey` is a
sending key for one of your sending domains, used by `emails.send`. When you leave either out, the
library reads `BRUDCAST_API_KEY` and `BRUDCAST_SENDING_KEY` from the environment.

> [!WARNING]
>
> Use the library on your server only. An API key in browser code lets anyone who opens the page act
> for your organization, so the client refuses to start in a browser.

## What you can do

Each area of the API has its own property on the client. A call needs the scope listed for its
area: the `read` scope to read and the `write` scope to change anything. See
[API keys](/developers/api-keys.md) for the scopes.

| Area | Client properties | Scope |
| - | - | - |
| Contacts and their email, SMS and push channels | `contacts` | `contacts` |
| Lists, segments, tags and custom fields | `contactLists`, `contactSegments`, `contactTags`, `contactCustomFields` | `contacts` |
| Imports, exports and suppressions | `contactImports`, `contactExports`, `suppressions` | `contacts` |
| Campaigns and their results | `campaigns`, `campaigns.reports` | `campaigns` |
| Templates | `templates` | `templates` |
| Delivery logs | `messages` | `messages` |
| Reports | `reports`, `dashboard` | `analytics` |
| Webhook endpoints and deliveries | `webhookEndpoints`, `webhookDeliveries` | `webhooks` |
| Sending domains and dedicated IPs | `sendingDomains`, `ipPools`, `ipPoolRequests` | `domains` |
| Sender addresses, SMS sender IDs and push applications | `senderIdentities`, `pushApplications` | `senders` |
| Mail domains and mailboxes | `mailboxDomains`, `mailboxes` | `mailboxes` |
| Forms, websites and the preference center | `contactForms`, `websites`, `preferenceCenter` | `forms` |
| Form submissions | `contactForms.submissions` | `form_submissions` |

Each method returns the record from the reply's `data`.

## Contacts, lists and segments

```javascript theme={"system"}
const contact = await brudcast.contacts.create({
  firstName: "Jane",
  emails: [{ email: "jane@example.com", isPrimary: true }],
  tags: ["signup-2026"],
});

await brudcast.contactLists.contacts.add(listId, { contactIds: [contact.id] });

const rules = [
  { field: "tag", operator: "is", value: "vip" },
  { field: "last_activity", operator: "in_last", value: 30 },
];

const { contactCount } = await brudcast.contactSegments.previewCount({ matchMode: "all", rules });

const segment = await brudcast.contactSegments.create({
  name: "Engaged VIPs",
  type: "dynamic",
  matchMode: "all",
  rules,
});
```

`previewCount` tells you how many contacts the rules match before you save the segment. See
[Segments](/contacts/segments.md).

## Import contacts from a CSV file

```javascript theme={"system"}
import { createReadStream } from "node:fs";

const upload = await brudcast.contactImports.upload(createReadStream("customers.csv"));

const preview = await brudcast.contactImports.preview(upload.id, {
  fieldMappings: upload.suggestedMappings,
  duplicateHandling: "update",
});

await brudcast.contactImports.create({
  uploadId: upload.id,
  fieldMappings: upload.suggestedMappings,
  duplicateHandling: "update",
  consentConfirmed: true,
});
```

The import runs in the background. Call `contactImports.retrieve` to follow its `status`. See
[Import contacts](/contacts/importing.md).

## Run a campaign

```javascript theme={"system"}
const campaign = await brudcast.campaigns.create({
  name: "October newsletter",
  type: "regular",
  channels: ["email"],
  recipients: { segmentIds: [segment.id] },
  emailContent: {
    fromName: "Example",
    fromEmail: "news@mail.example.com",
    subject: "What's new in October",
    htmlContent: "<h1>Hello {{first_name}}</h1>",
    editorMode: "html",
  },
});

const readiness = await brudcast.campaigns.readiness(campaign.id);

if (readiness.sendable) {
  await brudcast.campaigns.send(campaign.id, {
    sendOption: "schedule",
    scheduledAt: "2026-10-24T09:00:00Z",
  });
} else {
  console.log(readiness.requirements.map((requirement) => requirement.message));
}

const stats = await brudcast.campaigns.reports.statistics(campaign.id);
```

`readiness` returns the same checks as the [send checklist](/campaigns/send-checklist.md), so you can
see what's missing before you send.

## Set up a sending domain

```javascript theme={"system"}
const domain = await brudcast.sendingDomains.create({ domain: "mail.example.com" });

for (const record of domain.domainDnsRecords ?? []) {
  console.log(record.type, record.name, record.value);
}

await brudcast.sendingDomains.verifyDnsRecords(domain.id);
```

Publish the records at your DNS host first. Verification runs in the background; call
`sendingDomains.retrieve` to see the domain's `status`. See
[Add a sending domain](/channels/email/add-a-sending-domain.md).

## Send email

```javascript theme={"system"}
const { jobId } = await brudcast.emails.send(
  {
    from: { name: "Example", address: "hello@mail.example.com" },
    to: [{ address: "jane@example.com" }],
    subject: "Your order is confirmed",
    html: "<p>Thanks for your order.</p>",
  },
  { idempotencyKey: `order-confirmation-${orderId}` },
);
```

Pass an `idempotencyKey` that you store with the order. If your process stops after sending, a
retry with the same key returns the first result instead of sending the email again. Brudcast keeps
each key for 24 hours.

## Page through lists

Awaiting a list returns its first page. Looping over it with `for await` fetches every page in turn:

```javascript theme={"system"}
const page = await brudcast.contacts.list({ status: ["active"], limit: 50 });
console.log(page.meta.total);

for await (const contact of brudcast.contacts.list({ tags: ["vip"] })) {
  console.log(contact.id);
}
```

## Handle errors

A failed call throws an error with the HTTP `status`, the API's `code` and a `requestId`. Quote the
`requestId` when you contact support.

```javascript theme={"system"}
import { APIError, InsufficientScopeError } from "@brudcast/node";

try {
  await brudcast.campaigns.send(campaignId, { sendOption: "now" });
} catch (error) {
  if (error instanceof InsufficientScopeError) {
    console.log(`Add the ${error.requiredScope} scope to the key`);
  } else if (error instanceof APIError && error.code === "E_CAMPAIGN_NOT_SENDABLE") {
    console.log(error.errors);
  } else {
    throw error;
  }
}
```

See [Errors](/developers/errors.md) for every code.

## Retries and timeouts

The library retries a failed call twice, waiting longer each time, or as long as the reply's
`Retry-After` asks. It retries a create only with an idempotency key, which it adds to every `POST`
for you, so a retry can't create the same thing twice. Each attempt times out after 60 seconds.

```javascript theme={"system"}
const brudcast = new Brudcast({ maxRetries: 3, timeout: 20_000 });

await brudcast.contacts.retrieve(contactId, { timeout: 5_000 });
```

## Verify webhooks

Import from `@brudcast/node/webhooks` and pass the raw request body:

```javascript theme={"system"}
import express from "express";
import { constructEvent } from "@brudcast/node/webhooks";

app.post("/webhooks/brudcast", express.raw({ type: "application/json" }), (req, res) => {
  const event = constructEvent({
    payload: req.body,
    signature: req.header("x-webhook-signature-v1"),
    secret: process.env.BRUDCAST_WEBHOOK_SECRET,
  });

  console.log(event.event, event.delivery_id);
  res.sendStatus(204);
});
```

> [!NOTE]
>
> Use `express.raw()` on the webhook route, not `express.json()`. Parsing the body changes its
> bytes, so the signature can never match.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.