Fiscalise every sale.

Talk straight to ZIMRA's fiscal backend with your own device certificate. No middleware. No per-receipt fees. Every receipt signed, chained and accepted.

Read the docs

Signed.

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.

Chained.

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.

Verified.

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.

Anatomy of a
fiscal receipt.

Every line on this slip is computed by the SDK. Here is what goes into each one.

  1. 01
    Taxpayer header

    Name, TIN and branch address come from getConfig(), never typed by hand, so they always match what ZIMRA registered.

  2. 02
    Counters and numbers

    Fiscal day, receipt counter and global number, persisted across restarts. A gap here is RCPT012, a Red error that locks the day.

  3. 03
    Lines in integer cents

    Amounts are whole units or cents("11.50"). A float like 11.4999 is refused, not rounded, so a signature never drifts.

  4. 04
    Tax counters

    Accumulated per tax, currency and money type, then sorted in the enum order FDMS verifies, Cash before Card. Not alphabetical.

  5. 05
    Verification code

    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.

  6. 06
    The QR

    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

  1. getConfig()

    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.

  2. openDay()

    Day 20 opens. Counters reset, the receipt counter starts at zero and a fresh hash chain begins.

  3. Integer cents

    120.00 becomes 12000 before anything is signed. A float like 119.9999 is refused, not rounded.

  4. The signing string

    Eight fields joined in the exact order FDMS rebuilds them on its side. One byte off and the signature fails.

  5. SHA-256

    The string is hashed. This hash is what the next receipt carries as its previousReceiptHash.

  6. Signed, in DER

    ECDSA with the device's P-256 key, wrapped in ASN.1 DER. The raw format WebCrypto produces is rejected.

  7. Chained

    Receipt 29 carries receipt 28's hash. Break one link and every receipt after it comes back Red.

  8. Code and QR

    MD5 over the signature, first 16 hex characters. That is the code on the slip and the tail of the QR.

  9. closeDay()

    Counters sorted the way FDMS checks them, Cash before Card, then signed. Day 20 closes clean.

TSpos.ts src / checkout
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();

Or just ask.

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.

  • Claude
  • Codex
  • Gemini CLI
  • Cursor
  • Copilot
  • Windsurf
You

open a fiscal day and ring up two loaves of bread, cash

open_fiscal_dayDay 8
submit_receipt$5.00 accepted
FISCAL INVOICE Bread × 25.00 TOTAL USD5.00

Documented
nowhere
else.

What it cost real fiscal days to learn, shipped as defaults and pinned by 130 tests.

Red

RCPT020

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.

Red

RCPT012

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.

Red

RCPT030

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.

Day close

Cash<Card

Counter order is enum order.

BalanceByMoneyType counters sort by enum position, not alphabetically. Swap one pair and the whole day-close signature fails.

Registration

ZIMRA-
0000037367

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.

Docs

+ more

The rest of what FDMS never wrote down.

0validation errors, live on ZIMRA's test FDMS
0hoffline window, queued and flushed in order
0dependencies in the fiscal core
0tests, including killing the device mid-write

What you get,
itemised.

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

  • Device registration & CSR✓

    ECDSA P-256 keys, ZIMRA's exact CSR subject, renewal before expiry.

  • mTLS transport✓

    Plain node:https, one keep-alive agent per device, retries with jitter.

  • Receipt signing✓

    Canonical strings, cent-exact rounding, DER signatures, hash chain.

  • Fiscal day lifecycle✓

    Open, accumulate, close, and recover a lost day from server counters.

  • Offline queue✓

    Append-only journal, flushed in order inside the 72-hour grace window.

  • Crash safety✓

    Pending-submit markers and reconcile(). Every global number exactly once.

  • Errors that explain themselves✓

    Colour, cause, fix, and whether the day can still close.

  • Local FDMS simulator✓

    npx zimra-fdms-simulator, real mutual TLS, replays live validation codes.

  • Conformance vectors✓

    Port it to any language and prove it signs exactly like FDMS expects.

  • CLI + MCP server✓

    Run the day from a terminal or from an agent. Same profile, same chain.

TOTAL$0.00

MIT licensed · no per-receipt fees · no middleware

The code is solved.
The paperwork is human.

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