← Back to home

Server API contract

How to build a validation endpoint the app can talk to.

The deal: for every scan, the app sends a POST with JSON to the API URL you configure. Your server replies with a small JSON object saying whether the ticket is valid, already used, or invalid — plus an optional message and ticket details. That's it.
On this page

The request

POST  <your configured API URL>

Headers

HeaderValue
Content-Typeapplication/json
Acceptapplication/json
X-App-VersionApp version, e.g. 1.0.0
AuthorizationBearer <api key> — only if a key is set
X-API-Key<api key> — same key, sent both ways for convenience

Body

FieldTypeDescription
codestringThe scanned barcode/QR value.
typestringBarcode format, e.g. qr, code128, ean13.
configIdstringIdentifier of the scanner configuration in use.
scannerNamestring?Optional lane/scanner label. Omitted if not set.
deviceIdstringStable, random per-install device identifier.
scannedAtstringISO 8601 timestamp of the scan.
POST /validate HTTP/1.1
Host: api.myevent.com
Content-Type: application/json
Accept: application/json
X-App-Version: 1.0.0
Authorization: Bearer sk_live_…
X-API-Key: sk_live_…

{
  "code": "TKT-4F9A21",
  "type": "qr",
  "configId": "cfg_8a1c",
  "scannerName": "Entrance #1",
  "deviceId": "a1b2c3d4-e5f6a7b8",
  "scannedAt": "2026-06-26T18:14:58.204Z"
}

The response

Reply with a JSON object. The app reads three things from it:

200 OK
{
  "status": "valid",
  "message": "Welcome — VIP entry",
  "ticket": { "name": "Ada Lovelace", "type": "VIP", "gate": "A", "seat": "12" }
}

Status mapping

The app maps your status string to a traffic-light result. Matching is case-insensitive.

ResultAccepted valuesBoolean
Green — validvalid, ok, successvalid: true
Yellow — already usedused, already_scanned, duplicate, warning
Red — rejectedinvalid, denied, not_found, expiredvalid: false
Erroranything unrecognized, a non-JSON body, or HTTP ≥ 500

Return HTTP 200 for the normal valid / used / invalid outcomes — these are business results, not transport errors. Any response below 500 is parsed for its JSON body; 500+ always shows the error popup regardless of body.

Examples

Valid → green

{ "status": "valid", "message": "Welcome — VIP entry",
  "ticket": { "name": "Ada Lovelace", "type": "VIP", "gate": "A" } }

Already used → yellow

{ "status": "used", "message": "Ticket already scanned",
  "ticket": { "name": "Alan Turing", "firstScan": "2026-06-26T19:42:10Z" } }

Invalid → red

{ "status": "invalid", "message": "Unknown ticket" }

Minimal boolean form

{ "valid": true, "message": "OK" }

Authentication

If the configuration includes an API key, it arrives on every request as both an Authorization: Bearer <key> header and an X-API-Key: <key> header — read whichever you prefer. Reject unauthenticated requests however you like; returning a red/error JSON body (or a 401) both surface clearly to the operator. Keys are provisioned per configuration, so you can issue a different key per event or per door.

Example server

A complete validation endpoint in Node.js / Express:

import express from 'express';
const app = express();
app.use(express.json());

app.post('/validate', (req, res) => {
  // --- auth ---
  const key = req.get('X-API-Key')
    || (req.get('Authorization') || '').replace(/^Bearer /, '');
  if (key !== process.env.SCANNER_KEY) {
    return res.status(401).json({ status: 'invalid', message: 'Bad API key' });
  }

  // --- look up the ticket ---
  const { code, scannedAt, scannerName } = req.body;
  const ticket = db.find(code);                 // your lookup
  if (!ticket) {
    return res.json({ status: 'invalid', message: 'Unknown ticket' });
  }
  if (ticket.usedAt) {
    return res.json({
      status: 'used',
      message: 'Already scanned',
      ticket: { name: ticket.name, type: ticket.type, firstScan: ticket.usedAt },
    });
  }

  // --- mark used + accept ---
  db.markUsed(code, { at: scannedAt, by: scannerName });
  res.json({
    status: 'valid',
    message: 'Welcome!',
    ticket: { name: ticket.name, type: ticket.type, gate: ticket.gate },
  });
});

app.listen(3000);

Whether a code counts as valid or used is entirely your server's call — typically by recording the first scan and treating repeats as duplicates.

Already have an API with a different shape?

You don't have to match this contract exactly. All response handling lives in a single adapter file — src/tickets/parseTicketResponse.ts — which is the only place coupled to your JSON. Edit STATUS_MAP, pickStatus, pickMessage, and pickTicketFields to read your fields, and the rest of the app is unaffected. See it on GitHub.

Notes & limits

→ Generate a setup code once your endpoint is live.