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

@enochmk/cbs-client

v2.12.2

Published

CBS (Core Billing System) SOAP API client for AirtelTigo

Readme

@enochmk/cbs-client

TypeScript SOAP client for AirtelTigo Huawei CBS R25 customer, account, subscriber, balance, offering, lifecycle, and transaction operations.

Install

npm install @enochmk/[email protected]

The package includes the compiled dist directory, integration guides in docs, and release notes in CHANGELOG.md. The repository contains unit tests, a Postman collection, and a guarded live-CBS status-change test script.

Version 2.12.2 adds typed Recent Usage records and retains paged account-transfer, transaction, recharge, and refund queries alongside adjustBalances() from 2.11.0. See the billing log integration guide for request options, pagination, returned data, and error handling.

Configure the client

Keep CBS credentials in your application's secret manager or environment. Never put credentials in source code, tests, Postman exports, or committed .env files.

import { CbsClient } from '@enochmk/cbs-client';

const client = new CbsClient({
  baseUrl: process.env.CBS_BASE_URL!,
  username: process.env.CBS_USERNAME!,
  password: process.env.CBS_PASSWORD!,
  rejectUnauthorized: process.env.CBS_REJECT_UNAUTHORIZED === 'true',
  timeout: 30_000,
});

rejectUnauthorized defaults to false for deployments using an internal CBS certificate. Set it to true when the CBS certificate chain is trusted. The .env.example file contains placeholders only.

Shared request options

Every request accepts optional CBS request-header overrides:

await client.queryCustomerInfo('270118755', {
  messageSeq: 'htc-query-001',
  version: 1,
  beId: '101',
  operatorId: '101',
  accessMode: 3,
  msgLanguageCode: 2002,
  timeType: 1,
  remoteAddress: '10.0.0.10',
  remark: 'Customer support lookup',
});

Every outbound MessageSeq is generated by the client as CRM_<UUID> so CBS can identify requests originating from CRM. Caller-provided messageSeq values do not override the outbound CBS identifier.

Use CbsRequestDefaults, PaymentModeCode, and SubscriberStatusCode instead of repeating stable numeric constants. MSISDNs accept 9, 10, or 12 digits and are normalized to the CBS 9-digit primary identity format.

The HTC application owns the identifier format sent to CBS: individual customers use IND00000000001, enterprise customers use ENT00000000001, accounts use ACC-{Customer}-1, and subscribers use SUB-{Customer}-1. These application keys are resource identifiers; subscriber lifecycle requests still use the MSISDN as PrimaryIdentity.

Customer operations

Create a customer

createCustomer supports individual and organization information, customer basic information, properties, addresses, sales information, default accounts, and effective time. CAF files and HTC workflow metadata remain application-owned and are not sent to CBS.

await client.createCustomer({
  registerCustKey: 'CUST-REG-20260814',
  customerKey: 'CUST-20260814185641998',
  customerCode: 'CUST-20260814185641998',
  customerType: 1,
  customerNodeType: 1,
  customerClass: 1,
  individual: {
    idType: '1', // The application chooses the ID type.
    idNumber: 'GHA-123456789-0',
    title: 'MR',
    firstName: 'Kwame',
    middleName: 'Kofi',
    lastName: 'Mensah',
    gender: 'M',
    nationality: 'GHA',
    birthday: '19900101',
    mobilePhone: '270118755',
    email: '[email protected]',
    properties: [{ code: 'KYC_SOURCE', value: 'SHARED_KYC' }],
  },
  addressInfo: {
    addressKey: 'ADDR-202608141',
    address1: '1 Independence Avenue',
    address2: 'Accra',
    postCode: 'GA-000-0000',
  },
  customerBasicInfo: {
    defaultWrittenLanguage: 2002,
    properties: [{ code: 'CUSTOMER_SEGMENT', value: 'B2B' }],
  },
  salesInfo: { salesId: 'SALES-001', salesChannelId: 'HTC' },
});

For an enterprise customer, replace individual with organization:

await client.createCustomer({
  registerCustKey: 'CUST-REG-ENTERPRISE-001',
  customerKey: 'CUST-ENTERPRISE-001',
  customerType: 1,
  organization: {
    idType: '1',
    idNumber: 'BR-123456',
    organizationType: 1,
    name: 'Example Ghana Limited',
    shortName: 'Example GH',
    industry: 'Telecommunications',
    phoneNumber: '0302000000',
    email: '[email protected]',
    website: 'https://example.com',
  },
});

The application decides the customer type and organization or individual idType; the client passes those values through unchanged.

Create a configurable subscriber

Use createSubscriber() for prepaid, postpaid, and hybrid requests. Use customerKey for an existing corporate customer, or provide a configurable registerCustomer block with opType: 1 for a standalone subscriber. Account and payment-relation fields are optional where CBS permits them and are serialized only when supplied.

await client.createSubscriber('270105755', {
  customerKey: 'MK202608201545',
  paymentMode: 2,
  offeringId: '2018105022',
  accounts: [
    {
      accountKey: 'MK202608201629',
      paymentType: 0,
      defaultAccount: true,
    },
    {
      accountKey: 'MK202608201545',
      paymentType: 1,
      defaultAccount: false,
      paymentRelationKey: 'PostPayRelKey270105755',
      priority: 55,
    },
  ],
});

Add or modify a payment limit through the same configurable payment-relation operation:

await client.changePaymentRelation({
  primaryIdentity: '270105755',
  modifyPayRelation: {
    payRelationKey: 'PostPayRelKey270105755',
    paymentLimit: {
      operationType: 1,
      paymentLimitKey: 'PostPayRelKey270105755',
      limitValue: 50_000_000,
    },
  },
  paymentLimits: [
    {
      paymentLimitKey: 'PostPayRelKey270105755',
      limitCycleType: 'B',
      limitType: 'M',
      limitValueType: 'A',
      limitValue: 50_000_000,
    },
  ],
});

For a live tenant smoke test, use the guarded manual script. It generates a unique organization and sends the minimal payload that was verified against CBS R25:

npm run test:create-enterprise-customer -- --execute

The script uses customerType: 2, industry: 'Telcom', and omits organizationType and custom properties such as TIN. Those values are tenant-specific or HTC-local and should not be sent to CBS unless the target tenant explicitly supports them.

The container-flow smoke script is guarded and uses the three configured test MSISDNs by default:

npm run test:create-container-flow -- --execute --cleanup

--cleanup churns the three MSISDNs first. GHS amounts are converted to CBS units using CBS_AMOUNT_DIVISOR (default 100000). The prepaid amount is sent as InitBalance; only the postpaid and hybrid amounts are sent as payment limits.

Query a customer hierarchy

Query by MSISDN or by exactly one CBS key. The parsed result includes customer, subscriber, account, offering, lifecycle, billing-cycle, main-balance, account-balance, and free-unit fields when CBS returns them. data.AcctList contains normalized balance results and data.FreeUnits contains normalized free-unit totals and allocation details. Free-unit details are always returned as an array, including when CBS returns only one detail.

const byMsisdn = await client.queryCustomerInfo('270118755');
const byCustomer = await client.queryCustomerInfoByKey({
  customerCode: 'CUST-20260814185641998',
});
const byAccount = await client.queryCustomerInfoByKey({
  accountCode: 'ACCT-20260814185641998-1',
});
const bySubscriber = await client.queryCustomerInfoByKey({
  subscriberKey: 'SUB-20260814185641998-1-1',
});

console.log(byMsisdn.data.PaymentMode);
console.log(byMsisdn.data.Status);
console.log(byMsisdn.data.PrimaryOffering);
console.log(byMsisdn.data.MainBalance?.amountInGhc);
console.log(byMsisdn.data.AcctList);
console.log(byMsisdn.data.FreeUnits);

Update customer information

changeCustomerInfo requires exactly one of customerKey, customerCode, or primaryIdentity.

await client.changeCustomerInfo({
  customerKey: 'CUST-20260814185641998',
  individual: {
    lastName: 'Mensah-Updated',
    idNumber: '',
    email: '[email protected]',
  },
  additionalProperties: [{ code: 'CRM_STATUS', value: 'VERIFIED' }],
});

Activate or deregister a customer

await client.custActivation({ customerKey: 'CUST-20260814185641998' });

await client.custDeactivation({
  customerKey: 'CUST-20260814185641998',
  opType: '2', // Normal deregistration in the R25 reference.
});

Customer deregistration requires no valid subscriber or account references. For cleanup, delete subscribers first, then accounts, then the customer.

Account operations

Create a standalone account

Use createAccount when an account must be provisioned independently. When creating a subscriber and its account together, prefer the typed subscriber creation methods below.

await client.createAccount({
  registerCustKey: 'CUST-20260814185641998',
  accountKey: 'ACCT-20260814185641998-1',
  accountCode: 'ACCT-20260814185641998-1',
  userCustomerKey: 'CUST-20260814185641998',
  paymentType: 1,
  creditLimitType: 'C_BILL_CYCLE_INITIAL_CREDIT',
  // The application supplies CBS units: GHS 10 = 1,000,000.
  creditLimit: 1_000_000,
});

Credit limits are passed unchanged. The client does not multiply or divide the value.

Update an account credit limit

await client.changeAccountCreditLimit({
  accountCode: 'ACCT-20260814185641998-1',
  creditLimitType: 'C_BILL_CYCLE_INITIAL_CREDIT',
  newLimitAmount: 2_000_000,
});

Add or remove payment relations

Identify the account, subscriber, customer, or primary identity with exactly one supported access key. Supply either addPayRelation or deletePayRelationKey as required by the operation.

await client.changePaymentRelation({
  accountKey: 'ACCT-20260814185641998-1',
  addPayRelation: {
    payRelationKey: 'PAY-20260814185641998-5',
    accountKey: 'ACCT-20260814185641998-1',
    priority: 50,
  },
});

await client.changePaymentRelation({
  accountKey: 'ACCT-20260814185641998-1',
  deletePayRelationKey: 'PAY-20260814185641998-5',
});

Deregister an account

await client.acctDeactivation({
  accountCode: 'ACCT-20260814185641998-1',
  opType: '2', // Deregister in the R25 reference.
});

An account cannot be deregistered while it is a default account or has valid payment relations. Remove the subscriber/payment relationships first.

Subscriber operations

Create prepaid, postpaid, or hybrid subscribers

The application owns every key. customerKey must equal the parent customer key. Each account's accountKey and accountCode must match and be unique; payment relation keys must also be unique. The client does not generate identifiers, choose identity types, or transform credit limits.

const common = {
  customerKey: 'CUST-20260814185641998',
  subscriberKey: 'SUB-20260814185641998-1-1',
  offeringId: '2018105068',
  status: 1,
};

await client.createPostpaidSubscriber('270118755', {
  ...common,
  accounts: [
    {
      accountKey: 'ACCT-20260814185641998-1',
      accountCode: 'ACCT-20260814185641998-1',
      paymentRelationKey: 'PAY-20260814185641998-1',
      paymentType: 1,
      creditLimit: 1_000_000,
    },
  ],
});

await client.createPrepaidSubscriber('270105755', {
  customerKey: 'CUST-20260814185641998',
  subscriberKey: 'SUB-20260814185641998-2-1',
  offeringId: '2018246521',
  status: 1,
  // Omit accounts to create the subscriber without an account.
});

await client.createHybridSubscriber('261180254', {
  customerKey: 'CUST-20260814185641998',
  subscriberKey: 'SUB-20260814185641998-3-1',
  offeringId: '2018105022',
  status: 1,
  accounts: [
    {
      accountKey: 'ACCT-20260814185641998-3',
      accountCode: 'ACCT-20260814185641998-3',
      paymentRelationKey: 'PAY-20260814185641998-3',
      paymentType: 1,
      defaultAccount: true,
      priority: 50,
      creditLimit: 1_000_000,
    },
    {
      accountKey: 'ACCT-20260814185641998-4',
      accountCode: 'ACCT-20260814185641998-4',
      paymentRelationKey: 'PAY-20260814185641998-4',
      paymentType: 0,
      defaultAccount: false,
      priority: 51,
    },
  ],
});

Rules enforced by the client:

  • Prepaid accounts use paymentType: 0; postpaid accounts use paymentType: 1.
  • Prepaid and postpaid creation accepts zero accounts or exactly one account.
  • Hybrid creation requires exactly two accounts: one prepaid and one postpaid.
  • Hybrid creation requires exactly one default account.
  • The primary offering status is set to the supplied subscriber/account status value.
  • The client sends the supplied credit-limit value unchanged.

Update subscriber offering and payment mode

await client.changeSubscriberOffering({
  primaryIdentity: '270118755',
  oldOfferingId: '2018105068',
  newOfferingId: '2018105071',
});

await client.changeSubscriberPaymentMode({
  primaryIdentity: '270118755',
  paymentMode: 1,
  oldOfferingId: '2018105068',
  newOfferingId: '2018105071',
  accountKey: 'ACCT-20260814185641998-1',
  paymentRelationKey: 'PAY-20260814185641998-1',
});

changeSubscriberOffering() sends an immediate EffectiveTime mode when one is not supplied. CBS R25 requires the element-only form inside PrimaryOffering: <bcs:EffectiveTime><bcc:Mode>I</bcc:Mode></bcs:EffectiveTime>.

Subscriber lifecycle and status

Query the current lifecycle before attempting a status change. CBS may reject transitions from states such as Idle when the subscriber is not serviceable. The status-change request sends the R25 NewStatus element required by the CBS ChangeSubStatusRequest schema.

const lifecycle = await client.querySubLifeCycle('270118755');
console.log(lifecycle.data.CurrentStatusIndex);

await client.subActivate('270118755');
await client.changeSubscriberStatus('270118755', { status: 'SUSPEND' });
await client.changeSubscriberStatus('270118755', { status: 'CALL_BARRING' });
await client.changeSubscriberStatus('270118755', {
  status: 'ACTIVE',
  operation: 'CUSTOMER_RESUME',
});

await client.subDeactivation('270118755', {
  opType: '1', // Deployment-specific: use the value configured by CBS.
});

Lifecycle status codes exposed by SubscriberStatusCode are 1 Idle, 2 Active, 3 Call Barring, 4 Suspend, 6 Tested, 7 In Stock, and 8 Pre-deregistration.

Subscriber lifecycle requests use the MSISDN as the CBS PrimaryIdentity. subscriberKey is a resource key used during subscriber creation and is ignored by lifecycle mutation methods. Customer queries use CustomerKey; account queries use AccountKey or AccountCode.

changeSubscriberStatus() defaults to customer-request operations, and supports the following named operation variants:

| Operation | CBS OpType | Target status | | -------------------------------------------------------------------------------- | -----------: | ------------: | | CUSTOMER_RESUME / CUSTOMER_BARRING / CUSTOMER_SUSPENSION | 10 / 11 / 12 | 2 / 3 / 4 | | ARREARS_RESUME / ARREARS_BARRING / ARREARS_SUSPENSION | 30 / 31 / 32 | 2 / 3 / 4 | | CREDIT_CONTROL_RESUME / CREDIT_CONTROL_BARRING / CREDIT_CONTROL_SUSPENSION | 40 / 41 / 42 | 2 / 3 / 4 | | OPERATOR_RESUME / OPERATOR_BARRING / OPERATOR_SUSPENSION | 60 / 61 / 62 | 2 / 3 / 4 |

For a guarded live test against MSISDN 270118755, provide credentials through environment variables and explicitly pass --execute:

CBS_BASE_URL=https://cbs.example.invalid/services/BcServices \
CBS_USERNAME=... \
CBS_PASSWORD=... \
npm run test:change-subscriber-status -- --execute

The script changes the subscriber to SUSPEND; it does not automatically activate an Idle subscriber. Check the lifecycle result first and activate it using subActivate() when required.

For permanent number deletion, use deleteNumber. It sends SubDeactivation with R25 opType=3:

await client.deleteNumber('270118755', {
  // Subscriber deletion uses the MSISDN/PrimaryIdentity above.
});

This is destructive. Verify the identifier and query the subscriber after the request.

Subscribe or remove an appendant offering

const added = await client.subscribeAppendantProduct('270118755', {
  offeringId: '2019000001',
  bundledFlag: 'N',
  offeringClass: 'I',
  status: 1,
});

await client.unsubscribeAppendantProduct('270118755', {
  offeringId: '2019000001',
  purchaseSeq: added.data.PurchaseSeq!,
});

Deprecated subscriber helpers

createSubscriberForAccount and poolActivation remain available for source compatibility but are deprecated. Use the typed subscriber creation methods and explicit lifecycle calls instead:

await client.deleteNumber(msisdn);
await client.createPrepaidSubscriber(msisdn, createOptions);
await client.subActivate(msisdn);

Balance and transaction operations

const balance = await client.queryBalance('270118755');
console.log(balance.data.TotalAmount, balance.data.amountInGhc);

await client.adjustAccount('270118755', {
  adjustmentAmt: 100_000,
  balanceType: 'C_MAIN_ACCOUNT',
  adjustmentType: 2,
  currencyId: 1054,
  adjustmentReasonCode: 'DNTREQ',
  opType: 2,
  adjustmentSerialNo: 'ADJ-20260814-001',
});

// Account balances and free units can also be adjusted in one CBS request.
await client.adjustBalances('270118755', {
  adjustmentSerialNo: 'MOVE-20260928-001',
  accounts: [
    {
      balanceType: 'C_BONUS_FUND',
      adjustmentAmt: 100_000,
      adjustmentType: 1,
      currencyId: 1054,
    },
  ],
  adjustments: [
    {
      freeUnitType: 'C_BigTime_Data_10GH',
      adjustmentAmt: 500,
      adjustmentType: 1,
      expireTime: '20370101000000',
    },
  ],
});

const transactions = await client.queryXTransaction('270118755');
console.dir(transactions.data, { depth: null });

// Search the subscriber's billing transaction log.
const pageSize = 50;
let totalRows = 0;

for (let startRow = 0; ; ) {
  const page = await client.queryTransaction('560043149', {
    startTime: '20261001000000',
    endTime: '20261005235959',
    totalRows, // CBS expects 0 on the first request, then the returned total.
    startRow, // Zero-based offset: 0, 50, 100, ...
    pageSize,
  });

  totalRows = page.pagination.totalRows ?? totalRows;
  for (const transaction of page.transactions) {
    console.dir(transaction, { depth: null });
  }

  const nextStartRow = startRow + page.pagination.rowsReturned;
  const reachedKnownEnd = page.pagination.totalRows !== undefined && nextStartRow >= totalRows;
  const endedWithoutTotal =
    page.pagination.totalRows === undefined && page.transactions.length < pageSize;
  if (page.pagination.rowsReturned === 0 || reachedKnownEnd || endedWithoutTotal) break;
  startRow = nextStartRow;
}

const cdr = await client.queryCdrDetail('270118755', '123456789');
console.dir(cdr.data, { depth: null });

const rechargePage = await client.queryRechargeLog('560043149', {
  startTime: '20261001000000',
  endTime: '20261005235959',
  pageSize: 50,
  subscriberLevelOnly: true,
});
console.dir(rechargePage.recharges, { depth: null });

const refundPage = await client.queryRefundLog('560043149', {
  startTime: '20261001000000',
  endTime: '20261005235959',
  pageSize: 50,
});
console.dir(refundPage.refunds, { depth: null });

const transferPage = await client.queryTransferLog(msisdn, {
  startTime: '20261001000000',
  endTime: '20261005235959',
  queryTypes: ['0', '1'],
  successfulOnly: true,
  pageSize: 50,
});
// Read transferPage.transfers and transferPage.pagination.

queryTransaction uses the ArServices endpoint and returns transactions as an array plus normalized pagination values (totalRows, request startRow and pageSize, and rowsReturned). data and metadata preserve the parsed CBS response, including namespaced or deployment-specific fields not modeled by the client. CBS paging uses TotalRowNum as the prior total and BeginRowNum as a zero-based offset; FetchRowNum is limited to 1000. queryRechargeLog also uses ArServices and requires a time range. Its pageSize maximum is 500. By default, CBS returns recharge records for the subscriber's default account; set subscriberLevelOnly: true to request only records tied to that subscriber. The manual's C_SUB_LOG behavior and recharge-summary output depend on CBS configuration. rechargeType and innerRechargeType are mutually exclusive. queryCdrDetail uses BbServices and requires a CDR sequence; it does not search CDRs by number. queryRefundLog uses ArServices; its date range is optional and its pageSize maximum is 1000. It returns refunds as normalized records and preserves all parsed CBS fields in data and metadata. Refund amounts remain raw CBS values; use CurrencyID when interpreting them.

queryTransferLog queries the subscriber's default-account transfer logs using ArServices. Both time bounds are required; page size is 1–500 and startRow/totalRows are 0–65534. queryTypes selects transfer types ('0' in, '1' out, '2' out fee, '3' in fee, '4' exchange, '5' migrate-in, '6' migrate-out); migration types require CBS configuration. successfulOnly: true returns successful records, false includes unsuccessful records, and omitting it uses the CBS default. The result includes transfers, pagination, data, and metadata. Signed transfer amounts and identifiers remain strings.

Recent Usage / consumption CDRs

queryXTransaction(msisdn, options?) calls QueryLastXTransaction on BcServices. It returns recent consumption CDRs for a subscriber, including voice, SMS, MMS, data and other charging events. options.subscriberKey selects by subscriber key instead of MSISDN. No date-range, paging, limit-size or server-search parameters are supported by this contract.

Version 2.12.2 adds typed records and records, a normalized array; data and metadata continue to preserve the original CBS result. See the Recent Usage integration guidance for field meanings, display rules and limits, and the vendor contract request for historical queries.

Cleanup order

Use bottom-up cleanup so CBS does not reject a parent with active children:

for (const msisdn of subscriberMsisdns) {
  await client.deleteNumber(msisdn);
}

for (const accountCode of accountCodes) {
  await client.acctDeactivation({ accountCode, opType: '2' });
}

await client.custDeactivation({ customerKey, opType: '2' });

Query each subscriber by MSISDN, each account by account code, and the customer by customer key or code to confirm that CBS no longer returns the records.

Testing and release checks

Tests are local HTTP-backed unit tests; they do not load credentials or connect to CBS:

npm ci
npm run typecheck
npm test
npm run build
npm pack --dry-run

npm run build is also the prepublishOnly hook. Review the tarball file list from npm pack --dry-run before publishing.

Postman collection

postman/cbs-client.collection.json contains executable R25 SOAP examples. Set the collection variables locally before use; the committed collection contains placeholders and no credentials. The billing log examples were generated from client requests. Set msisdn, startTime, and endTime before querying logs; the combined adjustment example also requires freeUnitType.

License

MIT