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

ebm-rw

v1.3.0

Published

TypeScript client SDK for Rwanda Revenue Authority's EBM 2.1 / VSDC tax API

Readme

ebm-rw

TypeScript Node.js License npm

A typed TypeScript/JavaScript client SDK for the Rwanda Revenue Authority (RRA) EBM 2.1 / VSDC tax API — the fiscalization interface POS and CIS/ERP software must integrate with to register sales, purchases, stock movements, and items with RRA.

Built directly against RRA's own VSDC reference implementation and specification documents — every request/response shape below is verified against a running VSDC instance, not just the written spec.


Official RRA Documentation

📄 VSDC Specification Document (v1.0.4, 2022) https://www.rra.gov.rw/fileadmin/user_upload/vsdc_specification_document_v1.0.4__2022.pdf

📄 OSDC (Online Sales Data Controller) Documentation (v1.0.1, 2022) https://www.rra.gov.rw/fileadmin/user_upload/osdc_documentation_v1.0.1_2022-04-08.pdf

📄 RRA Easy Adoption Guide on EBM https://www.rra.gov.rw/fileadmin/user_upload/rra_easy_adoption_on_ebm_engl.pdf

⚠️ Device/branch registration and VSDC approval with RRA (via the MyRRA portal) are required before any of the operations below will succeed against RRA's systems.


Installation

npm install ebm-rw

Quick start

import { EbmClient, buildItemCd } from "ebm-rw";

const client = new EbmClient({ baseUrl: "https://your-vsdc-host" });

const init = await client.selectInitInfo({
  tin: "999991130",
  bhfId: "00",
  dvcSrlNo: "my-device-001",
});

const itemCd = buildItemCd({
  originNationCd: "RW",
  productTypeCd: "2",
  pkgUnitCd: "NT",
  qtyUnitCd: "BA",
  sequence: 1,
});

await client.saveItem({
  tin: "999991130",
  bhfId: "00",
  itemClsCd: "...",
  itemCd,
  itemTyCd: "2",
  itemNm: "Example product",
  orgnNatCd: "RW",
  pkgUnitCd: "NT",
  qtyUnitCd: "BA",
  taxTyCd: "B",
  dftPrc: 1000,
  isrcAplcbYn: "N",
  useYn: "Y",
  regrNm: "Admin",
  regrId: "admin",
  modrNm: "Admin",
  modrId: "admin",
});

Hosted vs. local (offline-first) usage

EbmClient only needs a reachable VSDC base URL — it has no built-in opinion on where that VSDC instance lives. That's what makes offline-first deployments possible:

  • Hosted: point baseUrl at a cloud-hosted VSDC instance (typically fronted by your own backend, which adds auth, outbox durability, and multi-tenant scoping — this SDK itself is still a thin, stateless HTTP client and doesn't implement any of that). Requires internet connectivity.
  • Local: point baseUrl at a VSDC instance running on the same local network as the POS, e.g. http://192.168.1.71:8080. No internet required for day-to-day fiscalization — only VSDC's own periodic sync back to RRA needs connectivity, and that's VSDC's responsibility, not this SDK's.
const client = new EbmClient({
  baseUrl: settings.mode === "local" ? settings.localVsdcUrl : "https://your-hosted-vsdc-host",
});

Retry/backoff (opt-in)

EbmClient can optionally retry a single call's transient failures — a fetch() throw (network unreachable, DNS failure, timeout) or an HTTP 5xx response — with exponential backoff and jitter. It's off by default (zero behavior change unless you opt in), never retries HTTP 4xx responses, and never retries a business-logic failure (a parsed response with a non-success resultCd, e.g. "994" duplicate) — those still throw on the first attempt. This is still just a bounded, stateless retry of one HTTP call, not outbox/durability: that's still the embedding service's job.

const client = new EbmClient({
  baseUrl: settings.localVsdcUrl,
  retry: { attempts: 2, baseDelayMs: 250, maxDelayMs: 5000 }, // all fields optional; attempts defaults to 0
});

dvcSrlNo must be sent byte-exact. RRA compares it exactly as registered — leading/trailing whitespace is significant. A stripped or re-typed value that looks identical will fail selectInitInfo with resultCd: 901 "It is not valid device", indistinguishable from a genuinely unapproved device.


Full API reference

Every VSDC request carries tin and bhfId ("00" = head office). Every response is wrapped in the same envelope unless noted:

interface EbmEnvelope<TData> {
  resultCd: string;
  resultMsg: string;
  resultDt: string; // yyyyMMddhhmmss
  data: TData;
}

"Save"-style requests also carry audit fields: regrNm, regrId (registrant name/ID) and modrNm, modrId (modifier name/ID).

Initialization

selectInitInfo — one-time device activation

Call once per physical/virtual device, before anything else. Returns the taxpayer/branch/device record RRA holds for this dvcSrlNo, including the cmcKey your integration needs to persist for subsequent calls.

await client.selectInitInfo({
  tin: "999991130",
  bhfId: "00",
  dvcSrlNo: "my-device-001",
});
// => { resultCd, resultMsg, resultDt, data: { info: {
//        tin, taxprNm, bsnsActv,                              // taxpayer
//        bhfId, bhfNm, bhfOpenDt, prvncNm, dstrtNm, sctrNm,
//        locDesc, hqYn, mgrNm, mgrTelNo, mgrEmail,             // branch
//        dvcId, sdcId, mrcNo, intrlKey, signKey, cmcKey,
//        lastSaleInvcNo, lastPchsInvcNo, lastSaleRcptNo, ...   // device
//      } } }

Reference / code data

selectCodes — pull RRA's common code/classification lists

await client.selectCodes({ tin, bhfId, lastReqDt: "20260101000000" });
// => { data: { clsList: [{ cdCls, cdClsNm, cdClsDesc, useYn, dtlList: [{ cd, cdNm, cdDesc, srtOrd, useYn }] }] } }

selectItemsClass — pull the item classification tree (itemClsCd values)

await client.selectItemsClass({ tin, bhfId, lastReqDt: "20260101000000" });
// => { data: { itemClsList: [{ itemClsCd, itemClsNm, itemClsLvl, taxTyCd, mjrTgYn, useYn }] } }

selectCustomer — look up a taxpayer by TIN

await client.selectCustomer({ tin, bhfId, custmTin: "104345680" });
// => { data: { custList: [{ tin, taxprNm, taxprSttsCd, prvncNm, dstrtNm, sctrNm, locDesc }] } }

selectBranches — pull your own registered branch offices

await client.selectBranches({ tin, bhfId, lastReqDt: "20260101000000" });
// => { data: { bhfList: [{ tin, bhfId, bhfNm, bhfSttsCd, prvncNm, dstrtNm, sctrNm, locDesc, mgrNm, mgrTelNo, mgrEmail, hqYn }] } }

selectNotices — pull RRA notices/announcements

await client.selectNotices({ tin, bhfId, lastReqDt: "20260101000000" });
// => { data: { noticeList: [{ noticeNo, title, cont, dtlUrl, regrNm, regDt }] } }

Branch info

saveBranchCustomer — register a repeat/known customer against your branch

await client.saveBranchCustomer({
  tin, bhfId,
  custNo: "CUST-0001",
  custTin: "104345680",
  custNm: "Acme Ltd",
  useYn: "Y",
  regrNm: "Admin", regrId: "admin", modrNm: "Admin", modrId: "admin",
});
// => { data: null }

saveBranchUser — register a POS/branch user (e.g. a cashier)

await client.saveBranchUser({
  tin, bhfId,
  userId: "cashier01",
  userNm: "Jane Doe",
  pwd: "...",
  useYn: "Y",
  regrNm: "Admin", regrId: "admin", modrNm: "Admin", modrId: "admin",
});
// => { data: null }

saveBranchInsurance — pharmacy taxpayers only

await client.saveBranchInsurance({
  tin, bhfId,
  isrccCd: "01",
  isrccNm: "RSSB",
  isrcRt: 10.0,
  useYn: "Y",
  regrNm: "Admin", regrId: "admin", modrNm: "Admin", modrId: "admin",
});
// => { data: null }

Items

saveItem — register/update a product in your catalog

await client.saveItem({
  tin, bhfId,
  itemClsCd: "5020230201",
  itemCd,                       // build with buildItemCd() — see below
  itemTyCd: "2",                // 1 Raw Material, 2 Finished Product, 3 Service, 4 Assets, 5 Expenses
  itemNm: "500ml bottled water",
  orgnNatCd: "RW",
  pkgUnitCd: "NT",
  qtyUnitCd: "BA",
  taxTyCd: "B",                 // B = 18% standard rate
  dftPrc: 500,
  isrcAplcbYn: "N",
  useYn: "Y",
  regrNm: "Admin", regrId: "admin", modrNm: "Admin", modrId: "admin",
});
// => { data: null }

saveItemComposition — define a bundle/recipe (BOM) for a composite item

await client.saveItemComposition({
  tin, bhfId,
  itemCd: "parentItemCd",
  cpstItemCd: "componentItemCd",
  cpstQty: 2,
  regrNm: "Admin", regrId: "admin", modrNm: "Admin", modrId: "admin",
});
// => { data: null }

selectItems — pull your registered catalog (including RRA-side reclassifications)

await client.selectItems({ tin, bhfId, lastReqDt: "20260101000000" });
// => { data: { itemList: [{ itemClsCd, itemCd, itemTyCd, itemNm, orgnNatCd, pkgUnitCd,
//        qtyUnitCd, taxTyCd, dftPrc, isrcAplcbYn, rraModYn, useYn, ... }] } }

Import items

selectImportItems — pull customs-declared items awaiting confirmation

await client.selectImportItems({ tin, bhfId, lastReqDt: "20260101000000" });
// => { data: { itemList: [{ taskCd, dclDe, itemSeq, dclNo, hsCd, itemNm, imptItemsttsCd,
//        orgnNatCd, exptNatCd, pkg, pkgUnitCd, qty, qtyUnitCd, ... }] } }

updateImportItems — confirm an imported item into your own catalog

await client.updateImportItems({
  tin, bhfId,
  taskCd: "...",
  dclDe: "20260101",
  itemSeq: 1,
  hsCd: "...",
  itemClsCd: "5020230201",
  itemCd: "...",
  imptItemSttsCd: "3",
  modrNm: "Admin", modrId: "admin",
});
// => { data: null }

Sales

saveSales — the core invoicing call: submit a sale, get back the fiscal receipt signature

await client.saveSales({
  tin, bhfId,
  invcNo: 1024,             // your own sequential invoice number, per branch
  orgInvcNo: 0,              // 0 for a new sale; original invcNo when refunding/adjusting
  rcptTyCd: "S",             // S = normal Sale, R = Refund
  salesTyCd: "N",            // always "N" per RRA guidance
  pmtTyCd: "01",             // 01 CASH .. 06 MOBILE MONEY .. 07 OTHER
  salesSttsCd: "02",
  cfmDt: "20260101120000",
  salesDt: "20260101",
  totItemCnt: 1,
  taxblAmtA: 0, taxblAmtB: 500, taxblAmtC: 0, taxblAmtD: 0,
  taxRtA: 0, taxRtB: 18, taxRtC: 0, taxRtD: 0,
  taxAmtA: 0, taxAmtB: 90, taxAmtC: 0, taxAmtD: 0,
  totTaxblAmt: 500,
  totTaxAmt: 90,
  totAmt: 590,
  prchrAcptcYn: "N",
  // custTin/custNm: "104345680"/"ACME Ltd" — when the buyer is a business (has a TIN), RRA spec
  // v1.0.5 §3.3.6.1 requires prcOrdCd (Purchase Order Code, max 6 chars) too; omit both for
  // individual/walk-in customers.
  receipt: { rptNo: 1, prchrAcptcYn: "N" },
  itemList: [{
    itemSeq: 1,
    itemCd,
    itemNm: "500ml bottled water",
    pkgUnitCd: "NT", pkg: 1,
    qtyUnitCd: "BA", qty: 1,
    prc: 590, splyAmt: 590, dcRt: 0, dcAmt: 0,
    taxTyCd: "B", taxblAmt: 500, totTaxAmt: 90, totAmt: 590,
  }],
  regrNm: "Admin", regrId: "admin", modrNm: "Admin", modrId: "admin",
});
// => { data: { rcptNo, intrlData, rcptSign, totRcptNo, vsdcRcptPbctDate, sdcId, mrcNo } }
// Print intrlData/rcptSign on the physical receipt — this is the fiscal proof of sale.

resendSales — manually re-trigger delivery for a previously-submitted sale

await client.resendSales({ /* see INTEGRATION_GUIDE.md for when to use this */ });

Purchases

selectTrnsPurchaseSales — pull sales suppliers have declared against you as buyer

await client.selectTrnsPurchaseSales({ tin, bhfId, lastReqDt: "20260101000000" });
// => { data: { saleList: [{ spplrTin, spplrNm, spplrBhfId, spplrInvcNo, rcptTyCd, cfmDt,
//        salesDt, totItemCnt, totTaxblAmt, totTaxAmt, totAmt, itemList: [...] }] } }

savePurchases — confirm/record a purchase transaction

await client.savePurchases({
  tin, bhfId,
  invcNo: 501,
  orgInvcNo: 0,
  regTyCd: "M",             // Manual
  pchsTyCd: "N",
  rcptTyCd: "P",
  pmtTyCd: "01",
  pchsSttsCd: "02",
  pchsDt: "20260101",
  totItemCnt: 1,
  taxblAmtA: 0, taxblAmtB: 500, taxblAmtC: 0, taxblAmtD: 0,
  taxRtA: 0, taxRtB: 18, taxRtC: 0, taxRtD: 0,
  taxAmtA: 0, taxAmtB: 90, taxAmtC: 0, taxAmtD: 0,
  totTaxblAmt: 500, totTaxAmt: 90, totAmt: 590,
  itemList: [{
    itemSeq: 1, itemClsCd: "5020230201", itemNm: "500ml bottled water",
    pkg: 1, qtyUnitCd: "BA", qty: 1, prc: 590, splyAmt: 590,
    dcRt: 0, dcAmt: 0, taxblAmt: 500, taxTyCd: "B", taxAmt: 90, totAmt: 590,
  }],
  regrNm: "Admin", regrId: "admin", modrNm: "Admin", modrId: "admin",
});
// => { data: null }

Stock

selectStockItems — pull stock movement between HQ/branches

await client.selectStockItems({ tin, bhfId, lastReqDt: "20260101000000" });
// => { data: { stockList: [{ custTin, custBhfId, sarNo, ocrnDt, totItemCnt,
//        totTaxblAmt, totTaxAmt, totAmt, itemList: [...] }] } }

saveStockItems — record a stock in/out event

await client.saveStockItems({
  tin, bhfId,
  sarNo: 1, orgSarNo: 0,
  regTyCd: "M",
  sarTyCd: "02",             // stock in/out type code
  ocrnDt: "20260101",
  totItemCnt: 1,
  totTaxblAmt: 500, totTaxAmt: 90, totAmt: 590,
  itemList: [{
    itemSeq: 1, itemClsCd: "5020230201", itemNm: "500ml bottled water",
    pkgUnitCd: "NT", pkg: 1, qtyUnitCd: "BA", qty: 10,
    prc: 590, splyAmt: 5900, totDcAmt: 0,
    taxblAmt: 5000, taxTyCd: "B", taxAmt: 900, totAmt: 5900,
  }],
  regrNm: "Admin", regrId: "admin", modrNm: "Admin", modrId: "admin",
});
// => { data: null }

saveStockMaster — push current on-hand quantity for an item

await client.saveStockMaster({
  tin, bhfId,
  itemCd,
  rsdQty: 42,
  regrNm: "Admin", regrId: "admin", modrNm: "Admin", modrId: "admin",
});
// => { data: null }

Z-reports (daily fiscal summary)

saveZReport — submit the end-of-day summary

await client.saveZReport({
  tin, bhfId,
  zRepDt: "20260101",
  totRcptNo: 87,
  totSaleAmt: 452300,
  totTaxAmt: 68998,
  regrNm: "Admin", regrId: "admin", modrNm: "Admin", modrId: "admin",
});
// => { data: null }

checkZReport — verify a previously-submitted day's summary

await client.checkZReport({ tin, bhfId, zRepDt: "20260101" });
// => { data: { reportList: [{ zRepDt, totRcptNo, totSaleAmt, totTaxAmt }] } }

Utility / connectivity

selectServerTime — bare liveness check

await client.selectServerTime();
// => "20260101120000"   (plain string, not the usual envelope)

echoTest — post-initialization connectivity check

Requires a device that has already completed selectInitInfo successfully.

await client.echoTest({ tin, bhfId, echoMsg: "ping" });
// => { data: { echoMsg: "ping" } }

Sector-specific / newer operations

These were added in a newer VSDC build and only apply to fuel-station or hospitality/tourism taxpayers. Every other operation above is stable and applies to all sectors.

getFuelProducts — look up recognized fuel product codes (fuel/petroleum sector)

await client.getFuelProducts({ tin, bhfId });

saveRrpChangeHistory — record a Recommended Retail Price change (fuel/petroleum sector)

await client.saveRrpChangeHistory({ /* see JSDoc on RrpChangeHistSaveReq for fields */ });

Fuel/hospitality-sector fields also appear as optional fields on saveItem (rrp, ttCatCd, propertyTyCd, roomTypeCd, currentStockQty) and saveSales (taxblAmtTt/taxRtTt/taxAmtTt tourism tax buckets, rrp per line item) — omit them entirely for non-fuel/non-hospitality businesses.


Error handling

Every non-success resultCd throws EbmApiError:

import { EbmApiError } from "ebm-rw";

try {
  await client.saveSales({ /* ... */ });
} catch (err) {
  if (err instanceof EbmApiError) {
    console.error(err.resultCd, err.resultMsg);
    if (err.isDuplicate) {
      // resultCd 994 — already delivered on a previous attempt, safe to treat as success
    } else if (err.isClientSide) {
      // 8xx-series — request never reached RRA's business logic (network/formatting issue)
    }
  }
}

describeResultCode(resultCd) gives a human-readable label for any code. See the VSDC Specification PDF linked above for the authoritative code table.

What's covered

All VSDC operations documented above — initialization, code/reference-data sync, branch/customer/item/stock/import-item CRUD, sales, purchases, Z-reports, connectivity checks — plus itemCd build/parse helpers (buildItemCd/parseItemCd) and date-format helpers for the date string formats VSDC uses.

Some request/response shapes (saveItemComposition, saveZReports/checkZReport) were reconstructed from the reference implementation rather than fully-documented spec text — flagged with a comment in src/types/ and worth confirming against a live sandbox call before depending on them in production.

Live verification

scripts/liveCheck.ts is a manual, opt-in smoke test against a real/sandbox VSDC — never run in CI, never wired into any app's runtime. It reads tin/bhfId/dvcSrlNo/VSDC_BASE_URL from EBM/.env (copy EBM/.env.example and fill in real values first):

npm run live:codes   # confirms /code/selectCodes; diff its output against itemCode.ts's/pos-cloud's
                      # ebmUnitCodes.ts's hardcoded tables after any spec-drift concern
npm run live:init    # confirms /initializer/selectInitInfo

Never run live:init against a production dvcSrlNo. RRA only ever returns the one-time device-init payload (cmcKey, sdcId, mrcNo, lastSaleInvcNo, etc.) on a device's first-ever successful (resultCd '000') call — every call after that returns resultCd '902' ("already installed") with the payload absent. Re-running this against an already-activated device is safe (it'll just report 902), but there is no "undo" if it's ever run against a device that hasn't been activated yet and shouldn't be.

Documentation

See INTEGRATION_GUIDE.md for a practical guide to which operation to call for which real-world POS event (checkout, refund, day close, receiving stock, etc.).

License

MIT