Skip to content

Checking in

The gate’s side of a check-in is small: read the member’s QR code, send it to core, act on the answer. Everything else (who the member is, whether the code is valid, what a check-in earns) is core’s business.

sequenceDiagram
  autonumber
  actor M as Member
  box rgb(227,244,243) FINNS platform
    participant APP as FINNS app
    participant CORE as Core
  end
  box rgb(246,241,233) Gate
    participant GATE as Gate
  end

  M->>APP: Open "Check in"
  APP->>CORE: Request a one-time QR
  CORE-->>APP: QR code (short-lived, single use)
  APP->>M: Show QR
  M->>GATE: Present QR to the scanner
  GATE->>CORE: POST /api/v1/gate/check-ins {gateId, qr}<br/>Bearer gate token
  CORE-->>GATE: 200 {result: "granted"} or {result: "denied", reason}
  Note over GATE: nowrap:Open only on "granted"
Step Contract
Read Scan the QR code. Its content is opaque text starting with FINNS1:. Don’t decode or modify it.
Send One POST /api/v1/gate/check-ins per scan, with {gateId, qr} and the gate token. See the API reference.
Act Open only when core answers {"result": "granted"}. A denied result, an error, a timeout or an unreadable answer all mean “do not open”.

Core decides everything else. A scan is recorded whether it is granted or denied.

flowchart TB
  classDef ok fill:#E3F4F3,stroke:#13686C,color:#12191B
  classDef bad fill:#FFF4E8,stroke:#E9724C,color:#12191B
  classDef step fill:#ffffff,stroke:#D9CFBE,color:#12191B

  s["Scan: {gateId, qr}"]:::step --> f{"Well-formed<br/>FINNS1 token?"}
  f -- "no" --> d1["denied · malformed"]:::bad
  f -- "yes" --> k{"Known QR?"}
  k -- "no" --> d2["denied · unknown"]:::bad
  k -- "yes" --> u{"Already used?"}
  u -- "yes" --> d3["denied · used"]:::bad
  u -- "no" --> e{"Expired?"}
  e -- "yes" --> d4["denied · expired"]:::bad
  e -- "no" --> g["granted<br/>QR consumed · check-in recorded<br/>points credited"]:::ok

A QR works once, even if two gates scan it at the same moment. The points credit is tied to the QR, so a check-in can never be credited twice.

Part Contract
Format FINNS1: followed by a random URL-safe string
Meaning None outside core. It is not signed and not decodable by the gate.
Lifetime Short; the app refreshes it while it is on screen. An expired code is denied · expired.
Versioning The prefix. A gate that sees a newer FINNSn: prefix it doesn’t know must not guess: it reports that it can’t read the code.

Internal and confidential. Share partner sections only as partner builds (pnpm build:partner).