@ab-ecosystem/cpaas
1.1.0PublicMessaging & CRMOne CrmClient covers the whole gateway — the audience module (contacts, tags, custom fields, segments, imports and exports), the channels module (WhatsApp onboarding, templates, webhooks, billing and wallet), campaigns (bulk sends to a CSV of numbers, scheduling, per-recipient reporting) and data stores you own. The live chat inbox is a Socket.IO connection, shipped as the /realtime subpath.
$ npm install @ab-ecosystem/cpaas@ab-ecosystem/cpaas
The official TypeScript / JavaScript SDK for the AB Ecosystem CPaaS platform — WhatsApp Business API channels, CRM contacts, bulk campaigns, message templates and a realtime live-chat inbox, behind one typed client.
npm install @ab-ecosystem/cpaas
One CrmClient covers the whole gateway: the audience module (contacts,
tags, custom fields, segments, imports and exports), the channels module
(WhatsApp onboarding, templates, webhooks, billing and wallet), campaigns
(bulk sends to a CSV of numbers, scheduling, per-recipient reporting), and
data stores (a MongoDB you own, registered with the platform). The live chat
inbox is a Socket.IO connection, shipped as the @ab-ecosystem/cpaas/realtime
subpath.
Dependency-free, written in TypeScript, types included — no @types/ package to
install.
Table of contents
- Installation
- Quick start
- Client options
- Core concepts — pagination, filters, scoping, headers
- Guides
- API reference map
- Error handling
- Plan limits and quotas
- TypeScript
- Offline documentation
- For AI coding agents
- FAQ
- Compatibility
- License
Installation
npm install @ab-ecosystem/cpaas
# or
pnpm add @ab-ecosystem/cpaas
# or
yarn add @ab-ecosystem/cpaas
The live chat inbox needs one optional peer dependency. Skip it if you only use the REST client:
npm install socket.io-client
Requirements: Node.js 18 or newer (the SDK uses the global fetch), or any
modern browser. The package ships CommonJS plus .d.ts declarations, so it
imports cleanly from CommonJS, from ESM through Node's interop, and from every
bundler.
Quick start
import { CrmClient } from '@ab-ecosystem/cpaas';
const crm = new CrmClient({
gatewayUrl: 'https://api.example.com',
clientId: 'tpc_xxxxxxxxxxxxxxxx',
authToken: process.env.CRM_TOKEN!,
origin: 'https://app.example.com', // required on Node, ignored in browsers
});
// 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' });
Construct the client once and share it. It holds no per-request state, and every resource runs over the same HTTP client, so the token resolver and the timeout apply everywhere.
Client options
new CrmClient(options: ClientOptions)
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
gatewayUrl | string | yes | — | Base URL of the gateway, no trailing slash. |
clientId | string | yes | — | Sent as the client-id header on every call. |
authToken | string | (() => string | Promise<string>) | yes | — | Bearer token, or a resolver called per request. |
origin | string | on Node | page origin | Sent as the origin header and matched against the token's referer claim. Browsers set this themselves and ignore any override; on Node the gateway refuses the call without it. |
fetch | typeof fetch | no | global fetch | Custom fetch implementation. |
timeoutMs | number | no | 30000 | Per-request timeout in milliseconds. |
Every request carries authorization: Bearer <authToken>, client-id and
origin.
Rotating tokens
Pass a function instead of a string and it is called before every request, so a short-lived token never needs the client rebuilt:
const crm = new CrmClient({
gatewayUrl: 'https://api.example.com',
clientId: 'tpc_xxxxxxxxxxxxxxxx',
authToken: () => auth.getAccessToken(), // sync or async
});
Core concepts
Two pagination shapes
Most lists are offset paginated and answer Paginated<T>:
{ data: T[], pagination: { page: number, rows: number, total: number, pages: number } }
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
reference 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.
Two async iterators walk the pages for you:
for await (const contact of crm.contacts.iterate({ tag: vipTagId })) { /* … */ }
for await (const row of crm.campaignMessages.iterate(campaignId)) { /* … */ }
Object-valued query parameters
filter, range, and the billing list's template / country / campaign
arrays travel as JSON on the query string. Pass real values — the SDK serialises
them:
await crm.billing.list('all', {
range: { start: 1767225600, end: 1769817600 },
country: ['IN', 'US'],
});
The contact filter DSL
One condition language 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.
await crm.contacts.list({
filter: {
conditions: [
{ operator: null, field_id: 'city', field_type: 'text', condition: 'equal to', value: 'Pune' },
],
},
});
Every field, condition and operator is in docs/contact-filter.md.
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.
A team member's visibility is their role's data_scope: own sees the contacts
assigned to them as agent, all sees the account's. Reading a row outside that
scope answers 404, never 403 — a 403 would confirm the row exists, which is
what the scope is hiding.
A contact a team member creates is assigned to that member as its agent,
whatever their data_scope.
Permission scopes
Every method documents the scope its route requires — contacts:read,
channels:write and so on. They are listed for planning; the gateway does not
enforce them yet. Per-key permissions (CRM.CONTACT.CREATE, …) are enforced, and
are listed on each reference page.
Guides
Contacts and the audience
const contact = await crm.contacts.create({
name: { first_name: 'Ada', last_name: 'Lovelace' },
whatsapp: '+91 98765 43210',
tags: [vipTagId],
custom_field_hash: { city: 'Pune', lead_score: 80 },
});
contact.profile.whatsapp.identifier; // '919876543210'
A WhatsApp number is accepted in any shape and stored as digits only, because
that is the form an inbound message carries. One number identifies one contact
per account; a repeat is 409 conflict. A contact holds one identity per
channel.
delete is two-stage and answers 200 both times: the first call trashes
({ status: 'trashed' }, recoverable with update(id, { status: 'live' })), the
second deletes for good ({ status: 'deleted' }).
Reference: docs/audience/contacts.md.
Connect a WhatsApp channel
Channels are provisioned by onboarding, not created through the API. There is
no channels.create. connect() runs the whole Meta Embedded Signup flow from a
browser — it opens the dialog, waits for the return, and completes the channel:
const channel = await crm.channelOnboarding.connect({
type: 'whatsapp',
redirectUri: 'https://app.example.com/channels/connected',
});
A console that drives its own popup can call the steps directly — types(),
url(), pending(), exchange(), complete() — and the popup drivers
openSignup() and awaitSignupReturn() are exported for that.
Reference: docs/channels/onboarding.md.
WhatsApp message templates
await crm.templates.create(channelId, template);
const approved = await crm.templates.list(channelId, { status: 'approved' });
await crm.templates.sync(channelId); // pull Meta's current status
Reference: docs/channels/templates.md.
Send a WhatsApp campaign
A campaign is 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 to reach for when you hold the recipients yourself. The
list does not travel inside the campaign body — a million numbers is not a
request body — so the helper parses the input, encrypts it under a per-file
secret, uploads it through a presigned URL, and creates the campaign pointing at
the file:
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 }],
});
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, splitting recipients across channels or
templates and 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:
await crm.campaigns.trigger(id); // send now; cancel works for ~1 minute
await crm.campaigns.trigger(id, { scheduledAt }); // send then
A send time is refused on create and update, because arming a campaign is
also what records who authorised it.
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
create result is the count written into the file, and it is the one to show a
user.
The audience helpers are exported on their own, so a UI can show "12,480 recipients, 3 duplicates dropped" before anything is uploaded:
import { parseDelimited, resolveAudience } from '@ab-ecosystem/cpaas';
const { headers } = parseDelimited(text);
const { receivers, skipped, duplicates } = resolveAudience({ csv: text, recipientColumn });
Reference: docs/campaign/campaigns.md.
Campaign reporting
const stats = await crm.campaignMessages.stats(campaignId);
const replies = await crm.campaignMessages.replies(campaignId);
The funnel, the per-recipient rows behind it and the replies are all counted off the message rows when asked for — there is no roll-up to wait for and no export job to poll.
Reference: docs/campaign/campaign-messages.md.
Live chat inbox (realtime)
A separate transport, not the REST gateway: the inbox is a Socket.IO connection
with its own origin. Install socket.io-client and import the /realtime
subpath — nothing resolves it if you only use the REST client.
import { Inbox } from '@ab-ecosystem/cpaas/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 uploads are the one part of the inbox that is HTTP, because an upload is a request with an answer. Upload through the REST client, 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' });
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.
Reference: docs/inbox/connection.md.
Scheduled messages
A send with a time, plus a condition checked just before it goes out — which is what lets a drip stop itself when a lead converts.
await crm.scheduledMessages.createSeries(input);
await crm.scheduledMessages.sendNow(id);
Reference: docs/audience/scheduled-messages.md.
Data stores
A MongoDB you own, registered with the platform. The platform holds the credential — encrypted — and the schemas you declare over it. It holds none of your data: every record call is a live round trip to your own cluster, and deleting a store forgets our side of it while leaving your database untouched.
const store = await crm.dataStores.create({ /* connection */ });
await crm.dataStoreCollections.create(store.id, { /* schema */ });
await crm.dataStoreRecords.insert(store.id, collectionId, { /* document */ });
Reference: docs/data-store.md.
API reference map
Every resource is a property on CrmClient. Each row names its reference page,
which ships inside the package under docs/.
Audience — /v1/audience
| Property | Methods | Reference |
|---|---|---|
crm.contacts | list get create update delete iterate | docs/audience/contacts.md |
crm.contactTags | list get create update delete | docs/audience/contact-tags.md |
crm.contactNotes | list get create update delete | docs/audience/contact-notes.md |
crm.customFields | list get create update delete | docs/audience/custom-fields.md |
crm.cannedReplies | list get create update delete — addressed by id or name | docs/audience/canned-replies.md |
crm.segments | list get create update delete | docs/audience/segments.md |
crm.crmTriggers | list get create update delete logs | docs/audience/crm-triggers.md |
crm.imports | list get create delete | docs/audience/imports.md |
crm.exports | list get create delete | docs/audience/exports.md |
crm.webhooks | list get create update delete | docs/audience/webhooks.md |
crm.scheduledMessages | list get create createSeries createRepeating reschedule cancel cancelSeries sendNow | docs/audience/scheduled-messages.md |
crm.notifications | subscribe unsubscribe devices | docs/audience/notifications.md |
crm.inboxMedia | upload createUpload | docs/inbox/media.md |
Channels — /v1/channels
| Property | Methods | Reference |
|---|---|---|
crm.channels | list get updateDetails disable rotateToken getProp setProp | docs/channels/channels.md |
| — | every prop that getProp / setProp accepts | docs/channels/channel-props.md |
crm.channelOnboarding | types url pending exchange complete connect | docs/channels/onboarding.md |
crm.templates | list get create update delete sync | docs/channels/templates.md |
crm.channelWebhooks | list get create update delete | docs/channels/webhooks.md |
crm.billing | list stats | docs/channels/billing.md |
crm.wallet | list | docs/channels/wallet.md |
crm.activity | list | docs/channels/activity.md |
Campaigns — /v1/campaigns
| Property | Methods | Reference |
|---|---|---|
crm.campaigns | list get create createBulk createGroup uploadReceivers update trigger schedule cancel delete | docs/campaign/campaigns.md |
crm.campaignMessages | stats list iterate replies | docs/campaign/campaign-messages.md |
Data stores
| Property | Methods | Reference |
|---|---|---|
crm.dataStores | regions list get create update remove test | docs/data-store.md |
crm.dataStoreCollections | list get create update remove | docs/data-store.md |
crm.dataStoreRecords | insert list search get update bulkUpdate remove bulkRemove | docs/data-store.md |
Realtime — @ab-ecosystem/cpaas/realtime
| Member | Methods | Reference |
|---|---|---|
new Inbox(options) | ready on ping | docs/inbox/connection.md |
inbox.chats | list get delete transfer bulkTransfer archive unarchive block markAsRead markAsUnread pauseAutomation resumeAutomation close done pauseTimer updateTags update assignBot | docs/inbox/chats.md |
inbox.messages | list get send sendMedia delete retryDownload | docs/inbox/messages.md |
inbox.referrals(contactId) | click-to-WhatsApp ad payloads | docs/inbox/referrals.md |
inbox.sessions(contactId) | per-channel reply windows | docs/inbox/sessions.md |
Error handling
| Class | When |
|---|---|
CrmConfigError | Thrown client-side, before any request, when gatewayUrl, clientId or authToken is missing or empty. |
CrmApiError | The gateway answered a non-2xx status. Carries status, code, message and details. |
import { CrmApiError, CrmConfigError } from '@ab-ecosystem/cpaas';
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);
}
}
| Status | Meaning |
|---|---|
402 | Subscription or quota — see below. |
403 | The role does not hold the permission key (A403), or the plan does not cover the feature (E403). |
404 not_found | No such row for this caller — including a row hidden by data_scope. |
409 conflict | A unique value is taken: a contact's number, a name, a slug. |
422 | Body or query failed validation; details lists the failing paths. |
Plan limits and quotas
Every call needs an active subscription, and 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 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 } |
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 come back 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);
}
}
TypeScript
Declarations ship with the package — there is no @types/ to install. Every
input and output is an exported type, so prefer importing one over hand-writing
an object literal:
import type {
Contact,
CreateContactInput,
ListContactsQuery,
Campaign,
Channel,
Paginated,
Page,
} from '@ab-ecosystem/cpaas';
const query: Partial<ListContactsQuery> = { search: 'acme', rows: 50 };
const page: Paginated<Contact> = await crm.contacts.list(query);
strict mode is supported and expected.
Offline documentation
The full reference ships inside the package, so it is readable without a network and greppable by an editor or an agent:
node_modules/@ab-ecosystem/cpaas/docs/
├── audience/ contacts, tags, notes, custom fields, canned replies,
│ segments, triggers, imports, exports, webhooks,
│ scheduled messages, notifications
├── campaign/ campaigns, campaign-messages
├── channels/ channels, channel-props, onboarding, templates,
│ webhooks, billing, wallet, activity
├── inbox/ connection, chats, messages, media, referrals, sessions
├── contact-filter.md
└── data-store.md
Each page lists every action with its route, permission key, request body, query parameters and response shape.
For AI coding agents
Read this section before generating code against this SDK.
Entry points. There are exactly two:
import { CrmClient } from '@ab-ecosystem/cpaas'; // REST, everything
import { Inbox } from '@ab-ecosystem/cpaas/realtime'; // Socket.IO live chat
Ground truth. The reference pages under
node_modules/@ab-ecosystem/cpaas/docs/ are authoritative and complete — read
the page for the resource you are touching before writing the call. The map above
says which file. The type declarations under dist/*.d.ts are the second source.
Do not invent a method that appears in neither.
Invariants that are easy to get wrong:
- Creating a campaign does not send it.
create/createBulk/createGroupreturn a draft.campaigns.trigger(id)is what sends. - Recipients never go in the campaign body. Use
createBulkorcreateGroup, which upload the list and point the campaign at the file. campaign.receiversis0on a fresh campaign. Show thereceiversfrom the create result instead.- There is no
channels.create. A channel is provisioned throughcrm.channelOnboardingand retired withchannels.disable— there is no delete either. channels.getnever 404s. It answers{ data }, and{ data: {} }when nothing matched. Check withif (!('id' in data)).- Two pagination shapes.
Paginated<T>({ data, pagination }) for most lists;Page<T>({ data, nextCursor }) for contact notes and audience webhooks only. originis required on Node and must match the token's referer claim. Omitting it is a rejected request, not a warning.- Pass objects, not JSON strings, for
filter,rangeand the billing array filters. The SDK serialises them. - A hidden row is
404, not403. Do not branch on403to detect "exists but forbidden". tagsandcustom_field_hashreplace on update, they do not merge. Read the row first if you mean to add one tag to the existing set.contacts.deleteis two-stage and returns200both times. Readresult.status—'trashed', then'deleted'.- The realtime subpath needs
socket.io-clientinstalled by the app. It is an optional peer dependency, and nothing resolves it otherwise.
Error handling to generate: catch CrmApiError and branch on err.status
and err.code; catch CrmConfigError only around construction. Never parse
err.message.
Do not hand-write request URLs or call fetch directly — every route is a
method, and the client adds the three required headers.
FAQ
Does this SDK send WhatsApp messages?
Yes — one-to-many through crm.campaigns, one-to-one through the realtime inbox
(inbox.messages.send / sendMedia), and on a schedule through
crm.scheduledMessages.
How do I connect a WhatsApp Business number?
crm.channelOnboarding.connect({ type: 'whatsapp', redirectUri }) runs Meta's
Embedded Signup flow end to end from the browser. See
docs/channels/onboarding.md.
Does it work in the browser?
Yes. Browsers set the origin header themselves, so that option is only needed
on Node. Treat any token you ship to a browser as public.
Does it work with ESM, Next.js, Vite or a bundler? Yes. The package is CommonJS with type declarations, which Node's ESM interop and every major bundler consume without configuration.
Do I need socket.io-client?
Only for the live chat inbox. It is an optional peer dependency.
Can I bring my own fetch, or set a timeout?
Yes — fetch and timeoutMs on ClientOptions.
How do I handle an expiring token?
Pass a function as authToken. It is called before every request, and on every
socket connect for the Inbox.
What happens when a plan limit is hit?
A 402 CrmApiError with code: 'quota_exceeded' and
details: { resource, limit, used }.
Compatibility
| Node.js | 18 or newer |
| Browsers | any with fetch |
| Module format | CommonJS + .d.ts — works from ESM and all bundlers |
| Runtime dependencies | none |
| Optional peer | socket.io-client ^4.7.0, for /realtime only |
License
MIT
Every resource is a namespace on the client. This is the full surface of @ab-ecosystem/cpaas at v1.1.0.
Audience
/v1/audience
| Resource | Methods |
|---|---|
crm.contacts | list · get · create · update · delete · iteratedocs/audience/contacts.md |
crm.contactTags | list · get · create · update · deletedocs/audience/contact-tags.md |
crm.contactNotes | list · get · create · update · deletedocs/audience/contact-notes.md |
crm.customFields | list · get · create · update · deletedocs/audience/custom-fields.md |
crm.cannedReplies | list · get · create · update · delete — by id or namedocs/audience/canned-replies.md |
crm.segments | list · get · create · update · deletedocs/audience/segments.md |
crm.crmTriggers | list · get · create · update · delete · logsdocs/audience/crm-triggers.md |
crm.imports | list · get · create · deletedocs/audience/imports.md |
crm.exports | list · get · create · deletedocs/audience/exports.md |
crm.webhooks | list · get · create · update · deletedocs/audience/webhooks.md |
crm.scheduledMessages | list · get · create · createSeries · createRepeating · reschedule · cancel · cancelSeries · sendNowdocs/audience/scheduled-messages.md |
crm.notifications | subscribe · unsubscribe · devicesdocs/audience/notifications.md |
crm.inboxMedia | upload · createUploaddocs/inbox/media.md |
Channels
/v1/channels
| Resource | Methods |
|---|---|
crm.channels | list · get · updateDetails · disable · rotateToken · getProp · setPropdocs/channels/channels.md |
crm.channelOnboarding | types · url · pending · exchange · complete · connectdocs/channels/onboarding.md |
crm.templates | list · get · create · update · delete · syncdocs/channels/templates.md |
crm.channelWebhooks | list · get · create · update · deletedocs/channels/webhooks.md |
crm.billing | list · statsdocs/channels/billing.md |
crm.wallet | listdocs/channels/wallet.md |
crm.activity | listdocs/channels/activity.md |
Campaigns
/v1/campaigns
| Resource | Methods |
|---|---|
crm.campaigns | list · get · create · createBulk · createGroup · uploadReceivers · update · trigger · schedule · cancel · deletedocs/campaign/campaigns.md |
crm.campaignMessages | stats · list · iterate · repliesdocs/campaign/campaign-messages.md |
Data stores
a MongoDB you own, registered with the platform
| Resource | Methods |
|---|---|
crm.dataStores | regions · list · get · create · update · remove · testdocs/data-store.md |
crm.dataStoreCollections | list · get · create · update · removedocs/data-store.md |
crm.dataStoreRecords | insert · list · search · get · update · bulkUpdate · remove · bulkRemovedocs/data-store.md |
Realtime
@ab-ecosystem/cpaas/realtime
| Resource | Methods |
|---|---|
new Inbox(options) | ready · on · pingdocs/inbox/connection.md |
inbox.chats | list · get · delete · transfer · bulkTransfer · archive · unarchive · block · markAsRead · markAsUnread · pauseAutomation · resumeAutomation · close · done · pauseTimer · updateTags · update · assignBotdocs/inbox/chats.md |
inbox.messages | list · get · send · sendMedia · delete · retryDownloaddocs/inbox/messages.md |
inbox.referrals(contactId) | click-to-WhatsApp ad payloadsdocs/inbox/referrals.md |
inbox.sessions(contactId) | per-channel reply windowsdocs/inbox/sessions.md |
Install
$ npm install @ab-ecosystem/cpaas$ pnpm add @ab-ecosystem/cpaas$ yarn add @ab-ecosystem/cpaasOptional peer dependency: socket.io-client ^4.7.0 (optional)
Entry points
import { CrmClient } from '@ab-ecosystem/cpaas'; // REST, everything
import { Inbox } from '@ab-ecosystem/cpaas/realtime'; // Socket.IO live chat
Quickstart
import { CrmClient } from '@ab-ecosystem/cpaas';
const crm = new CrmClient({
gatewayUrl: 'https://api.example.com',
clientId: 'tpc_xxxxxxxxxxxxxxxx',
authToken: process.env.CRM_TOKEN!,
origin: 'https://app.example.com', // required on Node, ignored in browsers
});
// 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' });
Error classes
| Class | When it is thrown |
|---|---|
CrmConfigError | Thrown client-side, before any request, when gatewayUrl, clientId or authToken is missing or empty. |
CrmApiError | The gateway answered a non-2xx status. Carries status, code, message and details. |
Compatibility
- Node.js
- >=18
- Module formats
- CommonJS + .d.ts
- Runtime dependencies
- none
- TypeScript
- Types bundled — no @types package
Invariants that are easy to get wrong
Read these before generating code against cpaas. Each one is behaviour the API guarantees, not a style preference.
- 01
Creating a campaign does not send it — create / createBulk / createGroup return a draft, and campaigns.trigger(id) is what sends.
- 02
Recipients never go in the campaign body. Use createBulk or createGroup, which upload the list and point the campaign at the file.
- 03
campaign.receivers is 0 on a fresh campaign, because the audience resolves at send time. Show the receivers from the create result instead.
- 04
There is no channels.create. A channel is provisioned through crm.channelOnboarding and retired with channels.disable — there is no delete either.
- 05
channels.get never 404s. It answers { data }, and { data: {} } when nothing matched — check with if (!('id' in data)).
- 06
Two pagination shapes: Paginated<T> for most lists, Page<T> with a nextCursor for contact notes and audience webhooks only.
- 07
origin is required on Node and must match the token’s referer claim. Omitting it is a rejected request, not a warning.
- 08
Pass objects, not JSON strings, for filter, range and the billing array filters — the SDK serialises them.
- 09
A hidden row is 404, not 403. Do not branch on 403 to detect "exists but forbidden".
- 10
tags and custom_field_hash replace on update, they do not merge. Read the row first if you mean to add one tag to the existing set.
- 11
contacts.delete is two-stage and returns 200 both times — read result.status for ‘trashed’, then ‘deleted’.
- 12
The realtime subpath needs socket.io-client installed by the app. It is an optional peer dependency, and nothing resolves it otherwise.
Highlights
- One client, four modules
- Audience, channels, campaigns and data stores are all properties on a single CrmClient. Construct it once and share it — it holds no per-request state, so the token resolver and the timeout apply everywhere.
- Bulk campaigns that scale
- createBulk parses your CSV, encrypts it under a per-file secret, uploads through a presigned URL and creates the campaign pointing at the file. A million numbers is not a request body.
- Realtime live chat inbox
- A Socket.IO transport on the /realtime subpath. Requests are promises; pushes are what other agents and contacts did — so one handler per kind of change is enough.
- Meta Embedded Signup, end to end
- channelOnboarding.connect() opens the dialog, waits for the return and completes the channel. The individual steps and the popup drivers are exported for consoles that drive their own window.
- A filter DSL shared by five surfaces
- One condition language powers contacts.list, saved segments, CRM triggers, exports and scheduled-message conditions. Learn it once.
- Docs inside the package
- The full reference ships under node_modules/@ab-ecosystem/cpaas/docs/ — readable without a network, greppable by your editor or your coding agent.
What you can build with cpaas
- Build your own WhatsApp CRM dashboard on the audience and channels modules
- Drive bulk WhatsApp campaigns from a CSV, with every column a per-recipient template variable
- Embed a live-chat agent console with assignment, transfer, tags and canned replies
- Run drip sequences that stop themselves when a lead converts
- Register your own MongoDB as a data store and keep every record on your cluster
- Preview "12,480 recipients, 3 duplicates dropped" before anything is uploaded
Other packages in @ab-ecosystem
@ab-ecosystem/aistore-admin
The headless ecommerce admin API, fully typed
v0.1.0 · ISC · 465.5 kB
@ab-ecosystem/aistore-storefront
Catalog, cart, checkout and search for any frontend
v0.1.0 · ISC · 262.5 kB