Booking feed
CoinOp is a till on a server inside the venue. It has no address on the internet and is not going to get one — it holds every customer record and takes payments, and a booking form is not a good enough reason to put that machine where the world can reach it.
So the traffic goes the other way. CoinOp asks the website, once a minute, over HTTPS. The website exposes one read-only endpoint and nothing else changes. That is the whole integration.
1 · The endpoint
GET https://<your-site>/api/coinop/bookings?since=2026-08-03T09:00:00.000Z&sinceMs=1785542400000
Authorization: Bearer <token>- HTTPS only. The response carries customer names and phone numbers.
- Bearer token that you generate and give to the venue once. Any long random string; rotate it by changing it in both places.
- A missing or wrong token is 401. Never fall back to serving anything.
2 · since
The same instant, sent twice, in two formats. Use whichever you prefer and ignore the other.
| Parameter | Looks like | For |
|---|---|---|
since | 2026-08-03T09:00:00.000Z | anything that date-parses a string |
sinceMs | 1785542400000 | anything that would rather have an integer |
Return every booking whose updatedAt is greater than or equal to it.
updatedAt, never on startsAt. A booking for next Friday that was paid for this morning has to arrive today, and one for tonight that was cancelled a minute ago has to arrive now. Filtering on startsAt loses the first and delays the second.CoinOp always sends it, and asks for the last seven days on its first run. Overlapping is fine — CoinOp keys on id and applies the same booking twice with no effect. Returning too little is the failure that matters, not too much.
A since you cannot parse is a 400. Never an empty list.
3 · The response
200 OK, content-type: application/json.
{
"bookings": [
{
"id": "bk_1042",
"status": "confirmed",
"resource": { "kind": "billiards", "name": "3" },
"startsAt": "2026-08-10T19:00:00+04:00",
"minutes": 60,
"customer": { "name": "Rami Haddad", "phone": "+971501234567" },
"fee": { "amount": 3000, "paid": true, "method": "stripe", "reference": "pi_3Q7xKl2eZvKY" },
"note": "birthday, 6 people",
"updatedAt": "2026-08-09T12:58:04+04:00"
}
]
}{"bookings": []} when there is nothing. Never a bare null, never a 404, never a 500 for “no results” — an empty list is a successful answer, and CoinOp treats anything else as the website being broken.4 · Fields
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | Stable and unique forever. Never reused, never changed — not on reschedule. |
status | "confirmed" | "cancelled" | yes | Cancellations must be sent, not deleted. |
resource.kind | "billiards" | "ps5" | "cinema" | yes | Lower case, exactly these. |
resource.name | string | null | no | A specific table if the customer chose one, otherwise null. |
startsAt | ISO 8601 with offset | yes | 2026-08-10T19:00:00+04:00 |
minutes | integer > 0 | yes | How long they booked. |
customer.name | string | yes | As given. |
customer.phone | string | yes | E.164 preferred: +971501234567 |
fee.amount | integer | yes | Fils. 3000 is AED 30.00. |
fee.paid | boolean | yes | True once the fee is settled, however it was settled. |
fee.method | "stripe" | "manual" | wanted | Decides whether the money enters CoinOp’s books. |
fee.reference | string | for a real payment | Payment intent or charge id — what ties a figure to a payout. |
fee.refunded | integer | no | Fils refunded, if any. |
note | string | null | no | Anything staff should see. Shown on the floor. |
updatedAt | ISO 8601 with offset | yes | Changes whenever anything above changes. |
Anything else you send is ignored, so extra fields are safe to add. Do not send card numbers, addresses, or anything CoinOp has no use for.
5 · Which bookings to include
Only bookings that are paid and confirmed, or that were and are now cancelled. A half-finished checkout — the customer opened Stripe and wandered off — should never appear. If it later completes, send it then, with updatedAt set to the moment it completed.
6 · Cancellations
Send them, with "status": "cancelled" and a bumped updatedAt. This is not bookkeeping: if a cancellation never arrives, staff hold a table at 8pm for somebody who cancelled at noon, and the table sits empty on a Friday. Deleting the row on your side and saying nothing is the one failure mode that costs the venue money.
Keep cancelled bookings in the feed for at least 7 days, so a CoinOp that was switched off over a weekend still learns about them.
7 · When something is wrong, say so
400 for a request you cannot parse, 401 for a bad token, 5xx if you are broken. Never answer an error with 200 and an empty list: that is indistinguishable from a quiet night, and it is how a booking somebody had made and paid for became invisible on every screen in the building.