Documented
nowhere
else.
What it cost real fiscal days to learn, shipped as defaults and pinned by 130 tests.
Talk straight to ZIMRA's fiscal backend with your own device certificate. No middleware. No per-receipt fees. Every receipt signed, chained and accepted.
ECDSA P-256 · SHA-256 · ASN.1 DER
The canonical signing string, cent-exact, with every tax concatenated in the order
FDMS expects. Get one byte wrong and ZIMRA answers RCPT020 with no further detail.
previousReceiptHash → receiptHash
Each receipt carries the hash of the one before it. The SDK keeps that chain on disk, survives a crash mid-request and reconciles on restart, so every global number lands exactly once.
7C3A-91F0-4BD2-E865
A verification code and QR on every slip, in the exact format ZIMRA's portal checks. Live against FDMS: zero validation errors.
Release week, on X
Every line on this slip is computed by the SDK. Here is what goes into each one.
Name, TIN and branch address come from getConfig(), never typed by hand, so they always
match what ZIMRA registered.
Fiscal day, receipt counter and global number, persisted across restarts. A gap here is
RCPT012, a Red error that locks the day.
Amounts are whole units or cents("11.50"). A float like 11.4999 is refused, not rounded,
so a signature never drifts.
Accumulated per tax, currency and money type, then sorted in the enum order FDMS verifies, Cash before Card. Not alphabetical.
The first 16 hex characters of an MD5 over the device's DER signature, grouped in fours. Read it out and the customer can check it by hand.
Portal URL, 10-digit device ID, date, 10-digit global number and that same hash. Scan it and ZIMRA says Invoice is valid.
01 / 09
Mutual TLS both ways. The device proves who it is with its own certificate, and FDMS answers with the tax table and the QR base URL.
Day 20 opens. Counters reset, the receipt counter starts at zero and a fresh hash chain begins.
120.00 becomes 12000 before anything is signed. A float like 119.9999 is refused, not rounded.
Eight fields joined in the exact order FDMS rebuilds them on its side. One byte off and the signature fails.
The string is hashed. This hash is what the next receipt carries as its previousReceiptHash.
ECDSA with the device's P-256 key, wrapped in ASN.1 DER. The raw format WebCrypto produces is rejected.
Receipt 29 carries receipt 28's hash. Break one link and every receipt after it comes back Red.
MD5 over the signature, first 16 hex characters. That is the code on the slip and the tail of the QR.
Counters sorted the way FDMS checks them, Cash before Card, then signed. Day 20 closes clean.
import { FiscalDevice } from "zimra-fdms";
const device = new FiscalDevice(id, creds);
await device.getConfig();
await device.openDay();
const sale = await device.submitReceipt({
currency: "USD", invoiceNo: "INV-0029",
lines: [{ name: "FDMS integration audit",
price: 120, quantity: 1, taxId: 513 }],
payments: [{ moneyType: "Cash",
amount: 120 }],
});
print(sale.verificationCode, sale.qrData);
await device.closeDay();
The package is also a Model Context Protocol server. Claude Code, or any MCP client, runs the same day through eight typed tools, on the same profile and the same hash chain.
open a fiscal day and ring up two loaves of bread, cash
open_fiscal_dayDay 8submit_receipt$5.00 acceptedWhat it cost real fiscal days to learn, shipped as defaults and pinned by 130 tests.
Signatures must be ASN.1 DER.
WebCrypto produces raw P1363 signatures and FDMS rejects them without saying why. The SDK converts to DER by default. It is the difference between Invoice is valid and a day of RCPT020.
Global numbers never skip.
One gap and the receipt goes Red, and a day with a Red receipt cannot be closed by the device. The SDK writes a pending marker before every submit and keeps its own high-water mark.
Time only moves forward.
A receipt dated before the previous one is Red. A future date is only Yellow, but it quietly breaks
lastReceiptGlobalNo in GetStatus. The SDK dates receipts from a clock corrected against FDMS's
own, and nudges same-second receipts forward.
Counter order is enum order.
BalanceByMoneyType counters sort by enum position, not alphabetically. Swap one pair and the whole day-close signature fails.
The CSR subject is exact.
The certificate request CN is ZIMRA-serial-id with the device ID zero-padded to ten digits.
Anything else and registration fails.
The rest of what FDMS never wrote down.
Everything an FDMS integration needs, in one MIT package. Runs on Node, Bun, Deno, browsers and React Native, with the device key sealed in Android Keystore.
zimra-fdms · v0.4.1
ECDSA P-256 keys, ZIMRA's exact CSR subject, renewal before expiry.
Plain node:https, one keep-alive agent per device, retries with jitter.
Canonical strings, cent-exact rounding, DER signatures, hash chain.
Open, accumulate, close, and recover a lost day from server counters.
Append-only journal, flushed in order inside the 72-hour grace window.
Pending-submit markers and reconcile(). Every global number exactly once.
Colour, cause, fix, and whether the day can still close.
npx zimra-fdms-simulator, real mutual TLS, replays live validation codes.
Port it to any language and prove it signs exactly like FDMS expects.
Run the day from a terminal or from an agent. Same profile, same chain.
MIT licensed · no per-receipt fees · no middleware
Going live still needs ZIMRA onboarding: sample-document approval, live device registration, ITF263 and ongoing compliance. Goko Consultancy & Training Services handles that path end to end.
Talk to Goko