Unofficial Python wrapper for the ZIMRA FDMS API by Tarmica Chiwara
Project description
# 🇿🇼 zimra
**Unofficial Python wrapper for the ZIMRA Fiscal Device Management System (FDMS) API**
[](https://pypi.org/project/zimra/)
[](https://www.python.org/downloads/)
[](LICENSE)
*Simplifying fiscal device integration for Zimbabwean businesses*
---
## What is this?
`zimra` is a Python library that handles the complexity of communicating with ZIMRA's Fiscal Device Management System. If you're building a POS system, e-commerce platform, or any application that needs to issue fiscal receipts in Zimbabwe — this library does the heavy lifting for you.
**What it handles for you:**
- 🔐 Device registration & certificate management (mTLS)
- 🧮 Automatic VAT calculation (tax-inclusive **and** tax-exclusive)
- 📝 Receipt preparation, signing (SHA256 + RSA PKCS#1 v1.5), and submission
- 📅 Fiscal day lifecycle (open → submit receipts → close)
- 📱 QR code generation for receipt verification
- ✅ Support for fiscal invoices, credit notes, and debit notes
---
## Table of Contents
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Step 1 — Register Your Device](#step-1--register-your-device)
- [Step 2 — Initialize the Device](#step-2--initialize-the-device)
- [Step 3 — Open a Fiscal Day](#step-3--open-a-fiscal-day)
- [Step 4 — Prepare & Submit a Receipt](#step-4--prepare--submit-a-receipt)
- [Step 5 — Close the Fiscal Day](#step-5--close-the-fiscal-day)
- [Tax-Exclusive Receipts](#tax-exclusive-receipts)
- [Credit Notes & Debit Notes](#credit-notes--debit-notes)
- [Exempt & Mixed Tax Items](#exempt--mixed-tax-items)
- [QR Code Generation](#qr-code-generation)
- [Utility Functions](#utility-functions)
- [API Reference](#api-reference)
- [Supported Tax Rates (2026)](#supported-tax-rates-2026)
- [Error Handling](#error-handling)
- [Contributing](#contributing)
- [License](#license)
---
## Installation
### From PyPI (recommended)
```bash
pip install zimra
```
### From source
```bash
git clone https://github.com/lordskyzw/zimra.git
cd zimra
pip install .
```
### Dependencies
The library requires the following packages (installed automatically):
| Package | Purpose |
|---------|---------|
| `requests` | HTTP communication with ZIMRA API |
| `pycryptodome` | RSA signing of receipts (PKCS#1 v1.5) |
| `cryptography` | Certificate generation (CSR, key pairs) |
| `colorama` | Colored logging output |
| `requests_toolbelt` | Enhanced HTTP utilities |
---
## Quick Start
This walkthrough takes you from a fresh device to submitting your first fiscal receipt. We'll use **test mode** throughout — switch to `test_mode=False` and `prod=True` when you're ready for production.
### Step 1 — Register Your Device
Before anything else, you need to register your fiscal device with ZIMRA. This generates an RSA key pair and exchanges a CSR for a signed certificate.
```python
from zimra import register_new_device
register_new_device(
fiscal_device_serial_no="9029D38C011B", # Your device's serial number
device_id="10626", # Device ID assigned by ZIMRA
activation_key="00398834", # 8-digit activation key from ZIMRA
folder_name="certs", # Directory to save certificate & key
certificate_filename="my_certificate", # Output: certs/my_certificate.crt
private_key_filename="my_private_key", # Output: certs/my_private_key.key
prod=False # False = test environment
)
```
> **📁 After running this, you'll have two files:**
> - `certs/my_certificate.crt` — your device certificate (issued by ZIMRA)
> - `certs/my_private_key.key` — your RSA private key (keep this secure!)
### Step 2 — Initialize the Device
Create a `Device` instance using your credentials and certificate files:
```python
from zimra import Device
device = Device(
device_id="10626",
serialNo="9029D38C011B",
activationKey="00398834",
cert_path="certs/my_certificate.crt",
private_key_path="certs/my_private_key.key",
test_mode=True, # True = sandbox, False = production
deviceModelName="Server", # Must match what ZIMRA has on record
deviceModelVersion="v1",
company_name="MyCompany"
)
```
> ⚠️ **Important:** The `deviceModelName` must exactly match what's registered with ZIMRA. A mismatch will result in `403 Forbidden` errors.
You can verify connectivity and check your device configuration:
```python
# Check device status (fiscal day state, etc.)
status = device.getStatus()
print(status)
# {'fiscalDayNo': 1, 'fiscalDayStatus': 'FiscalDayClosed', ...}
# Get full device configuration (taxpayer info, applicable taxes, etc.)
config = device.getConfig()
print(config)
# {'taxPayerName': 'MY COMPANY', 'taxPayerTIN': '...', 'applicableTaxes': [...], ...}
# Simple connectivity test
ping = device.ping()
print(ping)
```
### Step 3 — Open a Fiscal Day
A fiscal day must be opened before you can submit any receipts. The `fiscalDayNo` is a sequential integer that must be greater than the last closed day.
```python
result = device.openDay(fiscalDayNo=1)
print(result)
# {'fiscalDayNo': 1, 'operationID': '0HN4FDK6T1CNI:00000001'}
```
> The library automatically checks that the current fiscal day is closed before opening a new one. If day 1 is already open, it will return an error message.
### Step 4 — Prepare & Submit a Receipt
This is the core workflow. You provide a simplified receipt object, and the library:
1. Validates all mandatory fields
2. Formats receipt lines to ZIMRA's specification
3. Calculates taxes automatically (VAT extraction from inclusive prices)
4. Signs the receipt with your private key
5. Returns a fully prepared payload ready for submission
#### Build the receipt data:
```python
from datetime import datetime
receipt_data = {
"receiptType": "FISCALINVOICE", # FISCALINVOICE | CREDITNOTE | DEBITNOTE
"receiptCurrency": "USD", # USD | ZWG
"receiptCounter": 1, # Sequential counter per receipt type
"receiptGlobalNo": 1, # Global sequential number
"invoiceNo": "INV-001", # Your unique invoice number
"receiptDate": datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
"receiptLines": [
{
"item_name": "Widget A",
"tax_percent": 15.5, # Standard VAT rate (2026)
"quantity": 2,
"unit_price": 25.00
},
{
"item_name": "Widget B (Zero-rated)",
"tax_percent": 0, # Zero-rated item
"quantity": 1,
"unit_price": 10.00
}
],
"receiptPayments": [{
"moneyTypeCode": 0, # 0 = Cash, 1 = Card
"paymentAmount": 60.00 # Total payment amount
}]
}
```
#### Prepare and submit:
```python
# prepareReceipt handles tax calculation, formatting, and signing
prepared = device.prepareReceipt(receipt_data)
# Submit to ZIMRA
response = device.submitReceipt(prepared)
print(response)
```
> 💡 **Tip:** The `prepareReceipt` method assumes prices are **tax-inclusive** (prices already include VAT). For tax-exclusive pricing, see [Tax-Exclusive Receipts](#tax-exclusive-receipts).
### Step 5 — Close the Fiscal Day
At the end of the business day, close the fiscal day with a summary of the day's counters:
```python
result = device.closeDay(
fiscalDayNo=1,
fiscalDayDate="2026-02-22",
lastReceiptCounterValue=5, # Total receipts submitted today
fiscalDayCounters=[ # Summary of the day's transactions
{
"fiscalCounterType": "SaleByTax",
"fiscalCounterCurrency": "USD",
"fiscalCounterTaxPercent": 15.5,
"fiscalCounterTaxID": 515,
"fiscalCounterValue": 500.00
},
{
"fiscalCounterType": "BalanceByMoneyType",
"fiscalCounterCurrency": "USD",
"fiscalCounterMoneyType": 0,
"fiscalCounterValue": 500.00
}
]
)
print(result)
```
> To close an empty day (no receipts), pass `lastReceiptCounterValue=None` and `fiscalDayCounters=[]`.
---
## Tax-Exclusive Receipts
If your prices **do not** include VAT, use `prepareReceiptTaxExclusive` instead. The library will calculate VAT on top of the base prices and adjust the receipt total accordingly.
```python
receipt_data = {
"receiptType": "FISCALINVOICE",
"receiptCurrency": "USD",
"receiptCounter": 1,
"receiptGlobalNo": 2,
"invoiceNo": "INV-002",
"receiptDate": datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
"receiptLines": [
{
"item_name": "Service A",
"tax_percent": 15.5, # VAT will be calculated ON TOP of this price
"quantity": 1,
"unit_price": 100.00 # Pre-tax price
}
],
"receiptPayments": [{
"moneyTypeCode": 0,
"paymentAmount": 115.50 # base + VAT
}]
}
prepared = device.prepareReceiptTaxExclusive(receipt_data)
response = device.submitReceipt(prepared)
```
**Key difference:**
| Method | `unit_price` means | VAT calculation |
|--------|-------------------|-----------------|
| `prepareReceipt` | Price **including** VAT | VAT extracted: `total × rate / (1 + rate)` |
| `prepareReceiptTaxExclusive` | Price **excluding** VAT | VAT added: `total × rate` |
---
## Credit Notes & Debit Notes
To issue a credit note (refund) or debit note, set `receiptType` accordingly and include the required `creditDebitNote` and `receiptNotes` fields:
```python
credit_note = {
"receiptType": "CREDITNOTE",
"receiptCurrency": "USD",
"receiptCounter": 1,
"receiptGlobalNo": 3,
"invoiceNo": "CN-001",
"receiptDate": datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
"receiptNotes": "Refund for damaged goods",
"creditDebitNote": {
"receiptID": 12345, # Original receipt ID from ZIMRA
"deviceID": 10626,
"receiptGlobalNo": 1, # Original receipt's global number
"fiscalDayNo": 1
},
"receiptLines": [
{
"item_name": "Widget A (Returned)",
"tax_percent": 15.5,
"quantity": 1,
"unit_price": 25.00
}
],
"receiptPayments": [{
"moneyTypeCode": 0,
"paymentAmount": 25.00
}]
}
prepared = device.prepareReceipt(credit_note)
response = device.submitReceipt(prepared)
```
---
## Exempt & Mixed Tax Items
You can mix different tax rates in a single receipt, including exempt items:
```python
receipt_data = {
# ... standard fields ...
"receiptLines": [
{
"item_name": "Standard item",
"tax_percent": 15.5, # Standard 15.5% VAT
"quantity": 1,
"unit_price": 50.00
},
{
"item_name": "Reduced rate item",
"tax_percent": 5, # 5% withholding tax
"quantity": 1,
"unit_price": 30.00
},
{
"item_name": "Zero-rated item",
"tax_percent": 0, # Zero-rated
"quantity": 1,
"unit_price": 20.00
},
{
"item_name": "Exempt item",
"tax_percent": "exempt", # Use string "exempt" or "E"
"quantity": 1,
"unit_price": 15.00
}
],
# ...
}
```
> The library automatically groups items by tax rate, calculates consolidated tax amounts per group, and assigns the correct ZIMRA `taxID` for each.
---
## QR Code Generation
After submitting a receipt, generate a QR code URL for receipt verification:
```python
# After a successful submitReceipt call:
qr_url = device.generate_qr_code(
signature=prepared["receiptDeviceSignature"]["signature"],
receipt_global_no=1,
receipt_date=datetime.now().date()
)
print(qr_url)
# https://fdmstest.zimra.co.zw/0000010626220220260000000001
```
This URL can be encoded into a QR code on printed receipts for customers to verify the receipt with ZIMRA.
---
## Utility Functions
### Standalone Tax Calculator
Calculate VAT without initializing a `Device`:
```python
from zimra import tax_calculator
# Extract VAT from a tax-inclusive price
# Formula: VAT = total - (total / (1 + rate/100))
vat = tax_calculator(sale_amount=115.50, tax_rate=15.5)
print(vat) # 15.5
```
### Receipt Preprocessing
If you need to sanitize receipt data before preparing it:
```python
from zimra import preprocess_receipt, preprocess_tax_exclusivereceipt
# Ensures all numeric fields are properly formatted strings
# Calculates line_total for each receipt line
cleaned = preprocess_receipt(raw_receipt_data)
prepared = device.prepareReceipt(cleaned)
```
---
## API Reference
### `register_new_device()`
Registers a new device with ZIMRA and saves the certificate + private key locally.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `fiscal_device_serial_no` | `str` | *required* | Device serial number |
| `device_id` | `str` | *required* | ZIMRA-assigned device ID |
| `activation_key` | `str` | *required* | 8-digit activation key |
| `model_name` | `str` | `'Server'` | Device model name |
| `folder_name` | `str` | `'prod'` | Output directory for cert/key files |
| `certificate_filename` | `str` | `'certificate'` | Certificate filename (without extension) |
| `private_key_filename` | `str` | `'decrypted_key'` | Private key filename (without extension) |
| `prod` | `bool` | `False` | `True` = production, `False` = test |
### `Device` Class
#### Constructor
```python
Device(
device_id, serialNo, activationKey,
cert_path, private_key_path,
test_mode=True, deviceModelName='Server',
deviceModelVersion='v1', company_name='NexusClient'
)
```
#### Methods
| Method | Description | Returns |
|--------|-------------|---------|
| `getConfig()` | Fetch device configuration (taxpayer info, applicable taxes) | `dict` |
| `getStatus()` | Get current fiscal day status | `dict` |
| `ping()` | Connectivity test to ZIMRA server | `dict` |
| `openDay(fiscalDayNo)` | Open a new fiscal day | `dict` |
| `prepareReceipt(receiptData, ...)` | Prepare a tax-**inclusive** receipt for submission | `OrderedDict` |
| `prepareReceiptTaxExclusive(receiptData, ...)` | Prepare a tax-**exclusive** receipt for submission | `OrderedDict` |
| `submitReceipt(receiptData)` | Submit a prepared receipt to ZIMRA | `dict` |
| `closeDay(fiscalDayNo, fiscalDayDate, lastReceiptCounterValue, fiscalDayCounters)` | Close the current fiscal day | `dict` |
| `generate_qr_code(signature, receipt_global_no, receipt_date)` | Generate a QR code verification URL | `str` |
| `renewCertificate()` | Renew the device certificate | `str` |
---
## Supported Tax Rates (2026)
| Tax Rate | Tax ID | Description |
|----------|--------|-------------|
| `0` | `2` | Zero-rated (0%) |
| `"exempt"` or `"E"` | `3` | Exempt |
| `5` | `514` | Non-VAT Withholding Tax (5%) |
| `15.5` | `515` | Standard VAT (15.5%) — *current rate* |
| `15` | Legacy | Supported for backward compatibility |
---
## Error Handling
The library raises clear errors for common issues:
```python
from zimra import ZimraServerError
try:
prepared = device.prepareReceipt(receipt_data)
response = device.submitReceipt(prepared)
except ValueError as e:
# Missing mandatory fields, invalid date format, invalid tax_percent, etc.
print(f"Validation error: {e}")
except ZimraServerError as e:
# ZIMRA server returned an error
print(f"Server error {e.status_code}: {e}")
except Exception as e:
print(f"Unexpected error: {e}")
```
**Common gotchas:**
- `deviceModelName` must match exactly what ZIMRA has registered — otherwise you get `403`
- Fiscal day numbers must be sequential and the previous day must be closed
- `receiptGlobalNo` must be globally unique and sequential
- Certificate files must be accessible and valid
---
## Contributing
Contributions are welcome! This project is actively maintained and there are many ways to help:
- 🐛 **Bug reports** — Open an issue with steps to reproduce
- 💡 **Feature requests** — Suggest improvements or new functionality
- 🔧 **Pull requests** — Submit code changes
```bash
git clone https://github.com/lordskyzw/zimra.git
cd zimra
pip install -e . # Install in editable mode
```
---
## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
---
**Built with ❤️ by [Tarmica Chiwara](https://github.com/lordskyzw)**
*Alleviating complexity from Zimbabwean businesses*
]]>
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
zimra-0.0.5.tar.gz
(25.4 kB
view details)
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
zimra-0.0.5-py3-none-any.whl
(19.6 kB
view details)
File details
Details for the file zimra-0.0.5.tar.gz.
File metadata
- Download URL: zimra-0.0.5.tar.gz
- Upload date:
- Size: 25.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cfd559ac87b777802b04f1c1ca6695c67ee19c001dd6ca77ad593eddc89ab2e0
|
|
| MD5 |
ce9e1813a9067c21139ca287a37e6285
|
|
| BLAKE2b-256 |
456c796d435026db7979aaba4d65d213222c75201e25db68e1365932d9672c76
|
File details
Details for the file zimra-0.0.5-py3-none-any.whl.
File metadata
- Download URL: zimra-0.0.5-py3-none-any.whl
- Upload date:
- Size: 19.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
31ac45c3c511c2a1dfdfa4b6da533932cf4ddb5a48dd64a00938b21fe116b347
|
|
| MD5 |
d823bac2510ed06176ac0b79a18dda79
|
|
| BLAKE2b-256 |
a50137cf12a288e09c34d38d5d7e93e584c863a8ce253c391bc41e7346a60a9e
|