ab-ecosystem-cpass
v1.0.12
Published
JavaScript/TypeScript SDK for the CPaaS CRM gateway: audience and channels
Maintainers
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
- Quick start
- Client options
- Conventions — pagination, search, filters, scopes, errors
- Modules — one reference page per resource
Install
npm install ab-ecosystem-cpassNode 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 });