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

ab-ecosystem-cpass

v1.0.12

Published

JavaScript/TypeScript SDK for the CPaaS CRM gateway: audience and channels

Readme

ab-ecosystem-cpass

JavaScript/TypeScript client for the CRM gateway. Covers the audience module (contacts and everything hanging off them) and the channels module (connections, templates, webhooks and the log feeds).

Install

npm install ab-ecosystem-cpass

Node 18 or newer. The package is dependency-free and ships its own types; it uses the global fetch, or one you pass in.

Quick start

import { CrmClient } from 'ab-ecosystem-cpass';

const crm = new CrmClient({
  gatewayUrl: 'https://api.example.com',
  clientId: 'tpc_xxxxxxxxxxxxxxxx',
  authToken: '<ecosystem bearer token>',
});

// Audience
const contacts = await crm.contacts.list({ search: 'acme', rows: 50 });
const tag = await crm.contactTags.create({ name: 'VIP', color: '#f59e0b' });
await crm.contacts.update(contacts.data[0].id, { tags: [tag.id] });

// Channels
const channels = await crm.channels.list({ type: 'whatsapp', active: true });
const templates = await crm.templates.list(channels.data[0].id, { status: 'approved' });

Every request carries authorization: Bearer <authToken>, client-id and origin.

Client options

| Option | Required | Description | | --- | --- | --- | | gatewayUrl | yes | Base URL of the gateway, without a trailing slash. | | clientId | yes | Sent as the client-id header. | | authToken | yes | A string, or a function returning one per request — sync or async. | | origin | on Node | Sent as the origin header, and must match the token's referer claim. Browsers set this themselves and ignore any override; on Node the gateway rejects the call without it. | | fetch | no | Custom fetch implementation. Defaults to the global one. | | timeoutMs | no | Request timeout in milliseconds. Default 30000. |

Node

const crm = new CrmClient({
  gatewayUrl: 'https://api.example.com',
  clientId: 'tpc_xxxxxxxxxxxxxxxx',
  authToken: process.env.CRM_TOKEN!,
  origin: 'https://app.example.com',
});

Refreshing tokens

Pass a function and it is called before every request, so a rotating token never needs the client rebuilt:

const crm = new CrmClient({
  gatewayUrl: 'https://api.example.com',
  clientId: 'tpc_xxxxxxxxxxxxxxxx',
  authToken: () => auth.getAccessToken(),
});

Conventions

Two kinds of pagination

Most lists are offset paginated and answer Paginated<T>:

{ data: T[], pagination: { page, rows, total, pages } }

Send { page, rows, search }. page is 1-based and defaults to 1; rows defaults to 25 and caps at 100; search is trimmed and must be 1–120 characters. Which fields search matches is per resource and is stated on each page.

Two endpoints — contact notes and audience webhooks — are cursor paginated and answer Page<T>:

{ data: T[], nextCursor: string | null }

Send { limit, cursor }. limit defaults to 50 and caps at 100; pass the previous response's nextCursor to continue, and stop when it is null. These two page through rows that are appended constantly, where an offset would shift under a caller mid-walk.

crm.contacts.iterate(query) walks every page of the contact list for you.

Query values that are objects

filter, range, and the billing list's template / country / campaign arrays are sent as JSON on the query string. Pass them as real values — the SDK serialises them:

await crm.billing.list('all', {
  range: { start: 1767225600, end: 1769817600 },
  country: ['IN', 'US'],
});

The contact filter

One condition DSL is shared by five places: contacts.list({ filter }), a saved Segment, a CRM trigger's filters, an export's config.filter, and a scheduled message's conditions. It has its own page: contact filter.

Scoping

Audience rows belong to an account, and a request carries two identities: the caller, and the account they act for. Reads match either; writes land on the caller's own. Audience webhooks are the exception — they are scoped by tenant.

Scopes

Every method documents the scope its route requires, such as contacts:read or channels:write. They are listed for planning; the gateway does not enforce them yet.

Errors

| Error | When | | --- | --- | | CrmConfigError | Thrown client-side, before any request, when gatewayUrl, clientId or authToken is missing or empty. | | CrmApiError | The gateway returned a non-2xx status. Carries status, code, message and details. |

import { CrmApiError, CrmConfigError } from 'ab-ecosystem-cpass';

try {
  await crm.contacts.get(id);
} catch (err) {
  if (err instanceof CrmConfigError) console.error('bad SDK setup:', err.message);
  else if (err instanceof CrmApiError) console.error(err.status, err.code, err.message, err.details);
}

Common codes: 422 for a body or query that fails validation (details lists the paths), 404 not_found, 409 conflict for a name or slug already taken.

Plan limits

Every call needs an active subscription, and the metered resources are capped at what the plan includes. Both refusals are 402, told apart by code:

| Code | When | details | | --- | --- | --- | | subscription_required | The account has no subscription, or it has lapsed. Any call. | — | | not_included | The plan includes none of that resource. | { resource } | | quota_exceeded | The plan's allowance is used up. | { resource, limit, used } |

What counts, and against which inclusion:

| resource | Counted as | Inclusion | | --- | --- | --- | | contact | one contact | contact_18472 — 1 unit = 1000 contacts | | canned_reply | one reply | canned_reply_14172 | | scheduled_series | one series, whatever its length — a one-off schedule is free | series_91922 | | data_store | one registered store | data_store_52360 | | campaign | one campaign | campaign_73018 | | super_field | one custom field | super_field_29689 | | whatsapp_channel | one live WhatsApp channel | whatsapp_channels_28335 |

Counts are of what exists now, so deleting a row gives the allowance back. limit and used are returned on quota_exceeded so a panel can say how far over the account is without a second call.

try {
  await crm.campaigns.create(input);
} catch (err) {
  if (err instanceof CrmApiError && err.status === 402) {
    // err.code: 'subscription_required' | 'not_included' | 'quota_exceeded'
    console.error(err.message, err.details);
  }
}

Modules

One page per resource, every action with its payload, query and response. The same pages ship inside the package, so they are readable offline at node_modules/ab-ecosystem-cpass/docs/.

Audience — /v1/audience

| Resource | Page | Actions | | --- | --- | --- | | crm.contacts | contacts | list get create update delete iterate | | crm.contactTags | contact tags | list get create update delete | | crm.contactNotes | contact notes | list get create update delete | | crm.customFields | custom fields | list get create update delete | | crm.cannedReplies | canned replies | list get create update delete — addressed by id or name | | crm.segments | segments | list get create update delete | | crm.crmTriggers | CRM triggers | list get create update delete logs | | crm.imports | import history | list get create delete | | crm.exports | export history | list get create delete | | crm.webhooks | audience webhooks | list get create update delete | | crm.scheduledMessages | scheduled messages | list get create createSeries createRepeating reschedule cancel cancelSeries sendNow | | crm.notifications | push notifications | subscribe unsubscribe devices — the devices a user wants notified | | crm.dataStores | data store | list get create update remove test — a MongoDB you own, registered with the platform | | crm.dataStoreCollections | data store | list get create update remove — the schemas declared over it | | crm.dataStoreRecords | data store | insert list search get update bulkUpdate remove bulkRemove — documents in your own database |

Channels — /v1/channels

| Resource | Page | Actions | | --- | --- | --- | | crm.channels | channels | list get updateDetails disable rotateToken getProp setProp | | — | channel properties | every prop getProp/setProp accepts | | crm.channelOnboarding | connecting a channel | types url exchange complete connect | | crm.templates | templates | list get create update delete sync | | crm.channelWebhooks | channel webhooks | list get create update delete | | crm.billing | conversation billing | list stats | | crm.wallet | wallet ledger | list | | crm.activity | activity log | list |

Live chat inbox — Socket.IO

A separate transport, not the REST gateway: the inbox is a socket connection with its own origin. Install socket.io-client alongside the SDK and import the /realtime subpath — nothing resolves it if you only use the REST client.

import { Inbox } from 'ab-ecosystem-cpass/realtime';

const inbox = new Inbox({ url: 'https://inbox.example.com', clientId, token: () => accessToken });
await inbox.ready();

const { contacts } = await inbox.chats.list({ filter: { assignment: 'me', unread: 'unread' } });
inbox.on('message.received', (message) => append(message));

// Media: upload straight to storage, then send the link.
const link = await crm.inboxMedia.upload(file, { kind: 'image' });
await inbox.messages.sendMedia({ channel, contact, kind: 'image', link, caption: 'Your order' });

| Resource | Page | Actions | | --- | --- | --- | | — | connecting | handshake, requests vs pushes, rooms, errors, permissions | | inbox.chats | chats | list get delete transfer bulkTransfer archive unarchive block markAsRead markAsUnread pauseAutomation resumeAutomation close done pauseTimer updateTags update assignBot | | inbox.messages | messages | list get send sendMedia delete retryDownload | | crm.inboxMedia | media | upload createUpload — presigned uploads straight to storage | | inbox.referrals | referrals | click-to-WhatsApp ad payloads | | inbox.sessions | chat sessions | per-channel reply windows |

Requests are promises; pushes are what other agents and contacts did. The same event name can be both — an archive you performed and one another agent performed arrive identically, so one handler per kind of change is enough.

Campaigns

One message sent to many people. The audience is a single filters field with five modes, chosen by the shape of the array: [] for every contact, a list of filter conditions for a segment, [{numbers, hash}] for a bulk list, [{type: 'channel'|'template', ...}] for a group campaign split into buckets, or [{type: 'status'|'selection', ...}] to re-target an earlier campaign.

createBulk is the one worth knowing about. Hand it numbers or a CSV and it does the whole bulk path — the recipient list does not travel inside the campaign, because a million numbers is not a request body:

const { campaign, receivers, skipped, duplicates } = await crm.campaigns.createBulk({
  name: 'Diwali offer',
  audience: { csv: await file.text(), recipientColumn: 'phone' },
  messages: [{ channel: { id: channelId }, message: template }],
});

It parses the input, encrypts the list under a per-file secret, uploads it through a presigned URL, and creates the campaign pointing at the file. Every CSV column becomes a per-recipient template variable. Rows with no usable number and repeated numbers are dropped and counted — skipped and duplicates — rather than passed over silently.

createGroup is the bucketed form: it splits recipients across channels or templates, uploading one file per bucket.

await crm.campaigns.createGroup({
  name: 'Diwali offer',
  distributeBy: 'channel',
  buckets: [
    { channel: channelA, audience: { rows: north, recipientColumn: 'phone' } },
    { channel: channelB, audience: { rows: south, recipientColumn: 'phone' } },
  ],
  messages: [{ channel: { id: channelA }, message: template }],
});

Creating a campaign does not send it. Every create returns a draft: pending, with no scheduledAt. crm.campaigns.trigger(id) sends it now (with about a minute in which cancel still works), and crm.campaigns.trigger(id, { scheduledAt }) sends it then. A send time is refused on create and update, because arming a campaign is also what records who authorised it.

Note that campaign.receivers is 0 on a campaign you just created: the audience is resolved when the campaign sends, not when it is built. The receivers on the result is the count written into the file, and it is the one to show a user.

| Resource | Page | Actions | | --- | --- | --- | | crm.campaigns | campaigns | list get create createBulk createGroup uploadReceivers update trigger schedule cancel delete — creating does not send; trigger is what does | | crm.campaignMessages | campaign reporting | stats list iterate replies — /v1/campaigns/:id/report, counted off the per-recipient rows |

The audience helpers are exported too, so a UI can show "12,480 recipients, 3 duplicates dropped" before anything is uploaded:

import { parseDelimited, resolveAudience } from 'ab-ecosystem-cpass';

const { headers } = parseDelimited(text);
const { receivers, skipped, duplicates } = resolveAudience({ csv: text, recipientColumn });