Current CIS workflow contract
The version 0.2.2 invoice workflow supersedes the earlier basic invoice/refund examples below. Use CIS workflows for receipt profiles, registration, source receipts, inventory and printing.
Use EBM Hub in your browser
Your operator provides your workspace URL and account access. You do not need an API key or coding knowledge to use the browser.
- Sign in. If company signup is open, create an account with your company name, TIN, email and password. The operator verifies your company before approving fiscal operations. To access an existing company, ask the operator to add your user; do not register its TIN again.
- Choose your environment. Start with Sandbox. Live has separate records, API keys and device initialization. A company may use both at once after approval.
- Set up branches. Add the assigned two-digit branch code, branch name and device serial. Initialize each branch in the selected environment after approval.
- Fetch RRA reference lists. Retrieve classifications and common codes through an initialized branch. Use the correct classification, quantity/packaging unit and tax codes when registering goods or services.
- Build your catalogue. Choose Goods, Service or Raw material; enter the code, name, price including tax and reference codes. Service uses item type 3. Save to your catalogue, then select Register with RRA. Saving locally alone is not RRA registration. Entering an existing item code updates that catalogue entry.
- Issue an invoice. Select a branch, enter a unique positive invoice number, choose payment method and add catalogue items and whole-number quantities. Enter customer details and a purchase code where applicable. Review the total, then issue. For a refund, use a new invoice number, identify the original invoice and supply the RRA refund reason code.
- Follow delivery and print. Receipts shows issued records and their RRA delivery state. Pending is not accepted. Open a receipt to inspect and print it. If an invoice response is lost, keep the same tab and use Issue / retry; refreshing the page preserves that tab's pending submission so you can recover the same receipt.
- Close the day. Daily reports currently expose the supplied WAR's date-validation defect. A rejected request does not submit a Z-report or close the day.
API access is optional. An owner can create a named key for a POS/ERP in the API access screen after environment approval. Copy the key when it is displayed; the full value is shown once. Revoke keys that are no longer needed. Your integration can read its own receipt delivery records with GET/api/portal/receipts and details with GET/api/portal/receipts/{id}, using X-API-Key. Lists return up to 50 records; use ?offset=50 for the next page. API keys stay pinned to their assigned environment.
Owners can change company data and issue invoices. Viewers have read-only company access. Use Change password from the sidebar to replace your password; you will be asked to sign in again. For forgotten passwords or access to another company, contact your operator through the agreed support channel.
A successful password change invalidates your existing browser sessions. If another password or account change overlaps your request, the service refuses to overwrite it and asks you to sign in again. Password changes are recorded in the service audit log without passwords or password hashes; the credential update and audit record are saved together. Company integration API keys are managed separately in API access.
The browser currently supports whole-number quantities, prices with two decimal places, and normal sales/refunds. Ask your integration team about the API for fractional quantities or additional transaction modes. Follow your operator's approved receipt-printing instructions before live use.
Integration key permissions and rotation
In API access, owners can choose permissions for each new key. Uncheck all permissions for read-only access to permitted GET operations. Receipt permission covers issuance, print preparation, cash and report creation. Item registration and stock/receiving workflows also require receipt permission alongside their own permission. Reference lookups use code permission. All keys remain restricted to their company and environment; they cannot manage accounts, keys or reconciliation.
The key list shows granted permissions, last use and revocation state. Secret values appear only when created. To rotate a key, create a replacement with the intended permissions, install and verify it in the integration, then revoke the old key. Revocation does not revoke other integration keys.
The list initially shows the newest 100 keys. Select Load older keys to browse further, including revoked or expired credentials. Each older active key retains its Revoke action. A failed page load leaves the displayed keys intact and can be retried.
Owner-session clients can page GET/api/portal/keys using the returned next_before_id: send ?before_id=<next_before_id> until it is null. The response preserves the data array. Keys sort by creation time and identifier descending; creating newer keys during traversal does not shift older pages. Refresh the first page to see new keys. Cursors must identify an existing key in the selected company and environment; invalid or foreign cursors return HTTP 400. Customer API keys cannot use this management endpoint.
For existing owner-session provisioning integrations, key creation still accepts a name-only body and grants the previous four permissions. Supplying scopes: [] explicitly creates a read-only key. Supplied scopes must be among receipts:write, items:write, codes:read and stock:write; duplicate entries are normalized and unsupported permissions are rejected. Customer API keys cannot provision other keys.
Key creation also offers an optional expiry date/time entered in your browser's local timezone. The portal sends an absolute timestamp; the key table displays expiry in Kigali time and labels expired keys. Leave it blank for no automatic expiry. Expired credentials are rejected even if they were previously valid; create a replacement key through your owner session when needed.
Owner-session provisioning accepts optional expires_at as an RFC 3339 timestamp with an explicit timezone, for example 2099-01-01T12:00:00+02:00. It must be in the future. Omitted/null expiry preserves the existing non-expiring behavior. The creation response and key list include expires_at; listing never returns secret key values.
Creation and revocation record the acting user, company, key identifier/name/prefix, permissions, environment and expiry in the service audit log. The log contains neither the secret key nor its hash. The access change and audit entry commit together: an audit failure rolls back the change. Repeating a completed revocation succeeds without changing its original timestamp or adding another audit event. Owners can view these events in API access → Integration-key history. Select Load history to refresh the newest events or Load older events to continue. The table shows when a key was created/revoked, its name/prefix, who changed it, granted permissions and expiry in the selected environment. Historical changes before key auditing was enabled are not reconstructed. Password and other account audit events are not included in this key-history view.
Owner-session integrations can use GET/api/portal/keys/history. It returns a data array of up to 50 events and nullable next_before_id; request ?before_id=<next_before_id> for older events until null. IDs are decimal strings to preserve large-integer precision. Each event includes id, created_at, action, actor_id, actor, environment, key_id, name, prefix, scopes and expires_at. No secret keys, hashes or arbitrary audit details are returned. Company/environment scope is enforced on every page. Viewers and integration API keys receive HTTP 403; an operator can inspect the selected company's history.
Monthly activity
The overview's Monthly receipt activity panel lets you choose a month and inspect recorded receipts by branch and receipt mode in the selected environment. Month boundaries use Kigali time. Replaying an existing receipt does not increase the count. Delivery status reflects the current state; an older month's accepted/pending counts can change after delivery completes. These counts do not set subscription charges or limits.
Recovering an interrupted reconciliation
Owners/operators must first verify the exact uncertain operation against the configured VSDC records. In Stock & receiving, load unresolved operations and record the verified outcome, evidence reference and successful response when applicable.
The portal saves the exact decision and request key in this browser before sending it, scoped to the company, environment and branch. If the response is lost, reopening Stock & receiving shows Recover saved decision. This explicitly replays the saved request to retrieve its committed result; it does not infer acceptance or invent a new decision. A successful recovery clears the saved request. Reload unresolved operations afterwards to refresh the list.
While a saved decision remains unresolved, a different reconciliation for the same branch is blocked in this browser. Recover the original first. Browser storage is local: clearing it or moving to another browser does not carry the recovery request with you. The server's reconciliation audit remains authoritative.
Reviewing recorded reconciliation decisions
In Stock & receiving, select a branch under Reconcile uncertain deliveries and choose Load operations and history. Recorded reconciliations lists the server's decisions newest first. Expand a record to inspect its audit reference, actor ID, evidence, payload fingerprint and attached response. This history is available to authorized owners/operators even in a fresh browser without a saved local request. It is read-only and records the operator's verified decision; its presence alone is not independent proof of RRA acceptance. Reload after submitting or recovering a decision to refresh the history.
Finding reference codes
In Reference data, choose an initialized branch and fetch common codes, item classifications or registered items. Common codes retain both the category ID and category name. Filter the returned rows by category, active/inactive status, or a code/name search; identical code values in different categories remain separate. An omitted usage flag is shown as Unspecified.
These filters apply to the response for the requested update period, not to a complete cached catalogue. An empty result can mean there were no matching updates. Fetch an earlier period when you need older reference records.
Catalogue registration status
The catalogue lists Registration required for saved or edited items that have not yet been registered. Registered items show the recorded timestamp and branch. After successful registration the catalogue refreshes from the server. Every save clears registration status, so register the reviewed current version before invoicing. A timestamp records success with the configured fiscal backend; it does not establish registration in other branches.
Bringing a registered reference item into your catalogue
Fetch Registered items in Reference data and choose Review in catalogue on the desired row. The catalogue form is populated for the branch used in the fetch. Review every field and fill any missing values. Expand Fetched item details to inspect the source, including optional values such as barcode, batch, standard name, safety quantity and group prices that will be preserved. Active/inactive and insurance flags are editable fields; importing an inactive item does not activate it.
Fetching or reviewing does not save or register anything. Choose Save to catalogue after review, then register the saved version. If the item code already exists, inspect the current record and acknowledge replacement before saving. Replacement keeps the item ID and clears its registration status. This is a manual review workflow, not automatic bulk synchronization. Reviewed replacements carry the catalogue version, and new-code drafts use a create-only condition. If another save occurs after review, the server rejects the stale save without overwriting that item. Reload the reference and review the current catalogue record again before saving. The draft is scoped to the current company/environment and is not persisted across a page reload.
Choosing invoice items
Select the branch before choosing invoice items. The picker shows only items that are active and registered for that branch. If none qualify, review and register your catalogue first. Switching branches clears item selections that do not belong to the new branch; quantities and discounts remain editable. Added lines use the same branch restrictions.
The server checks eligibility again when issuing, so an item changed by another user after the screen loaded can still be rejected. Refunds and copies continue to use their immutable original receipt rather than current catalogue selections.
Company staff API foundation
An active owner session can list the company's staff with GET/api/portal/staff and update a person with PATCH/api/portal/staff/{id}. Optional fields are display_name, role (owner, cashier, viewer), is_active, and branches (distinct two-digit codes). Empty branch assignments mean all branches; owners always have all branches. These records are company-wide across environments. API keys and platform-operator sessions cannot use these owner endpoints; operators use the existing company administration routes.
Owners cannot demote or deactivate themselves. The last active owner cannot be removed. Access changes rotate sessions and require the affected person to sign in again. A changed display name does not alter names already saved on receipts. Changes are audited. Owners can use the Staff page to review and edit existing people, including activation and branch assignments. Self-demotion and deactivation controls are disabled for the signed-in owner. Creation and password-reset controls are available on Staff; RRA staff registration is not yet delivered. Cashier sessions can issue normal (NS), training (TS) and proforma (PS) sales and prepare original prints in their currently assigned branches. Refunds, copies and training refunds require an owner. The server checks current branch assignments on each invoice and print attempt, including retries; branch changes end existing sessions. The branch selector lists only assigned branches for cashiers. API-key management and metadata are restricted to owners/operators; cashiers cannot access finance reports or raw fiscal maintenance routes. Integration-key scopes remain unchanged.
Staff creation is available through POST/api/portal/staff with email, display name, role and optional branches. Password reset uses POST/api/portal/staff/{id}/reset-password with no body. Both require an active owner JWT and a persistent X-Idempotency-Key. They return the user ID and a generated temporary password, which the staff member must change after sign-in. Use the existing Change password flow for your own account.
Keep the original request key and body until its outcome is known. Retry the exact request after a lost response; the same owner can recover the same temporary credential for up to 24 hours, provided no password/session change has superseded it. A 409 never automatically resets a password: review the account before deliberately issuing a new reset with a new key. Do not log temporary passwords or store them in browser local storage. The Staff page provides creation and reset controls. Reset requires confirmation. Interrupted requests remain available through Recover original request. The page keeps request metadata, including the staff email, in browser local storage scoped to the company and owner; it never stores the temporary password there. After saving the displayed password, acknowledge it to clear the pending request. If recovery expires, review the account before dismissing the request and deliberately starting a new reset. This feature requires Web Locks support to coordinate multiple browser tabs.
Recovery copies use authenticated encryption bound to company, owner, request key and input, with a separate derived key from the configured EBM_AES_KEY_HEX. This key must remain available for recovery; a key change makes existing encrypted recovery copies unreadable unless migrated. Password hashes remain Argon2. Credential ciphertext retention/cleanup and deployment key rotation still require operational qualification.
Use Activity next to a staff member to choose an inclusive date period and review totals for the selected environment. Net sales use exact integer formatting, including negative totals. The page excludes responses from a previous environment selection. Staff activity is also available through GET/api/portal/staff/{id}/activity?from=YYYY-MM-DD&to=YYYY-MM-DD, with inclusive Kigali dates (maximum 366 days) and the selected environment. It reports issued normal sales/refunds and net sales as a decimal string of integer cents (net_sales_cents), posted stock-movement line counts, cash deposit/withdrawal entry counts, and last sign-in. Receipt periods use the original transaction preparation timestamp. Training, proformas, copies and uncertain receipts are excluded. Historical records without actor information remain unattributed. Receiving reconciliation credits the original initiating actor; the recovery audit separately identifies the resolver. Cash entries are not customer receivable payments.
Company client API
The customer master is separate from the RRA taxpayer lookup cache and is isolated by company and selected environment. GET/api/portal/clients accepts search (literal name/TIN/normalized-phone substring), active, and after; each page contains at most 50 records and next_after. Retain the filters when requesting the next page. GET/api/portal/clients/{id} retrieves the current record.
Owners and cashiers can create a client with POST/api/portal/clients, a persistent X-Idempotency-Key, and name, plus a nine-digit tin or Rwandan mobile phone. Optional fields are email, address, and notes. Phone formats such as 0788 123 456 and +250788123456 normalize to the same identity. TIN and phone are each unique within a company/environment, including inactive records. Same actor/key/normalized input returns the original creation response, even if the client was subsequently edited. Changed input or actor conflicts; use GET for current details.
Owners can replace contact details and activation state using PUT/api/portal/clients/{id} with {"details":{"name":"Client","tin":"123456789","phone":null},"is_active":true} and If-Match: "<version UUID>". Omitted optional contact fields are cleared. A stale version returns 412; reload and review before trying again. There is no deletion operation. Viewers can read clients; API keys cannot use these new customer-master endpoints. Operator sessions can select the company through the existing company-selection header.
The Clients page provides search, active/inactive filters, cursor paging, client creation for owners/cashiers, and owner-only editing or deactivation. Viewers can read contact details. Interrupted creation retains the original client details and request key in browser storage, scoped to the signed-in person, company and environment. Recover original request confirms the result after a reload; Discard after review removes the saved request without undoing any client already created. This coordination requires Web Locks support. Stale edits remain in the form; Reload latest explicitly discards unsaved edits after confirmation.
On New invoice, search for an active saved client and choose it to fill the customer fields. These fields become read-only until you choose Enter customer manually. If saved details change before issuance, the server rejects the unprepared request and reopens review; search again and select the updated client. Already-prepared or issued invoices retain their original customer snapshot when retried. Refunds and copies inherit the original client and customer details, including after later deactivation.
API invoices accept optional client_id. Omit customer fields to use the saved master, or supply values that exactly match it. 409 CLIENT_SELECTION_CHANGED is a definitive pre-issuance rejection; it is different from uncertain fiscal conflicts, which require the original request and key. Without client_id, manual-customer requests remain supported for non-credit sales; new normal credit sales require a saved client.
New clients start with credit disabled and a zero limit. Owners can configure credit through PUT/api/portal/clients/{id}/credit, using the current quoted version in If-Match and a body such as {"credit_allowed":true,"credit_limit_cents":"150000"}. The amount is a string of integer cents (here RWF 1,500.00). A stale version returns 412; reload and review before changing it. Disabling credit or lowering the limit does not cancel already-prepared invoices.
New normal sales with payment code 02 or 03 require an active, credit-enabled saved client, including API-key submissions. Both codes count the whole invoice against credit because the fiscal request has no split amount. Preparation reserves the amount across branches; uncertain issuance retains it. A rejected invoice releases the reservation. Issuance posts the debt atomically with the recorded issued state, and full normal refunds offset the linked credit sale. Training, proforma and copy receipts do not post receivables. Retrying a prepared invoice keeps its original identity and approval even after the client changes.
GET/api/portal/clients/{id}/balance returns decimal strings for balance_cents, pending_credit_cents, credit_limit_cents and available_credit_cents. API keys cannot read these company client records. If history_requires_reconciliation is true, earlier untracked credit invoices make the historical balance incomplete: new credit is blocked until that history is reconciled. The migration does not invent debts or assume old credit invoices are unpaid. Previously recorded invoice requests preserve their original replay behavior.
CREDIT_CLIENT_REQUIRED, CREDIT_NOT_ALLOWED, CREDIT_LIMIT_EXCEEDED and CREDIT_HISTORY_REQUIRES_RECONCILIATION are definitive 409 refusals before preparation. Correct and review the invoice before a new submission. CREDIT_LIMIT_EXCEEDED includes available cents in the response. Uncertain fiscal conflicts still require preserving the original request/key.
Open a client in Clients to see the tracked balance, pending credit invoices, credit limit and available credit. Owners can change credit settings using amounts in RWF with up to two decimal places and a confirmation step. Cashiers and viewers have read-only access. Amounts retain exact cents even for large balances. If another user changes the record, or a save response is lost, use Reload latest and review the saved settings before trying again. Changing credit settings preserves unsaved contact fields. Refresh balance updates the balance without replacing unsaved settings. A historical-reconciliation notice means the tracked amounts are incomplete; do not treat them as the full customer debt.
Customer payments are recorded with POST/api/portal/clients/{id}/payments by owners or cashiers on their assigned collection branch. Send a persistent X-Idempotency-Key and a body such as {"branch":"00","amount_cents":"40000","payment_method":"cash"}. Supported methods are cash, mobile_money, bank, and cheque. Optional reference and note record collection details. paid_on defaults to today's Kigali date and cannot be in the future. Amounts are exact positive integer cents strings. Payments cannot exceed the tracked balance and do not issue another fiscal receipt. Inactive customers can repay existing debt. Earlier untracked credit must be reconciled before recording payments.
Payments settle the oldest posted credit invoices first. Optional allocations explicitly names credit_sale_entry_id and string amount_cents for each distinct invoice; their sum must equal the payment and none can exceed an invoice's open amount. Collection branch identifies where the payment was taken; allocations may settle this customer's invoices across branches.
Owners can correct a payment once through POST/api/portal/payments/{id}/reverse, with a persistent request key and a reason of 5–500 characters. Reversal appends history and reopens the original allocations; entries cannot be edited or deleted. A retry with the same actor, request key, target and body returns the original response, even after a reversal or later balance changes. A different request under the same key returns IDEMPOTENCY_MISMATCH. Do not use a replayed response's historical balance as today's balance; refresh the balance endpoint. API keys and viewers cannot record or reverse payments.
GET/api/portal/clients/{id}/statement?from=YYYY-MM-DD&to=YYYY-MM-DD returns a posted ledger statement. Both bounds are optional and inclusive in Africa/Kigali. Owners, cashiers and viewers can read it; API keys cannot. It returns opening and closing balances plus each entry's debit, credit, running balance, posting timestamp, branch, receipt link, payment date/reference/note, actor and reversal status. All monetary values are decimal strings of integer cents; totals may exceed the maximum size of one ledger entry.
Statement periods use the ledger posting date, never a backdated paid_on. A payment reversed after the selected period is marked as reversed, but its reversal changes the balance only in the period where it was posted. Entries sort by posting timestamp and then entry UUID. Empty periods retain their opening balance. All values in a response share one database snapshot; a later request may reflect later payments. Pending fiscal reservations are excluded. history_requires_reconciliation means these tracked figures are incomplete and cannot establish the customer's full historical debt.
Open a client in Clients to view the Statement section. Leave dates blank for all posted history, or choose inclusive Kigali posting dates and select Show statement. Editing dates disables print until that period loads. Preview / print statement opens the exact displayed period, with company/client identity, workspace, opening and closing balances, and debit/credit/running-balance columns. The browser print dialog can print or save a PDF; A4 is suitable for the statement table. Cashiers and viewers can read and print statements. Historical-incompleteness notices and payment-date explanations appear in both the view and printout. This is a customer statement, not a fiscal receipt. Navigation, environment changes and logout clear the shared print preview.
Owners and cashiers can use Record a payment on the client page: choose the collection branch, enter the exact RWF amount, method and optional reference/payment date/note, then review the confirmation. The server enforces branch assignment and the current balance. The form allocates payments oldest-first. Owners can use Correct a payment below the statement to reverse an unreversed payment with a reason and confirmation; cashiers and viewers do not see reversal controls.
The browser saves a payment or reversal request before sending it, scoped to company, environment, signed-in user and client. After a lost response or reload, choose Recover original request to retry the original key and body. A confirmed request remains visible until Start another operation is selected; this prevents another tab's queued submit from creating a second collection. Balances and statements refresh from the server after confirmation. A pending request can be discarded only after reviewing the statement and confirming its outcome; discarding never undoes a payment and submitting a new request may otherwise duplicate it. Saved request details remain in this browser until acknowledged or discarded; credentials are not saved there. Use a browser supporting Web Locks for payment coordination.
On Clients, select Show / refresh aging to review all tracked open invoices and customer credits, including inactive clients. Select a client in the table to open their details and statement. The columns show 0–30, 31–60, 61–90 and over-90-day invoice amounts, total open invoices, customer credit and net balance. These are ages since the credit sale was posted in Kigali, not contractual due dates. Pending fiscal reservations are excluded. Recording or reversing a payment refreshes a visible aging table.
GET/api/portal/receivables/aging provides the same report to owner/cashier/viewer sessions, with API keys denied. It returns the current Kigali as_of date, per-client rows and aggregate totals from one snapshot. All monetary values are decimal strings. Remaining invoice amounts subtract refunds and allocations from unreversed payments. If a collected invoice is refunded, its customer credit appears separately rather than being silently assigned to a different invoice. total_open_cents - customer_credit_cents = balance_cents. Clients with neither open invoices nor credit are omitted. Historical-reconciliation warnings mean these tracked amounts are incomplete. This is current aging, not a historical as-of reconstruction.
RRA branch-customer registration and the historical reconciliation workflow remain unfinished. Do not treat this release as a complete receivables workflow.
Questions about onboarding, keys or branches? Contact your EBM Hub onboarding contact.