Skip to content
v26.3

Disco API (Cashless Club)

The Disco module models a fully cashless club/nightclub operation: each guest receives a card (chip/RFID/NFC) at entry, which serves as an account for the entire stay. Everything — entry, drinks, coat check, table orders — is booked to the card and only settled on leaving (or via a prepaid top-up). The API covers the entire lifecycle of a card: enter → book → top up → settle → leave.

All endpoints live under the shared prefix api/v1/disco (three controllers share this prefix: guests/cards, statistics, configuration — plus the turnstile hardware API under api/v1/disco/turnstile). As in all areas: JWT bearer, response envelope { "success": true, "data": { ... } }, camelCase fields, opaque IDs.

Requires the Disco module

Requires the Disco module; without an active module license, the endpoints return HTTP 403 (errorCode: "MODULE_NOT_LICENSED").

Card ID vs. internal ID

Every record has an internal id (database key) and a guestId — this is the card number physically printed on the chip. All {cardId} routes expect the guestId, not the internal id.

Guests & Cards

DiscoController — base api/v1/disco:

Method Endpoint Purpose
POST /enter Admit a guest at the turnstile (card scan, entrance)
POST /leave Check out a guest with payment (scan, exit)
POST /leave-combined Settle & close multiple cards ("couples") together on one receipt
POST /book Book an article to a card (bar/counter)
POST /book/batch Book multiple articles to a card at once
POST /book/{guestId}/{bonIndex}/void Void a booked line item
POST /table/book-on-card Book a table order to a card (replaces table payment)
POST /wardrobe/return/{guestId}/{bonIndex} Coat-check return (removes the ticket number, amount remains)
GET /guest/{cardId} Guest/card incl. current spend (open cards only)
GET /guest/{cardId}/status Card status, including locked/voided/settled
GET /guest/{cardId}/checkout Settlement view: amount owed, prepaid coverage, may-leave
GET /guests All currently present (checked-in) guests
POST /guest/{cardId}/topup Top up prepaid balance
POST /guest/{cardId}/transfer Transfer card/balance to another card (card swap)
POST /guest/{cardId}/lock Lock the card (no further bookings possible)
POST /guest/{cardId}/unlock Unlock the card
POST /guest/{cardId}/storno Void the entire card + mark as non-payer
POST /guest/{cardId}/collect-storno Collect payment for a voided card afterwards at the till
POST /guest/{cardId}/image Store a card photo
POST /terminal/pay Trigger a self-service card payment at the info terminal
POST /personal/{cardId}/close Settle a staff card (own consumption)
POST /personal/close-all Settle all open staff cards as own consumption
POST /close-all Close all open cards (end of operation/end-of-day report)

Booking to a Card

POST /api/v1/disco/book books an article to an open card (bar/counter sale without payment — payment only happens at leave/checkout).

POST /api/v1/disco/book
Authorization: Bearer eyJ...
Content-Type: application/json

{
  "guestId": "crd_10042",
  "articleId": "art_cola",
  "articleName": "Cola 0,3l",
  "count": 2,
  "price": 3.50,
  "workplaceId": "wp_bar1",
  "workplaceName": "Bar 1"
}

Response (200 OK, DiscoGuestResponse):

{
  "success": true,
  "data": {
    "id": "dgu_9f8e7d",
    "guestId": "crd_10042",
    "gender": 1,
    "enterDate": "2026-07-12T22:05:00Z",
    "amount": 7.00,
    "payedAmount": 0,
    "uploadAccount": 20.00,
    "iou": 0,
    "isOpen": true,
    "isLocked": false,
    "openBons": [
      { "articleId": "art_cola", "articleName": "Cola 0,3l", "price": 3.50, "count": 2, "total": 7.00,
        "workplaceId": "wp_bar1", "workplaceName": "Bar 1", "bookDate": "2026-07-12T23:10:00Z", "isVoided": false }
    ]
  }
}

Card Status

GET /api/v1/disco/guest/{cardId}/status returns the status regardless of whether the card is still open — unlike GET /guest/{cardId}, which only finds open cards. This lets the bar/coat check/till also recognize locked or voided cards.

GET /api/v1/disco/guest/crd_10042/status
Authorization: Bearer eyJ...
{
  "success": true,
  "data": {
    "found": true,
    "isOpen": true,
    "isLocked": false,
    "stornoed": false,
    "guest": { "id": "dgu_9f8e7d", "guestId": "crd_10042", "amount": 7.00, "uploadAccount": 20.00 }
  }
}

Current Spend

GET /api/v1/disco/guest/{cardId} returns the same DiscoGuestResponse as above, including the current open bookings (openBons) and the prepaid balance (uploadAccount). Response 404 GUEST_NOT_FOUND if the card is not (or no longer) open.

Settlement/Checkout

GET /api/v1/disco/guest/{cardId}/checkout changes nothing — a pure preview of what the card still owes, what the prepaid balance covers, and whether it may exit automatically (self-checkout, mobile exit till, turnstile).

{
  "success": true,
  "data": {
    "cardId": "crd_10042",
    "found": true,
    "isOpen": true,
    "isLocked": false,
    "consumption": 7.00,
    "netOwed": 7.00,
    "prepaid": 20.00,
    "amountToPay": 0,
    "refund": 13.00,
    "canAutoSettle": true,
    "status": "ok",
    "message": "Karte kann raus — Restguthaben 13.00 €"
  }
}

status is one of ok | pay | locked | closed | notfound.

Table to Card

POST /api/v1/disco/table/book-on-card moves a table's open line items onto a Disco card (Disco table mode) — replaces cash/card payment at the table; the card limits continue to apply, and the table becomes free.

{ "tableId": "tbl_12", "cardId": "crd_10042", "openBonIds": null }

openBonIds: null transfers all open line items; a list transfers only one "immediate" round.

Topping Up

POST /api/v1/disco/guest/{cardId}/topup tops up the card's prepaid balance (uploadAccount) — the amount is freely chosen, and any remaining balance is refunded at checkout.

{ "amount": 20.00, "paymentMethod": "Bar" }

The response is again the updated DiscoGuestResponse (uploadAccount increased).

Risk of confusion: Disco card balance vs. customer account balance

This is the card prepaid balance for a single Disco visit (expires/is refunded on leaving). A permanent customer account balance is a separate concept — see Customer Account: Balance & Loyalty Points.

Statistics

DiscoStatsController — base api/v1/disco:

Method Endpoint Purpose
GET /stats Live metrics (revenue, guests present, utilization by group/workstation/staff/hour)
GET /search Search cards/guests (card number, name, period, gender, void filter, paging)
GET /personal/{ownerId}/activity Activity of a staff card (consumption, voids, breakage)
GET /daylog/{date} Daily log for a date

Example: GET /api/v1/disco/stats

{
  "success": true,
  "data": {
    "totalGuests": 214,
    "currentGuests": 87,
    "totalMale": 96,
    "totalFemale": 118,
    "totalRevenue": 4211.50,
    "averageSpend": 19.68,
    "byGroup": [ { "groupId": "eg_std", "groupName": "Standard", "count": 180, "revenue": 3500.00 } ],
    "byWorkplace": [ { "workplaceId": "wp_bar1", "workplaceName": "Bar 1", "count": 400, "revenue": 2200.00 } ],
    "byStaff": [ { "personalId": "per_1", "personalName": "Mo", "count": 120, "revenue": 900.00 } ],
    "byHour": [ { "hour": 23, "count": 60, "revenue": 800.00 } ]
  }
}

GET /search supports, among others, cardId, name, dateFrom/dateTo, gender, timeFrom/timeTo, workplaceId, enterGroupId, wardrobe, stornoed, offset/limit, excludeImages.

Configuration

DiscoConfigController — base api/v1/disco, classic CRUD for entry/price groups ("entergroups" — define e.g. the entry price, card limit, free drinks, minimum spend):

Method Endpoint Purpose
GET /entergroups All entry groups
GET /entergroups/{id} A single entry group
POST /entergroups Create an entry group
PUT /entergroups/{id} Update an entry group
DELETE /entergroups/{id} Delete an entry group

Turnstile Hardware

DiscoTurnstileControllerapi/v1/disco/turnstile/{key}/{cardId} (GET/POST).

No JWT — authentication via an API key in the URL ({key}), so that a simple turnstile controller can call the route without a login flow. 200 = card may exit, every other code carries a reason (locked/open/already settled/unknown/wrong key).

→ Details, setup, and the status code table: Disco Turnstile.

End-to-End Flow

A typical guest journey, all steps against crd_10042:

TOKEN="eyJ..."
API="https://ihre-instanz.dikas.de/api/v1"

# 1. Admission at the turnstile (card scan, entrance)
curl -X POST "$API/disco/enter" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "guestId": "crd_10042", "gender": 1, "enterGroupId": "eg_std" }'

# 2. Check status (e.g. re-entry after briefly leaving)
curl "$API/disco/guest/crd_10042/status" -H "Authorization: Bearer $TOKEN"

# 3. Top up prepaid balance
curl -X POST "$API/disco/guest/crd_10042/topup" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "amount": 20.00, "paymentMethod": "Bar" }'

# 4. Book at the bar (individually or as a batch)
curl -X POST "$API/disco/book" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "guestId": "crd_10042", "articleId": "art_cola", "articleName": "Cola 0,3l", "count": 2, "price": 3.50 }'

# 5. Query current spend
curl "$API/disco/guest/crd_10042" -H "Authorization: Bearer $TOKEN"

# 6. Book a table order to the card
curl -X POST "$API/disco/table/book-on-card" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "tableId": "tbl_12", "cardId": "crd_10042" }'

# 7. Before exiting: checkout preview
curl "$API/disco/guest/crd_10042/checkout" -H "Authorization: Bearer $TOKEN"

# 8. Leave with payment (if anything is still owed)
curl -X POST "$API/disco/leave" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "cardId": "crd_10042", "payments": [ { "method": "Cash", "amount": 0 } ] }'

Incident — card cannot pay at the exit (non-payer):

# Void the card: mark as non-payer, book consumption as shrinkage
curl -X POST "$API/disco/guest/crd_10042/storno" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "reason": "Karte verloren" }'

# Collected later at the till
curl -X POST "$API/disco/guest/crd_10042/collect-storno" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "payments": [ { "method": "Cash", "amount": 7.00 } ] }'