Skip to main content

The problem

An invoice number sequence must be gapless and non-duplicated to pass a fiscal audit. That is hard when:
  • The mobile app is in airplane mode or has a weak signal and still needs to issue a receipt.
  • The same firm has a web user who could emit an invoice on the same series between the time the phone reserves a number and the time it actually syncs.
Contazen solves both with a device-bound lock on the invoice series plus pre-allocated reservations carried on the device.

How it works

Lock a series

POST /invoice-series/{id}/lock binds the series to the calling mobile device. While locked:
  • Only that device can call POST /invoices with series_id equal to this series.
  • Only that device can request reservations.
  • Web / API-key callers that try to emit get series_locked_to_device (403).
Unlocking requires either the same device or force: true from an admin.

Pre-allocate numbers

The mobile app keeps a small pool (10 by default) of reservations in local storage. It tops the pool up whenever online and usage drops below 3:
POST /invoice-series/inv_abc/reserve-numbers
Authorization: Bearer {jwt}
Content-Type: application/json

{ "count": 10 }
The response carries {token, number} pairs. The app stores them in SQLite alongside each token’s expiry. When the user creates an invoice while offline, the app:
  1. Pops the next usable reservation and saves a draft with that number visible (e.g. “Factură #101”).
  2. Enqueues a POST /invoices mutation in its outbox with reservation_token and force_chronology: false.
  3. The outbox drains on the next network/foreground transition.

Emit an invoice from a draft

POST /invoices
Authorization: Bearer {jwt}
Idempotency-Key: {localUuid}
Content-Type: application/json

{
  "series_id": "inv_abc",
  "reservation_token": "rsv_0f3a9b...",
  "client_id": "cli_def",
  "date": "2026-04-20",
  "items": [ { "description": "...", "quantity": 1, "price": 100, "vat_rate": 19 } ]
}
The server verifies the reservation:
CheckFailure code
Token exists under the caller’s firmreservation_missing
Token belongs to the same series as series_idreservation_series_mismatch
Token status is pendingreservation_already_consumed / reservation_not_pending
Token has not expiredreservation_expired
Token belongs to the caller’s devicereservation_device_mismatch
On success, the invoice is created with number_idx equal to the reservation’s pre-allocated slot. The reservation flips to consumed and records the bill_id.

Discarded drafts

If the user deletes an offline draft before it syncs, the app calls POST /invoice-series/reservations/{token}/release. The reservation flips to released — the number itself is still gone (documented gap), but the audit trail captures intent.

Gap handling

The series counter advances the moment a reservation is created, not when the invoice is actually emitted. That means expired / released reservations can become gaps in the sequence. Romanian fiscal regulations accept gaps when a firm can document why they occurred — keep the reservation TTL generous (default 30 days) and have the device refresh the pool often enough that expiry is rare in practice. If your jurisdiction does not accept gaps, disable the reservation feature entirely: leave series unlocked and require the mobile device to be online when emitting. The existing online-only path in POST /invoices still works without any changes.

When to use the lock

SituationLock?
Field sales: phone issues invoices at the client’s premises, often without signal.Yes — lock the series the sales rep uses.
Home office: emits from laptop only.No.
Shop with both a POS (mobile) and back-office (web) on the same series.Yes — lock to the POS device. Emit back-office invoices on a different series.
Multi-device field team on the same series.Not recommended — only one device at a time can hold the lock. Give each sales rep their own series.

Error reference (invoice creation)

HTTPCodeMeaning
403series_locked_to_deviceSeries is locked; caller is not a mobile device.
403series_locked_other_deviceSeries is locked to a different device than the caller.
409reservation_already_consumedThe token was already used — the outbox must have double-fired. Idempotency-Key should prevent this; check your client.
409reservation_expiredTTL has passed; allocate a fresh batch.
409reservation_series_mismatchWrong series_id for the token.
403reservation_device_mismatchToken belongs to another device (same firm).