Issue RRA receipts from your own system
EBM Hub signs every sale through the RRA VSDC and returns the receipt data your POS or ERP prints. Work in the browser portal, or connect over the API with a company key.
POST /trnsSales/saveSales
Invoice1001
Branch00
Total RWF12,250.00
SDC INFORMATION
SDC IDSDC010000001
Receipt1/1 NS
Internal dataTEST-INTE-RNAL
SignatureTEST-SIGN-ATUR
Sandbox example. A sale is printable only after its SDC signature comes back.
For customers who prefer a browser, see the customer portal guide. API access is optional.
For a runnable backend integration example, use the Python client with the managed CIS guide. It uses Python's standard library and is included in the downloadable client package.
The Python client's EbmError retains status and response, plus the optional
top-level code, raw retry_after header and parsed retry_after_seconds.
Integer delay values from 0 through 2,147,483,647 are parsed; absent, malformed,
larger or HTTP-date values leave retry_after_seconds as None and retain the
raw header. These fields never trigger a retry. For a managed CIS HTTP 409 with
code == "BRANCH_BUSY", your application can wait at least the supplied delay
and retry the unchanged persisted request/key within its own attempt/time limit.
A delay header alone does not make another conflict or uncertain outcome safe
to retry. Transport failures still propagate for reconciliation.
This guide is for companies connecting a POS, ERP, billing system or other backend to our EBM service. Your onboarding contact supplies the service base URL, company API keys, enabled permissions and registered branch details. Examples use synthetic identifiers; replace them with your assigned values. No production URL or credentials are embedded in this document.
The machine-readable OpenAPI specification lists the 24 Java-compatible routes, request schemas and receipt-delivery endpoints and can be imported into API tooling. Set your assigned base URL in that tool before making requests.
1. Onboarding
Supply your company TIN, branch codes, technical contact and intended integration operations. We provision the company and its authorized branches and arrange device initialization for each environment. Confirm that a branch is initialized before submitting transactions.
You receive a sandbox key for integration testing and a separate production key for live use. The same company can use both at once. Each key identifies one company and one environment; it can serve multiple authorized company branches. Every fiscal request must use that company's TIN and the intended branch's bhfId.
| Setting | Development | Production |
|---|---|---|
| API environment name | sandbox |
production |
| Newly issued key prefix | ek_test_ |
ek_live_ |
| Fiscal state | Separate sandbox state | Separate live state |
| Branch initialization | Sandbox device credentials | Production device credentials |
The key determines the environment. Sending an environment header does not change an API key's environment. Existing migrated keys may retain an older prefix: use the environment supplied at onboarding as authoritative. Sandbox receipts and invoice history are not promoted into live state.
Keep keys in your backend or a protected device credential store. Do not embed them in a public website, mobile source code, logs or shared support attachments. A signed-in company owner can set optional expiry when creating a key and revoke it in API access. Expired keys receive HTTP 401. For rotation, create a replacement with the intended permissions, update the integration and verify it works, then revoke the old key. Contact your onboarding operator if you cannot access the account. API keys are not RRA signing/device keys; we manage the latter as part of branch setup.
2. Authentication and connectivity
Every compatibility request requires X-API-Key. POST requests use UTF-8 JSON and Content-Type: application/json. The paths below are relative to the base URL; an installation may include an existing deployment context in that base URL.
export EBM_BASE_URL='https://YOUR-ASSIGNED-HOST/YOUR-OPTIONAL-CONTEXT'
# Set EBM_API_KEY from your secret store; do not commit it to a script.
curl --request POST "$EBM_BASE_URL/test/echoTest" \
--header "X-API-Key: $EBM_API_KEY" \
--header 'Content-Type: application/json' \
--data '{"tin":"999555888","bhfId":"00","testStr":"integration-check"}'
Permission examples: receipts:write for sales, items:write for item writes, stock:write for stock operations and codes:read for code lookups. A signed-in company owner can select permissions when creating a key in API access; the key list shows the permissions granted. Unchecking all permissions creates a read-only key for permitted GET operations. Registration and stock/receiving workflows also require receipts:write, as detailed in the managed CIS guide. A key never grants access to another company's TIN.
3. Choose your invoicing integration
For a company using our managed CIS catalogue, stock, refund and print controls, use POST/api/portal/fiscal/invoices. See managed CIS API for the request schema and examples. Company API keys work on that endpoint with receipts:write.
The raw /trnsSales/saveSales route below is for an agreed external-CIS integration. Once a CIS profile exists for the company in the selected environment, raw sales and selected stock/purchase/import routes return HTTP 409 to prevent bypassing CIS controls. Confirm the integration mode during onboarding.
External CIS: submit a VSDC sale
Download sale-request.json as a complete local example. Replace its TIN, branch, invoice number, dates, product identifiers, amounts and operator fields with your actual transaction. The sample product code is a placeholder and does not establish RRA product registration.
curl --request POST "$EBM_BASE_URL/trnsSales/saveSales" \
--header "X-API-Key: $EBM_API_KEY" \
--header 'Content-Type: application/json' \
--header 'X-Idempotency-Key: pos-order-20260912-0042' \
--data-binary @sale-request.json
Important fields:
| Field | Meaning |
|---|---|
tin |
Your company TIN, as a string |
bhfId |
Registered two-digit branch code, for example 00 |
invcNo |
Your invoice number; unique within company, environment and branch |
orgInvcNo |
Original invoice reference where applicable |
salesTyCd |
N normal, T training, P proforma, C copy |
rcptTyCd |
S sale or R refund |
pmtTyCd |
Payment code from the EBM code catalogue |
salesSttsCd |
Sales status code from the catalogue |
salesDt |
Sale date in yyyyMMdd, without separators |
cfmDt, other datetime fields |
When supplied, yyyyMMddHHmmss |
totItemCnt |
Number of entries in itemList |
itemList |
Product, quantity, package, tax and amount details |
receipt |
Receipt/customer print details; include its prchrAcptcYn field |
regrId, regrNm, modrId, modrNm |
Identifiers/names of the registering and modifying operators |
Send invoice identifiers as JSON integers. JavaScript applications should avoid numbers above Number.MAX_SAFE_INTEGER; use a serializer that preserves 64-bit integer values if larger identifiers are needed. Do not send formatted numbers containing currency symbols or thousands separators.
The supported sale/receipt combinations are NS, NR, TS, TR, PS, CS, and CR. Refund requests require the applicable rfdRsnCd; coordinate the refund workflow and original invoice reference during onboarding. Do not change an existing invoice into a refund by resending altered content under its original idempotency key.
The supplied Java-compatible validation includes tax-rate, tax-computation and field constraints. Use the amounts produced by your agreed EBM mapping and validate that mapping in sandbox. This example is an API fixture, not a substitute for your accounting configuration.
4. Understand the response
Sales use an envelope containing resultCd, resultMsg, resultDt and, on success, data. The successful receipt data includes:
rcptNo: the receipt counter for the sale type.totRcptNo: the branch's total receipt counter.vsdcRcptPbctDate: the receipt publication timestamp.intrlDataandrcptSign: fiscal receipt values to retain and use as supplied.sdcIdandmrcNo: device identifiers when returned.
Training and proforma receipts have empty intrlData and rcptSign, matching the supplied Java applications. Do not manufacture signatures when they are empty.
For a new sale, resultCd: "000" confirms receipt issuance through the configured VSDC and storage by the service. It does not by itself confirm RRA delivery. Retain the complete response alongside your order and invoice. Use the receipt delivery endpoints below: ACCEPTED confirms recorded RRA acceptance; VSDC_ISSUED requires reconciliation with the configured VSDC. Webhooks are not implemented.
5. Retries and duplicate protection
Persist an idempotency key with the order before the first submission. Use that same key and identical transaction content if the connection times out or the response is lost. A completed retry returns the existing local receipt without allocating another invoice or receipt counter. Idempotency is scoped to company, environment and branch.
Use a different idempotency key for a different transaction. Reusing a completed key with changed transaction content returns HTTP 409; it is not an update operation. Local Rust compares the typed, sanitized VSDC request; remote VSDC and managed CIS retain their own original-input comparison. Preserve all original fields, including timestamps, for every retry. Legacy local idempotency records created before request binding was added have no provable input: their replay returns HTTP 409 and requires reconciliation with the existing receipt. Without the original key, resubmitting an already used invoice can return 924. Investigate that result before issuing a replacement invoice.
For local Rust signing, the service maintains its own durable RRA submission queue. With a remote VSDC, that VSDC owns downstream delivery; the service retains the issuance result and uncertain requests. Client retries are for obtaining the local outcome after uncertain HTTP delivery. They are not a way to force a second RRA submission.
6. Errors
Check both the HTTP status and resultCd. Compatibility errors can arrive with HTTP 200. HTTP authentication, authorization and rate-limit responses may use a different envelope. Treat messages as diagnostic text; branch your code on status/result codes.
| Signal | Client action |
|---|---|
| HTTP 401 | Check the supplied key, expiry and revocation status |
| HTTP 403 | Check the key's company and permitted operation |
| HTTP 409 | Preserve the original request; reconcile conflicting input, uncertain issuance or a required managed CIS workflow. Do not generate a replacement invoice automatically |
| HTTP 429 | Back off; respect Retry-After when supplied |
910 |
Correct invalid or missing request fields; resending unchanged is ineffective |
913 |
Correct a code value using the agreed catalogue |
834 |
Correct the sale/receipt type combination |
836 |
Ask support to investigate branch initialization or sequence state |
881, 882, 883 |
Purchase-code validation failed, where that compatibility policy is enabled |
901 |
Device setup is missing or invalid; contact support |
924 |
Invoice already exists; reconcile it with the original transaction |
894 |
Upstream connection failed; retry read operations with backoff and preserve sale idempotency |
Other non-000 codes |
Retain the response and investigate; do not assume every error is retryable |
For support, provide environment, company TIN, branch, invoice number, idempotency key, approximate request time and the response. Redact API keys and unnecessary customer details.
7. Endpoint catalogue
These are the Java-compatible routes. Access and required fields depend on your enabled operations. Initialization and reporting should be coordinated during onboarding.
| Method | Path | Operation |
|---|---|---|
| POST | /initializer/selectInitInfo |
Initialize a registered branch device |
| POST | /code/selectCodes |
Retrieve code catalogue |
| POST | /itemClass/selectItemsClass |
Retrieve item classifications |
| POST | /branches/selectBranches |
Retrieve branch information |
| POST | /branches/saveBrancheUsers |
Register branch user information |
| POST | /branches/saveBrancheInsurances |
Register branch insurance information |
| POST | /branches/saveBrancheCustomers |
Register branch customer information |
| POST | /customers/selectCustomer |
Look up a customer |
| POST | /items/selectItems |
Retrieve items |
| POST | /items/saveItems |
Register/update items |
| POST | /items/saveItemComposition |
Register item composition |
| POST | /imports/selectImportItems |
Retrieve imports |
| POST | /imports/updateImportItems |
Update an import item |
| POST | /stock/selectStockItems |
Retrieve stock movements |
| POST | /stock/saveStockItems |
Record stock movement |
| POST | /stockMaster/saveStockMaster |
Update stock master |
| POST | /trnsPurchase/selectTrnsPurchaseSales |
Retrieve supplier purchase transactions |
| POST | /trnsPurchase/savePurchases |
Record a purchase |
| POST | /trnsSales/saveSales |
Issue a local fiscal receipt |
| POST | /reports/checkZReport |
Check a Z report |
| POST | /reports/saveZReports |
Submit a Z report |
| POST | /notices/selectNotices |
Retrieve notices |
| POST | /test/echoTest |
Connectivity echo |
| GET | /main/selectServerTime |
Retrieve upstream server time |
For raw stock writes, supply the movement number (sarNo), original reference
(orgSarNo), registration type, line count and totals explicitly. The VSDC
compatibility endpoint forwards those values; it does not allocate a replacement
movement number or recalculate your totals. Managed CIS stock workflows construct
these fields on your behalf.
Catalogue lookup example:
{"tin":"999555888","bhfId":"00","lastReqDt":"20180520000000"}
Send that body to /code/selectCodes with a key permitted to read codes. Keep lastReqDt for incremental synchronization according to your integration workflow.
Reporting note for Java migrations
Z-reports include normal, copy, training and proforma groups. Compatibility reports reproduce the supplied WAR’s refund aggregation, including its known defect: a sale of 100 followed by refunds of 30 and 20 produces a reported refund amount of 120 (actual refunds are 50). Rust forwards the same WAR-compatible result. The supplied _9 WAR also has a report endpoint defect: an 8-digit report date returns 910; the required 14-digit date overflows its integer parser and returns 899. The compatibility endpoint reproduces those outcomes. A failed response does not submit a report or close the day.
8. Before live use
Complete sandbox exercises for each branch: normal sale, lost-response retry, duplicate invoice, refund, validation failure and upstream unavailability. Confirm printed receipt fields and reconcile transactions with support. Obtain a production key and separately initialized production branch; change credentials/configuration deliberately. Agreed service limits, retention, support contacts and availability commitments come from your service agreement, not hard-coded assumptions in a client.
Receipt delivery status
Use GET/api/portal/receipts with your X-API-Key to list up to 50 receipts in that key’s company and environment. Pass ?offset=50 for the next page. GET/api/portal/receipts/{id} returns a receipt and its delivery state; another company’s identifier returns 404. ACCEPTED confirms successful RRA delivery. Pending or failed states do not. These endpoints return a { "data": ... } envelope rather than the legacy resultCd envelope.
Development and Production
Your CIS uses one integration contract in both environments. Select the service URL and environment-pinned API key supplied for Development or Production. Development's API name remains sandbox; Production uses production. Browser users use the Environment selector. No request field selects Rust or Tomcat.
VSDC destinations and credentials are deployment settings managed by the administrator. See deployment setup and recovery. Changing an environment uses its separate company/branch fiscal state; it does not transfer receipts, counters or device assignments.
A receipt status of VSDC_ISSUED confirms issuance by the configured VSDC; final RRA delivery must be reconciled with that VSDC. Identical completed retries return the stored result. Changed input under an existing invoice/key returns HTTP 409. For an unknown signing outcome, retain the original request and contact the operator for reconciliation before issuing again.
Raw VSDC response contracts
OpenAPI 1.8.0 includes all 24 compatibility operations. Typed response fields come from Jackson serialization models in the supplied _9 WAR, including nested lists and nullable values. Successful sales describe fiscal receipt counters and signatures. Preserve decimal and 64-bit integer precision in client parsers.
Always inspect resultCd; HTTP 200 can contain a business error, with data or resultDt null or absent where documented. Standalone framework errors use timestamp, status, error and path. Hosted authentication, gateways and proxies can return other envelopes or plain text.
For standalone WAR-compatible deployments, send Accept: application/json. The supplied WAR and standalone Rust adapter can persist an invoice and then return HTTP 406 when the response cannot satisfy the requested media type. Treat that outcome as requiring reconciliation of the original invoice; do not issue a replacement under a new number. See standalone response negotiation. The shipped Python client already requests JSON.
Write routes and server-time passthrough do not enforce a typed upstream data model, so their schemas deliberately keep it flexible. These contracts describe the supplied compatibility target and recorded fixtures; they do not prove full parity, all remote-backend variants or RRA approval.
Monthly receipt activity
Use GET/api/portal/usage?month=2026-03 to read your company's recorded receipt counts in the API key's environment. Omit month for the current Kigali month. The response includes month, timezone, inclusive from, exclusive to_exclusive, environment, basis, totals and by_branch_and_mode inside data. Each group identifies branch and mode (NS, NR, CS, CR, TS, TR, PS or UNKNOWN). Counts are total, accepted, pending and vsdc_issued.
Month selection uses the receipt record's creation time in Africa/Kigali, not its submitted sales date. A completed idempotent replay adds no receipt record. Missing/unrecognized historical modes are counted as UNKNOWN. Accepted/pending apply to the embedded Rust backend; external VSDC records remain vsdc_issued, even if other delivery metadata exists. Delivery status is current when queried, so past-month status counts can change after later delivery. These are activity counts, not API request totals, subscription charges, quota enforcement or immutable billing statements. Use saved CIS reports for their documented reporting purpose.
Questions about onboarding, keys or branches? Contact your EBM Hub onboarding contact.