by Intelliverse X

Age & identity verification (Didit QR gate)

An optional, per-machine gate: when enabled, a buyer must complete an identity/age verification on their own phone — QR scan → Didit's hosted document + liveness flow — before a purchase can complete. Default is OFF: machines without the flag behave exactly as before.

The verification provider is the org's existing Didit stack (https://didit.me, v3 sessions API) — the same application the quests-economy KYC provider and the admin portal's didit-mcp tooling use. Kiosk-X talks to it through app/didit_client.py.

sequenceDiagram participant K as Kiosk screen participant P as Buyer's phone participant X as Kiosk-X cloud participant D as Didit (hosted) participant S as Stripe K->>X: createOrder payType=4 (Scan & Pay) X-->>K: QR → /verify/{orderNo} (no Stripe session yet) P->>X: scan QR, open /verify/{orderNo} X->>D: create session (workflow, vendor_data=verifyId) X-->>P: 302 → Didit hosted document + liveness D->>X: webhook (signature-verified, idempotent) P->>X: /verify/{orderNo}/return (polls decision as fallback) X->>S: create Checkout Session (only after the pass) X-->>P: 302 → Stripe payment page S->>X: payment webhook → settle → MQTT dispense

Enabling it (Operator X)

Machine detail → Require age/ID verification (web console and the Operator X app), or directly:

curl -s -X PATCH https://api.kiosk-x.ai/api/v1/machines/{machineNo} \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"ageVerification": {"enabled": true, "minAge": 21, "appliesTo": ["all"]}}'
Field Meaning
enabled The gate. Default false — nothing changes until you flip it.
minAge Age threshold buyers must prove (default 21).
appliesTo ["all"] (default) or product verticals, e.g. ["alcohol","vape"] — only orders containing those categories are gated.
failOpen Default false = fail closed: if the verification service is unreachable, restricted sales are blocked (and the failure is ledgered + the operator alerted). Opt into true to let sales proceed during an outage — every bypass is still ledgered as an error event.

Machines stocking restricted verticals (vape / alcohol / tobacco / energy) get an auto-suggestion to enable, surfaced on the machine detail (GET /api/v1/machines/{machineNo}/age-verificationsuggestion).

The buyer experience

Scan & Pay (one QR, verify → pay): the kiosk's payment QR routes the buyer to the verification step first; after the pass the same phone flow continues straight into Stripe Checkout. No second QR. A passed verification also sets a short-lived signed cookie (machine + threshold + expiry, KIOSKX_VERIFY_TOKEN_TTL, default 15 min) so a multi-item run of purchases verifies once.

Card reader: the buyer scans the machine's verification QR (GET /device/qr/verify/{machineNo}.png, served only while the gate is ON; Operator X shows/prints it), verifies on their phone, then pays at the reader. The pass arms the machine's single-transaction verification — the same store the on-machine ID scanner feeds.

Failure paths: underage or declined → a clear decline page and no payment is ever taken (the Stripe session is only minted after a pass). Didit unreachable → fail-closed page by default (see failOpen). Abandoned mid-verify → the session expires (KIOSKX_VERIFY_SESSION_TTL, default 30 min).

What the card-reader path can and cannot enforce

The Nayax VPOS reader authorizes independently of the vending cloud (MDB to the VMC, cellular to Nayax — see Payments & Nayax). The honest enforcement boundary:

The existing per-product age gate (goods with minAge > 0 require the on-machine ID scan, /apk/ageVerify) is unchanged and still applies when the machine-level gate is off. When both apply, the phone verification proves the higher threshold.

Didit wiring

Secrets ride the same seam as Stripe (env → k8s secret kiosk-x-secrets):

Env What
KIOSKX_DIDIT_API_KEY Didit application API key (x-api-key). The org's key lives in the cluster secret didit-mcp-secret (ns aicart).
KIOSKX_DIDIT_WORKFLOW_ID Verification workflow UUID (document + liveness + age).
KIOSKX_DIDIT_WEBHOOK_SECRET HMAC secret for POST /verify/webhooks/didit (X-Signature / X-Signature-V2 / simple schemes all verified; unsigned → 401). Point Didit's webhook at https://api.kiosk-x.ai/verify/webhooks/didit.
KIOSKX_DIDIT_BASE_URL Default https://verification.didit.me.

Webhook delivery is idempotent (a replay never double-ledgers or re-opens a decided session) and the buyer's return page polls the decision as a fallback, so a lost webhook cannot strand a verified buyer.

No key configured (sandbox) → the flow serves a clearly-labelled simulated ID check — the same honesty pattern as scanpay's simulated pay page. It returns 404 the moment Didit is configured.

Privacy posture

Operator visibility