npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@brudcast/node

v0.1.2

Published

The official Node.js library for the Brudcast API

Readme

Brudcast Node.js library

npm

The official server-side library for the Brudcast API. Everything your organization does in the Brudcast dashboard, from your own code:

  • Audience: contacts with every channel they can be reached on, lists, rule-based segments, tags, custom fields, CSV imports and exports, consent records and suppressions.
  • Campaigns: email, SMS and push, with A/B tests, readiness checks, credit estimates, scheduling, previews and test sends.
  • Transactional messaging: email through the send API, and the delivery log of every email and SMS.
  • Analytics: campaign reports, engagement, deliverability, channel performance, bounce classification and report exports.
  • Sending setup: sending domains with DNS and DKIM checks, dedicated IP pools, sender identities, push applications, WhatsApp business numbers and templates, and hosted mailboxes.
  • Forms: signup forms and pages, their fields, submissions, popup placements, websites and the preference center.
  • Webhooks: manage endpoints, retry deliveries, and verify signatures.

Built for production use:

  • TypeScript types for every request and reply, taken from the API's own models
  • Automatic pagination with for await
  • Safe automatic retries, with an idempotency key on every POST
  • Typed errors carrying the API's error code and request id
  • No runtime dependencies; Node.js 20 and later, Bun and Deno

Install

npm install @brudcast/node

Quick start

import Brudcast from '@brudcast/node';

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

const contact = await brudcast.contacts.create({
  firstName: 'Ada',
  emails: [{ email: '[email protected]', isPrimary: true }],
});

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

The library takes two credentials, both from the Brudcast dashboard:

| Option | Environment variable | Used for | | ------------ | ---------------------- | ------------------------------------------ | | apiKey | BRUDCAST_API_KEY | Everything except sending email (bk_...) | | sendingKey | BRUDCAST_SENDING_KEY | emails.send, for one sending domain |

An API key only reaches what its scopes allow (see What you can do); a call outside them fails with InsufficientScopeError, which names the scope to add. Billing, the wallet and API key management stay in the dashboard.

The client runs on your server only: it refuses to start in a browser, where it would expose your key.

What you can do

Every endpoint an organization API key can reach has a method. Each area needs its read scope to read and its write scope to change.

| Area | Client properties | Highlights | Scopes | | ------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | Contacts | contacts | create, update, bulk create, delete, archive and add to list; activity timeline; statistics; emails, sms, whatsapp, telegram, imessage and pushNotifications channels; consentRecords | contacts | | Audiences | contactLists, contactSegments, contactTags, contactCustomFields | list membership and bulk operations; dynamic and static segments with preview counts, snapshots and refresh | contacts | | Imports and exports | contactImports, contactExports | CSV upload with suggested mappings, dry-run preview, error report; exports with a download link | contacts | | Suppressions | suppressions | add, list and lift suppressed addresses | contacts | | Campaigns | campaigns, campaigns.reports | email, SMS and push; A/B tests, recipient estimates, readiness, credit estimate, send or schedule, pause, resume, cancel, duplicate, preview, test send; statistics, journeys, trends and performance | campaigns | | Templates | templates, whatsapp.templates | reusable content with placeholders, resolve, duplicate; approved WhatsApp templates | templates | | Transactional | emails, messages | send email (sending key); delivery logs for email and SMS | messages | | Analytics | reports, reports.exports, dashboard | overview, engagement, deliverability, channel performance, bounce classification; CSV and PDF exports | analytics | | Webhooks | webhookEndpoints, webhookDeliveries | endpoints, test events, secret rotation, delivery log and retry | webhooks | | Sending domains | sendingDomains, ipPools, ipPoolRequests | DNS records and verification, DKIM, tracking settings; dedicated IP pools and requests | domains | | Senders and apps | senderIdentities, pushApplications, whatsapp | sender addresses, SMS sender IDs and their coverage; push apps with credential rotation and test sends; WhatsApp business numbers and templates | senders | | Mailboxes | mailboxDomains, mailboxes | mail domains, mailboxes, aliases, password resets, suspension | mailboxes | | Forms | contactForms, websites, preferenceCenter | forms and pages, fields, publish, analytics, popup placements, websites, preference center settings | forms | | Form submissions | contactForms.submissions, contactForms.uploads | submit from your server, list, resend confirmation, download uploads | form_submissions |

A tour

Each example is checked against the library's types in examples/readme-tour.ts.

Contacts and lists

const contact = await brudcast.contacts.create({
  firstName: 'Ada',
  emails: [{ email: '[email protected]', isPrimary: true }],
  smsNumbers: [{ phoneNumber: '8031234567', countryCode: '234' }],
  tags: ['signup-2026'],
  customFields: { plan: 'pro' },
});

await brudcast.contactLists.contacts.add(listId, { contactIds: [contact.id] });
await brudcast.contacts.update(contact.id, {
  language: 'en',
  timezone: 'Africa/Lagos',
});

for await (const event of brudcast.contacts.activity(contact.id)) {
  console.log(event.type, event.campaignName, event.occurredAt);
}

Segments

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

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

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

for await (const member of brudcast.contactSegments.contacts.list(segment.id)) {
  console.log(member.id);
}

Importing a CSV

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',
});
console.log(`${preview.newCount} new, ${preview.duplicateCount} to update`);

const run = await brudcast.contactImports.create({
  uploadId: upload.id,
  fieldMappings: upload.suggestedMappings,
  duplicateHandling: 'update',
  listName: 'Customers',
  consentConfirmed: true,
});

const { status } = await brudcast.contactImports.retrieve(run.id);

Campaigns

const campaign = await brudcast.campaigns.create({
  name: 'October newsletter',
  type: 'regular',
  channels: ['email', 'sms'],
  recipients: { segmentIds: [segment.id] },
  emailContent: {
    fromName: 'Acme',
    fromEmail: '[email protected]',
    subject: 'What is new in October',
    htmlContent: '<h1>Hello {{first_name}}</h1>',
    editorMode: 'html',
  },
  smsContent: {
    body: 'Our October picks are live: acme.com/oct',
    senderId: 'ACME',
  },
});

const readiness = await brudcast.campaigns.readiness(campaign.id);
if (!readiness.sendable) {
  console.log(readiness.requirements.map((requirement) => requirement.message));
}

const cost = await brudcast.campaigns.creditEstimate(campaign.id);
if (readiness.sendable && cost.sufficient) {
  await brudcast.campaigns.send(campaign.id, {
    sendOption: 'schedule',
    scheduledAt: '2026-10-24T09:00:00Z',
  });
}

Reporting

const stats = await brudcast.campaigns.reports.statistics(campaign.id);
console.log(stats.delivered, stats.openRate, stats.clickRate);

for await (const row of brudcast.campaigns.reports.performance({
  from: '2026-10-01',
})) {
  console.log(row.name, row.openRate);
}

const deliverability = await brudcast.reports.deliverability({
  from: '2026-10-01',
  to: '2026-10-31',
});
console.log(
  deliverability.kpis.bounceRate,
  deliverability.kpis.blacklistStatus,
);

Transactional email and delivery logs

const { jobId } = await brudcast.emails.send(
  {
    from: { name: 'Acme', address: '[email protected]' },
    to: [{ address: '[email protected]' }],
    subject: `Order ${order.id} confirmed`,
    html: '<p>Thanks for your order.</p>',
  },
  { idempotencyKey: `order-confirmation-${order.id}` },
);

const message = await brudcast.messages.retrieve(jobId);
console.log(message.deliveryAttempts.map((attempt) => attempt.status));

for await (const sms of brudcast.messages.list({
  channel: 'sms',
  status: 'failed',
})) {
  console.log(sms.recipient.maskedNumber, sms.category);
}

Pass an idempotencyKey you store with the message: if your process crashes or times out after sending, retrying with the same key returns the first result instead of sending twice. Keys are honoured for 24 hours.

Sending domains

const domain = await brudcast.sendingDomains.create({
  domain: 'mail.acme.com',
});

for (const record of domain.domainDnsRecords ?? []) {
  console.log(record.type, record.name, record.value); // publish these at your DNS host
}

await brudcast.sendingDomains.verifyDnsRecords(domain.id);
const dkim = await brudcast.sendingDomains.dkims.verify(domain.id);

Form submissions from your server

const schema = await brudcast.contactForms.schema(formId);
console.log(schema.fields.map((field) => field.key));

await brudcast.contactForms.submissions.create(formId, {
  values: {
    email: '[email protected]',
    first_name: 'Ada',
    interests: ['product', 'events'],
  },
  context: { ipAddress: '203.0.113.7' },
});

Webhook endpoints

const endpoint = await brudcast.webhookEndpoints.create({
  url: 'https://acme.com/webhooks/brudcast',
  events: ['push.token_invalidated'],
});
console.log('store this secret:', endpoint.secret);

for await (const delivery of brudcast.webhookDeliveries.list({
  status: ['failed'],
})) {
  await brudcast.webhookDeliveries.retry(delivery.id);
}

Library behaviour

Pagination

List methods return a page you can await, or iterate across every page:

const page = await brudcast.contacts.list({ status: ['active'], limit: 50 });
page.data;
page.meta.total;

for await (const contact of brudcast.contacts.list({ tags: ['vip'] })) {
  // fetches the next page as needed
}

Logs that only grow (messages, webhook deliveries, contact activity) page by cursor the same way. Offset pages shift when rows are added or removed during iteration; pin createdAtTo to the start of a long run to avoid skipping rows.

Errors

Every failure is a BrudcastError. A reply from the API is an APIError with its status, the API's code, the field errors of a validation failure, and the requestId to quote to support:

import {
  APIError,
  InsufficientScopeError,
  RateLimitError,
} from '@brudcast/node';

try {
  await brudcast.campaigns.send(id, { sendOption: 'now' });
} catch (error) {
  if (error instanceof APIError && error.code === 'E_CAMPAIGN_NOT_SENDABLE') {
    console.log(error.errors); // what the campaign is missing
  } else if (error instanceof InsufficientScopeError) {
    console.log(`the key needs ${error.requiredScope}`);
  } else {
    throw error;
  }
}

| Status | Class | | ------ | -------------------------- | | 400 | BadRequestError | | 401 | AuthenticationError | | 402 | InsufficientCreditsError | | 403 | PermissionDeniedError | | 403 | InsufficientScopeError | | 404 | NotFoundError | | 409 | ConflictError | | 422 | UnprocessableEntityError | | 429 | RateLimitError | | 5xx | InternalServerError | | - | APIConnectionError | | - | APITimeoutError | | - | APIUserAbortError |

Most domain failures share status 400; tell them apart by error.code.

Retries and timeouts

Failed attempts are retried twice by default, with exponential backoff, or after the reply's Retry-After when it has one:

  • GET, PUT and DELETE: on a timeout, a dropped connection, a 408, a 429 or a 5xx.
  • POST: the same, because every POST carries an Idempotency-Key (your own, or one the library generates per call), so a retry cannot act twice.
  • PATCH and file uploads: only when the request cannot have reached the API (a refused connection, or a 429).
const brudcast = new Brudcast({ maxRetries: 3, timeout: 20_000 });

await brudcast.contacts.retrieve(id, { maxRetries: 0, timeout: 5_000 });
await brudcast.contacts.list({}, { signal: AbortSignal.timeout(10_000) });

The timeout applies to each attempt; signal cancels across all of them.

Raw responses

const { data, response } = await brudcast.contacts.retrieve(id).withResponse();
response.headers.get('x-ratelimit-remaining');
response.headers.get('x-request-id');

Uploads and downloads

Uploads take a File, Blob, Buffer, Uint8Array or stream. Export downloads (contactExports.download, reports.exports.download) return a short-lived signed url to fetch without an Authorization header.

Verifying webhooks

Verify deliveries with the webhooks entry point, against the raw request body:

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!,
    });

    if (event.event === 'push.token_invalidated') {
      event.data.tokenId;
    }

    res.sendStatus(204);
  },
);

constructEvent checks the HMAC-SHA256 signature over <t>.<raw body> in constant time and refuses signatures older than five minutes (tolerance changes it). A body parsed by express.json() can never match its signature; the library says so when it is given one. Use event.delivery_id to drop duplicate deliveries.

Configuration

| Option | Default | | ------------------------- | --------------------------------------- | | apiKey | BRUDCAST_API_KEY | | sendingKey | BRUDCAST_SENDING_KEY | | baseURL | BRUDCAST_BASE_URL, then production | | mailingBaseURL | BRUDCAST_MAILING_BASE_URL, then prod. | | timeout | 60 seconds per attempt | | maxRetries | 2 | | defaultHeaders | none | | fetch | the global fetch | | dangerouslyAllowBrowser | false |

Requirements

Node.js 20 or later. Bun and Deno work too; the webhooks entry point uses node:crypto.

Documentation

The guide at docs.brudcast.com/developers/nodejs-library walks through setup, and the API reference documents every endpoint the library calls.

License

MIT