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.
{
"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.
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.
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¶
DiscoTurnstileController — api/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 } ] }'