CoinTrak
Integration Contract · Draft v0.1

ATM Money-Kiosk ↔ CoinTrak

How your ATM redeems a CoinTrak pulse machine's cash-out for a player — scan the slip, confirm the amount with us, get authorization, dispense, and report it paid.

DRAFT — expect changes after your feedback mTLS Local LAN · no internet at pay time JSON · integer cents

1 What this does

A CoinTrak pulse gaming machine prints a redemption slip when a player cashes out. The player brings it to your ATM Money-Kiosk, which scans the slip's QR, asks CoinTrak what it's worth, gets authorization, dispenses the cash, and tells CoinTrak it paid.

Your ATM talks to a small local HTTPS service on the CoinTrak device on the same venue LAN — there is no internet round-trip at pay time.

2 Transport & connectivity

3 The redemption flow

In this exact order. The authorize ACK is your go-signal and the paying step — the slip is marked paid when we ACK, so a jam, a crash, or a lost confirm all leave the slip paid and send the player to a clerk. This deliberately favors "paid" so the ATM can never pay the same slip twice.

1
Scan the slip QR
You read a URL https://<host>/receipt/hand/<transaction_id> — take the last path segment as transaction_id.
2
Look up the handpay
GET /atm/handpay/lookup → amount + status.
3
Authorize & wait for the ACK
POST /atm/handpay/authorize → we validate and mark the slip paid, then reply authorized:true.
Dispense the cash — only after authorized:true
Never dispense without the ACK.
4
Confirm the outcome
POST /atm/handpay/confirmreport the outcome (the slip is already paid).
success

We log the dispense for reconciliation. Send dispensed_cents + dispense_ref.

fail

The slip stays paid — send the player to a clerk, who verifies and pays by hand.

4 The QR / correlation token

The slip QR encodes a full URL: https://<host>/receipt/hand/<transaction_id>. Read the complete URL and take the last path segment as the transaction_id — that's the key for all three calls. The QR content may change in a later revision; we'll tell you if so.

5 Endpoints

All requests/responses are JSON over mTLS. All money is integer cents.

GET/atm/handpay/lookup?tx={transaction_id}read-only

Returns the single handpay for that tx (never lists others).

200 { "transaction_id":"…", "amount_cents":2500, "status":"pending",
      "machine_name":"…", "claimable":true }
404 { "error":"not found" }
409 { "error":"not-claimable", "status":"paid|voided|held" }
POST/atm/handpay/authorizewait for the ACK before dispensing

Validates the slip is pending and the amount matches, then marks it paid and ACKs. Include a stable atm_id. Dispense only on authorized:true.

// request
{ "transaction_id":"…", "amount_cents":2500, "atm_id":"ATM123" }

200 (ACK — slip is now PAID) { "authorized":true, "transaction_id":"…", "amount_cents":2500 }
409 { "authorized":false, "error":"already-paid|voided|not-found|amount-mismatch" }

Idempotent: re-sending authorize for a slip your ATM just paid re-returns the ACK (a lost ACK still lets you dispense). A slip already paid by someone else, or voided/unknown → 409, do not dispense.

POST/atm/handpay/confirmreport — slip already paid

A report only — it records the dispense for reconciliation; it does not change the paid status. Idempotent — if you don't get a 200, retry the same request (the slip is already paid, so repeats are safe).

// success → record the actual cash out
{ "transaction_id":"…", "result":"success", "dispensed_cents":2500, "dispense_ref":"ATM123-000457" }
200 { "ok":true, "status":"paid", "logged":true }

// fail / short dispense → slip STAYS PAID; player goes to a clerk
{ "transaction_id":"…", "result":"fail", "reason":"jam" }
200 { "ok":true, "status":"paid", "clerk_review":true }

dispense_ref is your ATM's own unique reference for the dispense (required on success — we reconcile against your records). On fail the player is directed to an attendant; the slip is not reopened for another automated payout.

POST/atm/metersproposed — you define the payload

A periodic meter dump from the ATM — this is reconciliation leg 3 (your dispense records, tied out against the machine meters and our ledger). We're proposing the endpoint; you tell us the payload. A sketch of what's useful:

{ "atm_id":"ATM123", "ts":"2026-09-10T18:00:00Z",
  "cash_dispensed_cents_total":1234500,          // lifetime cumulative
  "dispense_count_total":418,
  "cassettes":[ { "denom_cents":2000, "count":300 } ],   // optional levels
  "last_dispense_ref":"ATM123-000457", "firmware":"…" }
200 { "ok":true }

Send us what your ATM actually exposes and your preferred cadence (e.g. every few minutes and/or after each dispense) — we'll finalize the schema around your fields.

6 Your client certificate

CoinTrak issues and provides your certificate per install — you don't generate anything. We send you a bundle containing:

  • your client certificate + private key (your ATM presents this on every request), and
  • the site CA certificate to trust the CoinTrak server.

We deliver it over a secure channel (e.g. a password-protected PKCS#12 / .p12). Keep the private key protected on the ATM and validate the CoinTrak server cert against the provided CA. Certs are per-site with a fixed validity; we coordinate rotation on reinstall.

7 Error handling & idempotency

Ambiguous window

If your ATM dispensed but the confirm never reached us (crash/network), the slip is already paid, so nothing double-pays — just retry confirm to close the dispense log. A paid slip with no dispense record is what routes a genuine non-dispense to a clerk, reconciled against your log via dispense_ref.

8 What we need from you

To finalize this contract:

Cert format & delivery
We issue and send your client cert + key + the site CA. What format do you need — PEM files or a PKCS#12 (.p12) bundle — and your secure channel to receive it?
Confirm payload
On confirm(success), can you return dispensed_cents + dispense_ref? (Required for reconciliation.)
Amount policy
Must a dispense always equal amount_cents (all-or-nothing), or can you do a partial dispense? Either way the slip is paid in full at authorize; a short/failed dispense sends the player to a clerk for the shortfall.
Stable atm_id
A stable id (and/or per-request id) on authorize, so a retried authorize after a lost ACK re-ACKs instead of failing.
Meter-dump payload (POST /atm/meters)
What meters/levels does your ATM expose (cumulative cash dispensed, counts, cassette levels…), in what JSON shape, and at what cadence? This is reconciliation leg 3 — tied out against the machine meters and our ledger.

9 Notes