# QR Fiscal Receipt — Thermal Receipt Integration Guide

For developers integrating an external POS / back-office (e.g. Sales Intellect) that needs
to print fiscalised receipts on an 80mm or 58mm thermal printer.

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

---

## 1. What you get

Every time a receipt is fiscalised with ZIMRA, we can deliver it to you in two ways:

| Mode | Who initiates | Use for |
|------|---------------|---------|
| **Push (webhook)** | We POST to your URL immediately after fiscalisation | Normal printing — zero polling |
| **Pull (REST)** | You GET the receipt with an API token | Reprints, retries, backfills |

Both give you **ready-to-print ESC/POS bytes** — the QR code, bold header, dividers and
paper cut are already in the byte stream. You do not build a layout; you just write bytes
to the printer.

An A4 PDF of the same receipt is also included / available.

---

## 2. Merchant setup (done once, by the merchant, in the portal)

Portal → **Settings** → **External API**:

1. **API token** — generate/copy it. Give it to the developer. Treat it as a password.
2. **Webhook URL** — the merchant pastes your HTTPS endpoint here.
3. **Webhook secret** — used to sign the payload; share it with the developer.
4. **Thermal paper width** — choose **80mm** or **58mm** (default 80mm). Used by both push and pull.

---

## 3. Push: the `receipt.fiscalised` webhook

We send:

```
POST <your webhook URL>
Content-Type: application/json
X-QRFRS-Event: receipt.fiscalised
X-QRFRS-Signature: sha256=<hex hmac of the raw body>
X-QRFRS-Receipt-Number: <source receipt number>
```

Body:

```json
{
  "event": "receipt.fiscalised",
  "receipt_id": "0f0a1c2e-....",
  "receipt_number": "INV-001234",
  "shop_id": "QUV4bFFxc0dvcm5UZzRKakRobnpQZz09",
  "pos_device_id": "WnB3L2w0UHl4K2U2Rk1iZWFpSE55dz09",
  "device_id": 41239,
  "device_serial": "SN-00041239",
  "fiscal_day_no": 42,
  "receipt_global_no": 1187,
  "verification_code": "ABCD-1234-EFGH-5678",
  "qr_url": "https://receipt.zimra.org/...",
  "fiscalised_at": "2026-08-18T14:03:11.000Z",

  "pdf_base64": "JVBERi0xLjcK...",
  "pdf_mime": "application/pdf",
  "pdf_filename": "receipt-INV-001234.pdf",

  "escpos_base64": "G0AbdAAbUgA...",
  "escpos_width_mm": 80,
  "escpos_mime": "application/octet-stream",
  "thermal_url": "https://qrfiscalreceipt.com/api/public/receipts/0f0a1c2e-..../escpos"
}
```

### Steps on your side

1. Expose an HTTPS `POST` endpoint that accepts JSON (allow bodies up to ~5 MB).
2. **Verify the signature before trusting the payload** — HMAC-SHA256 of the *raw* request
   body using the webhook secret, compared to the hex value after `sha256=`.
3. Base64-decode `escpos_base64` → `Buffer` / `byte[]`.
4. Write those bytes **raw** to the thermal printer (Bluetooth / USB / TCP port 9100).
   Do not add your own formatting, encoding conversion, or line wrapping.
5. Reply `200` quickly. Any non-2xx is treated as a failure; queue the print job locally
   and print asynchronously rather than holding the HTTP response open.
6. De-duplicate on `receipt_id` — a retry may deliver the same receipt twice.

### Node.js example

```js
import express from "express";
import crypto from "crypto";
import net from "net";

const SECRET = process.env.QRFRS_WEBHOOK_SECRET;
const app = express();

app.post("/qrfrs/receipt",
  express.raw({ type: "application/json", limit: "10mb" }),
  (req, res) => {
    const sig = (req.get("X-QRFRS-Signature") || "").replace(/^sha256=/, "");
    const expected = crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
    if (sig.length !== expected.length ||
        !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
      return res.status(401).send("bad signature");
    }

    const payload = JSON.parse(req.body.toString("utf8"));
    res.sendStatus(200); // ack first, print after

    const bytes = Buffer.from(payload.escpos_base64, "base64");
    const sock = net.createConnection(9100, "192.168.1.50", () => sock.end(bytes));
  });

app.listen(3000);
```

### C# example (print step)

```csharp
var bytes = Convert.FromBase64String(payload.escpos_base64);
using var client = new TcpClient("192.168.1.50", 9100);
using var stream = client.GetStream();
stream.Write(bytes, 0, bytes.Length);   // or write to the RAW printer handle / COM port
```

---

## 4. Pull: fetch ESC/POS on demand (reprints)

```
GET https://qrfiscalreceipt.com/api/public/receipts/{id_or_receipt_number}/escpos
Authorization: Bearer <API token>
```

`{id_or_receipt_number}` accepts either the `receipt_id` UUID or the receipt number.

| Query param | Effect |
|-------------|--------|
| *(none)* | Raw bytes, `Content-Type: application/octet-stream`, header `X-QRFRS-Width-Mm` |
| `?format=json` | `{ receipt_id, receipt_number, width_mm, escpos_mime, escpos_base64 }` |
| `?width=58` / `?width=80` | Override the merchant's saved width for this request |

```bash
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://qrfiscalreceipt.com/api/public/receipts/INV-001234/escpos?width=58" \
  -o receipt.bin

# Windows raw print
copy /b receipt.bin \\localhost\ThermalPrinter
# Linux / network printer
cat receipt.bin | nc 192.168.1.50 9100
```

A4 PDF equivalent: `GET /api/public/receipts/{id_or_receipt_number}/pdf` (same auth).

### Response codes

| Code | Meaning |
|------|---------|
| 200 | OK |
| 401 | Missing / invalid `Authorization` token |
| 404 | Receipt not found for this merchant |
| 409 | Receipt exists but is not fiscalised yet — retry later |
| 500 | Server error — retry with backoff |

---

## 5. Printer notes

- Bytes target a standard ESC/POS printer (Epson-compatible: Xprinter, Rongta, Sunmi, Bixolon…).
- Send them **unmodified** — no text encoding, trimming or newline translation.
- Match the width to the paper actually loaded: 80mm = 48 chars, 58mm = 32 chars.
- The QR code is rendered with native ESC/POS QR commands. If your printer lacks QR support,
  fall back to printing `qr_url` as text or use the PDF.
- Cash drawer / extra feed: append your own ESC/POS commands after our bytes.

## 6. Go-live checklist

- [ ] Merchant generated the API token and set the webhook URL + secret
- [ ] Thermal width set to the merchant's paper size
- [ ] Your endpoint verifies the HMAC signature and returns 200 within a few seconds
- [ ] Prints de-duplicated by `receipt_id`
- [ ] Failed prints retried via the pull endpoint
- [ ] Test receipt fiscalised end-to-end and printed correctly
