# QRFRS Public API v1 — Fiscalise any POS receipt

Base URL: `https://qrfiscalreceipt.com`

Any point-of-sale, ERP or e-commerce system can send a completed sale to QRFRS,
have it fiscalised with ZIMRA, and get back a printable receipt — 80mm/58mm
ESC/POS bytes for a thermal printer, or an A4 PDF.

---

## 0. Get an FDMS test device (do this first)

Test devices are provisioned by QRFRS, not self-service. Email
**support@qrfiscalreceipt.com** with the merchant name, TIN and VAT number and we
register a ZIMRA test device, open a fiscal day, create the portal account and
issue a test API token — usually within one business day.

Certify the following on the test device before go-live: a normal sale, a
multi-line sale, zero-rated and exempt lines, a multi-currency sale, a credit
note, and a retry of the same `receipt_number`.

---

## 1. Authentication


Every request carries the merchant's API token:

```
Authorization: Bearer <api_token>
```

The merchant generates the token in their QRFRS portal under
**Settings → External API**, and hands it to you. Tokens are per-merchant and
revocable; rotating one immediately invalidates the old value. `X-Api-Key` is
accepted as an alternative header.

---

## 2. Fiscalise a receipt (synchronous)

```
POST /api/public/v1/receipts?formats=escpos80
Content-Type: application/json
Authorization: Bearer <api_token>
```

### Request body (canonical schema)

```json
{
  "receipt_number": "INV-000123",
  "receipt_date": "2026-08-28T10:15:00Z",
  "receipt_type": "SALE",
  "currency": "USD",
  "branch_id": "SHOP-01",
  "device_id": "TILL-2",
  "buyer": {
    "name": "Acme Pvt Ltd",
    "tin": "2001507754",
    "vat_number": "220121106",
    "address": "16 Greendale Ave",
    "city": "Harare",
    "email": "accounts@acme.co.zw",
    "phone": "+263771234567"
  },
  "lines": [
    {
      "description": "Baby food puree",
      "hs_code": "21041000",
      "quantity": 5,
      "unit_price": 1.0,
      "line_total": 5.0,
      "tax_code": "A",
      "tax_rate": 15
    }
  ],
  "payments": [{ "method": "Cash", "amount": 5.0, "currency": "USD" }],
  "total": 5.0
}
```

| Field | Required | Notes |
|---|---|---|
| `receipt_number` | yes | Unique per merchant. Used as the idempotency key. |
| `receipt_date` | no | ISO 8601. Defaults to now. |
| `receipt_type` | no | `SALE` (default) or `CREDIT_NOTE`. |
| `currency` | no | `USD`, `ZWG`, … Defaults to the device currency logic. |
| `branch_id` | no | Shop identifier. Required for multi-branch merchants. |
| `device_id` | no | Till / terminal identifier. |
| `original_receipt_number` | credit notes | The receipt being credited. |
| `buyer` | no | Name **and** TIN must both be present for buyer details to print. |
| `lines[]` | yes | `description`, `line_total` required; `hs_code`, `tax_code`/`tax_rate` strongly recommended. |
| `payments[]` | no | Defaults to a single Cash payment for the total. |
| `total` | no | Defaults to the sum of `line_total`. |

`formats` query parameter accepts a comma list: `escpos80`, `escpos58`.
Omit it if you only want the fiscal metadata plus fetch URLs.

### Success response `200`

```json
{
  "receipt_id": "0f0b…",
  "receipt_number": "INV-000123",
  "status": "fiscalised",
  "fiscal_day_no": 42,
  "receipt_global_no": 1187,
  "receipt_counter": 63,
  "verification_code": "A1B2-C3D4-E5F6",
  "qr_url": "https://receipt.zimra.org/...",
  "fiscalised_at": "2026-08-28T10:15:02Z",
  "pdf_url": "https://qrfiscalreceipt.com/api/public/v1/receipts/0f0b…/pdf",
  "escpos_url": "https://qrfiscalreceipt.com/api/public/v1/receipts/0f0b…/escpos",
  "escpos_80_base64": "G0AbYQE…",
  "escpos_mime": "application/octet-stream"
}
```

`status` is `fiscalised` on first submission and `duplicate` when the same
`receipt_number` is posted again — the original fiscal data is returned, never a
second ZIMRA submission.

When the receipt's branch or till is switched off in the merchant's portal the
response is `200` with `{"status":"skipped","reason":"SOURCE_DISABLED"}`.

### Error response

```json
{ "status": "failed", "error": { "code": "DAY_CLOSED", "message": "No fiscal day is open" } }
```

| Code | HTTP | Meaning |
|---|---|---|
| `UNAUTHORIZED` | 401 | Missing/invalid API token. |
| `INVALID_PAYLOAD` | 400 / 422 | Malformed JSON or schema violation (`issues[]` lists the fields). |
| `DEVICE_NOT_CONFIGURED` | 412 | Merchant has no active ZIMRA device. |
| `DAY_CLOSED` | 409 | No fiscal day is open. |
| `MISSING_ORIGINAL_RECEIPT` | 422 | Credit note without a valid `original_receipt_number`. |
| `INVALID_TAX` | 422 | Tax code/rate not registered on the device. |
| `DEVICE_BUSY` | 422 | Another receipt is submitting; retry shortly. |
| `ZIMRA_UNAVAILABLE` | 502 | ZIMRA unreachable; retry with the same `receipt_number`. |
| `FISCALISATION_FAILED` | 422 | Other rejection; `message` carries ZIMRA's reason. |
| `SERVER_ERROR` | 500 | Unexpected failure. |

Retries are safe: the `receipt_number` idempotency key prevents duplicates.

---

## 3. Fetch a receipt later

```
GET /api/public/v1/receipts/{receipt_id|receipt_number}
GET /api/public/v1/receipts/{id}/escpos?width=80|58        → raw bytes
GET /api/public/v1/receipts/{id}/escpos?width=58&format=json → base64 JSON
GET /api/public/v1/receipts/{id}/pdf                        → A4 PDF
```

All accept the same `Authorization: Bearer <api_token>` header.

---

## 4. Webhook push (optional async path)

If the merchant configures an outbound webhook URL, QRFRS POSTs to it whenever a
receipt is fiscalised — including receipts created outside your system. The body
carries the same fiscal fields plus `pdf_base64`, `escpos_base64` and
`thermal_url`.

Verify the signature before trusting the body:

```
X-QRFRS-Signature: sha256=<hex hmac of the raw body, key = webhook secret>
```

```js
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
if (`sha256=${expected}` !== req.headers["x-qrfrs-signature"]) return res.sendStatus(401);
```

---

## 5. Printing the ESC/POS payload

```js
const bytes = Buffer.from(res.escpos_80_base64, "base64");
printerSocket.write(bytes);   // network printer on :9100
// or: fs.writeFileSync("\\\\.\\USB001", bytes) on Windows
```

The bytes already include the merchant header, buyer block, line items, tax
summary, ZIMRA verification code and QR code. You do not build any layout.

If you prefer your own layout, ignore the ESC/POS payload and print the
`qr_url` and `verification_code` on your existing template — both are mandatory
on a ZIMRA fiscal receipt.

---

## 6. Go-live checklist

**Merchant**
1. ZIMRA device registered and active in QRFRS.
2. Fiscal day open (or automatic day cycle enabled).
3. API token generated under Settings → External API.
4. Branch/till fiscalisation switches set correctly.

**Integrator**
1. Send HS codes and tax codes on every line.
2. Send `branch_id` (and `device_id` for multi-till shops).
3. Treat `receipt_number` as immutable and unique; retry on `ZIMRA_UNAVAILABLE`.
4. Handle `CREDIT_NOTE` with `original_receipt_number`.
5. Print the returned ESC/POS bytes or PDF.
6. Test against a sandbox device before switching the merchant to production.
