# POS Integration Guide

Developer reference for integrating a POS with the XPOS ESDC local service (port 8888): endpoints, request/response schemas, errors and the legacy V2 API.

# Overview & Integration Flow

XPOS ESDC exposes a local HTTP service that a Point of Sale (the built-in POS, or your own POS/ERP) uses to fiscalise sales. This guide is the integration reference for developers building against that service.

## Base URL

The service listens on port **8888** on all interfaces of the machine running XPOS ESDC:

```
http://localhost:8888        (POS on the same PC)
http://<esdc-host>:8888     (POS elsewhere on the LAN)
```

Two API surfaces are available:

- **v3** — the current API, under `/api/v3`. Use this for new integrations.
- **Legacy V2** — `/api/attention`, `/api/status`, `/api/sign`, for compatibility with older POS integrations. See the "Legacy V2 API" page.

## Interactive reference (Swagger)

The running service publishes a live, always-current OpenAPI document and an interactive UI:

```
http://localhost:8888/swagger/index.html
```

The Swagger schema is authoritative for exact field names and shapes — this guide explains the flow and the important fields; Swagger is the definitive contract for the build you are integrating with.

## The integration flow

A POS follows this sequence:

1. **Check availability** — `GET /api/v3/attention`. Confirms the SDC service is up and a reader is present.
2. **Read status** — `GET /api/v3/status`. Tells you whether a PIN is required (`isPinRequired`), whether an audit is required, the SDC date/time, the device identity, and the active tax rates.
3. **Verify PIN (once per session)** — if `isPinRequired` is true, `POST /api/v3/pin` with the operator's PIN. The SDC caches it in memory and signs subsequent invoices without it. The PIN is cleared on restart, so this step repeats after the app restarts.
4. **Sign invoices** — `POST /api/v3/invoices` for each sale. The response contains the verification URL, QR code, journal (receipt text) and signature.
5. **Print / store** — render the receipt from the response, or fetch the rendered PDF later from history.

## Idempotency (RequestId)

On `POST /api/v3/invoices`, send a unique `RequestId` request header. If the same `RequestId` is retried (after a timeout or dropped connection), the SDC returns the **same** already-signed invoice instead of signing again — the header is echoed back on the response. Always send a fresh `RequestId` per sale and reuse it only when retrying that exact sale.

## Content type &amp; CORS

- Requests and responses are JSON (`Content-Type: application/json`), except `POST /api/v3/pin`, whose body is the PIN as plain text.
- CORS is open for `GET`, `POST` and `OPTIONS` from any origin, so a browser-based POS can call the service directly.
- If the POS runs on another machine, allow inbound TCP **8888** through the Windows firewall on the SDC host.

# Status & Session

These endpoints establish that the SDC is ready and unlock signing for the session.

## GET /api/v3/attention

Lightweight health check. Returns whether the SDC is available and which card readers are connected.

```
GET /api/v3/attention

200 OK
{
  "available": true,
  "readers": ["ACS ACR39U ICC Reader 0"]
}
```

## GET /api/v3/status

The E-SDC status document. Read this before signing.

<table id="bkmrk-fieldmeaning-ispinre"><thead><tr><th>Field</th><th>Meaning</th></tr></thead><tbody><tr><td>`isPinRequired`</td><td>`true` if a PIN must be verified before signing.</td></tr><tr><td>`auditRequired`</td><td>`true` when the Secure Element has reached its limit and an audit is due.</td></tr><tr><td>`sdcDateTime`</td><td>The SDC's current date/time (use this as the fiscal clock).</td></tr><tr><td>`protocolVersion`</td><td>POS-to-SDC protocol version.</td></tr><tr><td>`secureElementVersion`</td><td>SE applet version.</td></tr><tr><td>`make`, `model`, `softwareVersion`, `hardwareVersion`, `deviceSerialNumber`</td><td>Device identity.</td></tr><tr><td>`uid`</td><td>Secure Element UID.</td></tr><tr><td>`currentTaxRates`</td><td>The tax-rate group currently in force (the labels/categories you may use on items).</td></tr><tr><td>`allTaxRates`</td><td>All known tax-rate groups (including future-dated).</td></tr><tr><td>`gsc`</td><td>Status codes, e.g. `1300` (SE not present), `2400` (SE limit reached).</td></tr></tbody></table>

## POST /api/v3/pin

Verifies the operator PIN on the Secure Element and caches it in memory for the session. **The request body is the PIN as plain text** (digits) — not JSON. The response is a 4-digit text response code.

```
POST /api/v3/pin
Content-Type: text/plain

3456

200 OK
0100
```

<table id="bkmrk-responsemeaning-0100"><thead><tr><th>Response</th><th>Meaning</th></tr></thead><tbody><tr><td>`0100`</td><td>PIN verified — signing is unlocked.</td></tr><tr><td>`2100`</td><td>PIN incorrect / no usable PIN supplied.</td></tr><tr><td>`2110`</td><td>Blocked, or only one attempt remains (the SDC refuses the last attempt to avoid locking the card).</td></tr><tr><td>`1300`</td><td>Secure Element not present / unreadable.</td></tr></tbody></table>

**Card safety:** the SDC will not spend the final PIN attempt. If only one try remains it returns `2110` rather than risk permanently locking the card — resolve the correct PIN before retrying.

## Secure Element helpers (read-only)

Useful for status displays and pre-flight checks; none of these sign or consume a PIN attempt.

<table id="bkmrk-endpointreturns-get-"><thead><tr><th>Endpoint</th><th>Returns</th></tr></thead><tbody><tr><td>`GET /api/v3/se/identity`</td><td>Seller identity from the SE certificate: `uid`, `tin`, `businessName`, `locationName`, `address`, `state`, `validFrom`, `validTo`, TaxCore URL.</td></tr><tr><td>`GET /api/v3/se/amount-status`</td><td>`saleRefund` (amount accumulated) and `limit`.</td></tr><tr><td>`GET /api/v3/se/pin-tries`</td><td>`triesLeft`.</td></tr><tr><td>`GET /api/v3/se/safety`</td><td>A safety snapshot: PIN tries left, whether blocked, amount used/limit/remaining, used fraction.</td></tr><tr><td>`GET /api/v3/se/version`</td><td>SE applet `version`.</td></tr></tbody></table>

# Signing an Invoice

The core endpoint. Signs a sale on the Secure Element and returns the fiscal result (verification URL, QR, journal, signature). Requires a PIN previously verified via `POST /api/v3/pin`.

```
POST /api/v3/invoices
Content-Type: application/json
RequestId: 4b1e...unique-per-sale
```

## Request fields

<table id="bkmrk-fieldtyperequirednot"><thead><tr><th>Field</th><th>Type</th><th>Required</th><th>Notes</th></tr></thead><tbody><tr><td>`invoiceType`</td><td>enum</td><td>Yes</td><td>`Normal`, `ProForma`, `Copy`, `Training`, `Advance`.</td></tr><tr><td>`transactionType`</td><td>enum</td><td>Yes</td><td>`Sale` or `Refund`.</td></tr><tr><td>`payment`</td><td>array</td><td>Yes</td><td>One or more `{ amount, paymentType }`. See payment types below.</td></tr><tr><td>`items`</td><td>array</td><td>Yes</td><td>One or more line items. See item fields below.</td></tr><tr><td>`dateAndTimeOfIssue`</td><td>string</td><td>No</td><td>ISO date/time; defaults to the SDC clock if omitted.</td></tr><tr><td>`cashier`</td><td>string</td><td>No</td><td>≤ 50 chars.</td></tr><tr><td>`buyerId`</td><td>string</td><td>No</td><td>≤ 20 chars (buyer TIN / ID).</td></tr><tr><td>`buyerCostCenterId`</td><td>string</td><td>No</td><td>≤ 50 chars.</td></tr><tr><td>`invoiceNumber`</td><td>string</td><td>No</td><td>≤ 60 chars (your POS's own number/ordinal).</td></tr><tr><td>`referentDocumentNumber`</td><td>string</td><td>Conditional</td><td>≤ 50 chars. The signed number of the original invoice — required for `Copy` and `Refund`, and whenever `referentDocumentDT` is present.</td></tr><tr><td>`referentDocumentDT`</td><td>string</td><td>No</td><td>Date/time of the referent document; if present, `referentDocumentNumber` becomes required.</td></tr><tr><td>`options`</td><td>object</td><td>No</td><td>Key/value flags, e.g. `{ "omitTextualRepresentation": 1 }` to skip building the journal text.</td></tr></tbody></table>

### Item fields

<table id="bkmrk-fieldtyperequirednot-1"><thead><tr><th>Field</th><th>Type</th><th>Required</th><th>Notes</th></tr></thead><tbody><tr><td>`name`</td><td>string</td><td>Yes</td><td>1–2048 chars.</td></tr><tr><td>`quantity`</td><td>number</td><td>Yes</td><td>≥ 0.001.</td></tr><tr><td>`unitPrice`</td><td>number</td><td>Yes</td><td>Price per unit.</td></tr><tr><td>`labels`</td><td>array</td><td>Yes</td><td>Tax label(s) for the item, e.g. `["A"]`. Must be labels present in the active tax group (unknown labels are rejected).</td></tr><tr><td>`totalAmount`</td><td>number</td><td>Yes</td><td>Line total (quantity × unitPrice).</td></tr><tr><td>`gtin`</td><td>string</td><td>No</td><td>8–14 chars if supplied.</td></tr></tbody></table>

### Payment types

`Other`, `Cash`, `Card`, `Check`, `WireTransfer`, `Voucher`, `MobileMoney`.

## Example request

```
{
  "invoiceType": "Normal",
  "transactionType": "Sale",
  "cashier": "Jack",
  "payment": [
    { "amount": 23.00, "paymentType": "Cash" }
  ],
  "items": [
    {
      "name": "Coffee",
      "quantity": 2,
      "unitPrice": 5.00,
      "labels": ["A"],
      "totalAmount": 10.00
    },
    {
      "name": "Sandwich",
      "quantity": 1,
      "unitPrice": 13.00,
      "labels": ["A"],
      "totalAmount": 13.00
    }
  ]
}
```

## Response

On success the SDC returns the fiscal result. Field names are camelCase (see Swagger for the exact schema of your build):

<table id="bkmrk-fieldmeaning-invoice"><thead><tr><th>Field</th><th>Meaning</th></tr></thead><tbody><tr><td>`invoiceNumber`</td><td>The signed fiscal invoice number.</td></tr><tr><td>`invoiceCounter` / `invoiceCounterExtension`</td><td>Per-type and combined counters.</td></tr><tr><td>`totalCounter` / `transactionTypeCounter`</td><td>Running counters from the SE.</td></tr><tr><td>`verificationUrl`</td><td>The URL a customer uses to verify the invoice with FRCS.</td></tr><tr><td>`verificationQRCode`</td><td>The QR code (base64) encoding the verification URL — print this on the receipt.</td></tr><tr><td>`journal`</td><td>The formatted textual receipt (40-column). Omitted if `options.omitTextualRepresentation` is set.</td></tr><tr><td>`signature` / `encryptedInternalData`</td><td>The SE signature and encrypted internal data.</td></tr><tr><td>`signedBy` / `requestedBy`</td><td>SE / requesting-signer identifiers.</td></tr><tr><td>`taxItems`</td><td>Per-category tax lines: `label`, `categoryName`, `rate`, `amount`.</td></tr><tr><td>`totalAmount`</td><td>Invoice total.</td></tr><tr><td>`taxGroupRevision`</td><td>Revision of the tax group applied.</td></tr><tr><td>`businessName`, `tin`, `locationName`, `address`, `district`, `mrc`</td><td>Seller details for the receipt header.</td></tr><tr><td>`sdcDateTime`</td><td>The signing date/time.</td></tr></tbody></table>

```
200 OK
{
  "invoiceNumber": "EKLBHT3Y-EKLBHT3Y-31",
  "verificationUrl": "https://sandbox.vms.frcs.org.fj/v/?vl=...",
  "verificationQRCode": "iVBORw0KGgoAAAANSUhEUgAA...",
  "journal": "============ FISCAL INVOICE ============ ...",
  "totalAmount": 23.00,
  "taxItems": [
    { "label": "A", "categoryName": "VAT", "rate": 15.0, "amount": 3.00 }
  ],
  "sdcDateTime": "2026-08-13T09:42:00"
}
```

## Refunds &amp; copies

For a `Refund` (or a `Copy`), set `transactionType`/`invoiceType` accordingly and reference the original signed invoice with `referentDocumentNumber` (and optionally `referentDocumentDT`). Taxes for a copy/refund are calculated using the tax group in force at the referent document's date.

## Validate without signing

`POST /api/v3/invoices/validate` runs the same request validation and tax rules **without** touching the card — useful to check a basket before committing. It returns `{ "valid": true }` or a 400 with field errors (see "Errors &amp; Validation").

# Errors, Response Codes & Validation

The SDC uses two error shapes: **field validation errors** (HTTP 400) and **operational response codes** (in the body/status).

## Validation errors (HTTP 400)

When a request fails structural or business validation, the SDC returns HTTP 400 with a `modelState` array — one entry per invalid field, each carrying one or more 4-digit codes:

```
400 Bad Request
{
  "message": "Bad Request",
  "modelState": [
    { "property": "items[0].name", "errors": ["2803"] },
    { "property": "payment",       "errors": ["2801"] }
  ]
}
```

<table id="bkmrk-codemeaning-2800fiel"><thead><tr><th>Code</th><th>Meaning</th></tr></thead><tbody><tr><td>`2800`</td><td>Field required.</td></tr><tr><td>`2801`</td><td>List length less than expected (e.g. no items / no payment).</td></tr><tr><td>`2803`</td><td>Field required (item field).</td></tr><tr><td>`2804`</td><td>Field length invalid.</td></tr><tr><td>`2805`</td><td>Field value invalid (bad type, unparseable date/number, unknown enum).</td></tr><tr><td>`2807`</td><td>Field out of range.</td></tr></tbody></table>

The `property` path mirrors the request, including array indices — e.g. `items[2].quantity`, `payment[0].paymentType`.

## Operational response codes

These signal SDC/Secure-Element state rather than a malformed request.

<table id="bkmrk-codewheremeaning-010"><thead><tr><th>Code</th><th>Where</th><th>Meaning</th></tr></thead><tbody><tr><td>`0100`</td><td>`POST /pin` body</td><td>PIN verified.</td></tr><tr><td>`1300`</td><td>`/pin` body, `/status.gsc`</td><td>Secure Element not present / unreadable.</td></tr><tr><td>`1500`</td><td>`/invoices` 400 body</td><td>PIN verification required — call `POST /api/v3/pin` first.</td></tr><tr><td>`2100`</td><td>`/pin` body</td><td>PIN incorrect.</td></tr><tr><td>`2110`</td><td>`/pin` body</td><td>Blocked, or last attempt withheld to avoid locking the card.</td></tr><tr><td>`2400`</td><td>`/status.gsc`</td><td>Secure Element limit reached — audit required.</td></tr></tbody></table>

### Missing PIN on sign

```
400 Bad Request
{ "message": "PIN verification required.", "code": "1500" }
```

### Secure Element / card faults

A card fault during signing (removed card, PIN lock, SE limit) returns HTTP 503 with the message and the 4-digit code, for example:

```
503 Service Unavailable
"Secure Element limit reached (code 2210)"
```

## Recommended handling

- **400 with `modelState`** — fix the offending field(s) and resubmit; do not retry unchanged.
- **1500** — prompt for the PIN, call `/pin`, then resubmit the sale (reuse the same `RequestId`).
- **503 (SE fault)** — surface the message to the operator; retry the same `RequestId` once the fault clears (e.g. card reinserted). Idempotency ensures no double-signing.

# History & Reports

Read-back endpoints for reprinting, reconciliation and reporting. These query stored records and do not touch the card.

## Invoice history

```
GET /api/v3/invoices?from=2026-08-01&to=2026-08-13&uid=EKLBHT3Y
```

Returns fiscalised invoice records for the period (defaults to the last 30 days). Each row includes `dateAndTime`, `invoiceNumber`, `uid`, `type`, `transaction`, `amount`, `referenceDocumentNumber`, `source` and `itemCount`. `uid` is optional.

## Single invoice (drill-down)

```
GET /api/v3/invoices/detail?number=EKLBHT3Y-EKLBHT3Y-31
```

Returns the full stored record for one invoice: items, per-invoice tax breakdown and payments. `404` if the number is unknown.

## Receipt PDF (reprint)

```
GET /api/v3/invoices/journal-pdf?number=EKLBHT3Y-EKLBHT3Y-31
```

Returns the rendered receipt as `application/pdf` — use it to reprint from history. `404` if the invoice has no stored textual representation (e.g. it was signed with `omitTextualRepresentation`).

## Recovery by RequestId

```
GET /api/v3/invoices/{requestId}
```

Returns the invoice previously signed under that `RequestId` (or null). Useful after a lost response — no re-signing occurs.

## Reports

All report endpoints take `from`, `to` and optional `uid`.

<table id="bkmrk-endpointreturns-get-"><thead><tr><th>Endpoint</th><th>Returns</th></tr></thead><tbody><tr><td>`GET /api/v3/reports/daily`</td><td>Daily totals.</td></tr><tr><td>`GET /api/v3/reports/payment-types`</td><td>Totals by payment type.</td></tr><tr><td>`GET /api/v3/reports/tax-amounts`</td><td>Tax totals by label/category.</td></tr><tr><td>`GET /api/v3/reports/list-of-items`</td><td>Item sales.</td></tr><tr><td>`GET /api/v3/reports/detailed`</td><td>A combined detailed report (summary + all breakdowns).</td></tr><tr><td>`GET /api/v3/reports/detailed/excel`</td><td>The detailed report as a multi-sheet `.xlsx` workbook.</td></tr></tbody></table>

## Audit status

```
GET /api/v3/audit/status?uid=EKLBHT3Y
```

Returns `queuedPackages` (unsent audit packages), `awaitingProof` (a proof-of-audit is pending) and `lastAuditStartUtc`. Auditing is automatic in the background; this is for visibility, not something the POS drives.

# Legacy V2 API

The Legacy V2 API exists for compatibility with older POS integrations (the FiscoBridge-era contract). New integrations should use v3. V2 signing reuses the exact same fiscalisation pipeline as v3 — only the request parser and the returned result shape differ.

## Endpoints

<table id="bkmrk-endpointpurpose-get-"><thead><tr><th>Endpoint</th><th>Purpose</th></tr></thead><tbody><tr><td>`GET /api/attention`</td><td>Availability + readers.</td></tr><tr><td>`GET /api/status`</td><td>Availability + readers.</td></tr><tr><td>`POST /api/sign`</td><td>Sign an invoice (V2 request → V2 result).</td></tr><tr><td>`POST /api/sign/SignInvoice`</td><td>Alias of `/api/sign`.</td></tr></tbody></table>

PIN verification uses the same `POST /api/v3/pin` as v3 — the cached PIN is shared across both APIs.

## Key differences from v3

- **Single payment type.** The V2 request carries one request-level `paymentType` (not a `payment` array). A single payment is synthesised with an amount equal to the sum of item totals.
- **Stringly-typed nested fields.** `items` and `options` are provided as JSON *strings*, and item `labels` is a JSON string such as `"[\"A\"]"`.
- **`buyerCostCenterId`** limit is 15 chars in V2 (50 in v3).
- **`unitPrice`** defaults to 1 if omitted (v3 requires it).
- **Result shape** is the abbreviated legacy `FiscalizationResultV2` (short field names) rather than the full-named v3 `InvoiceResult`. The underlying values (verification URL, QR, journal, signature, counters) are the same.

## Request fields (V2)

<table id="bkmrk-fieldnotes-invoicety"><thead><tr><th>Field</th><th>Notes</th></tr></thead><tbody><tr><td>`invoiceType`</td><td>`Normal` / `ProForma` / `Copy` / `Training` / `Advance`.</td></tr><tr><td>`transactionType`</td><td>`Sale` / `Refund`.</td></tr><tr><td>`paymentType`</td><td>Single value: `Cash`, `Card`, etc.</td></tr><tr><td>`items`</td><td>JSON string of an array of `{ gtin, name, quantity, unitPrice, labels, totalAmount }`; `labels` itself is a JSON string like `"[\"A\"]"`.</td></tr><tr><td>`dateAndTimeOfIssue`, `cashier`, `buyerId`, `buyerCostCenterId`, `invoiceNumber`, `referentDocumentNumber`, `options`</td><td>As v3, with the V2 limits noted above.</td></tr></tbody></table>

## Validation

V2 validation returns the same 400 `modelState` shape as v3; a parse failure (unparseable date, wrong JSON type) is reported as `2805` (field value invalid). You can dry-run a V2 request with `POST /api/v3/invoices/v2/validate`.

## Recommendation

Use V2 only to keep an existing POS working. For new development, target v3 for the typed `payment` array, the richer result fields, and `RequestId` idempotency.