Till (cash drawer)
Open the till with the cash in the drawer, put cash in or take it out during the day, and close it with what you counted. These are the same tills the POS screen opens and closes, so a till opened here shows as open on the POS and in the staff app.
Cash sales, cash refunds and cash security deposits are never sent here. They are read from the payments already recorded while the till is open. Each one counts at the till it was taken at, not at the booking's pickup location: a cash payment taken at one location for a booking picked up at another counts at the first.
Should be in the drawer = started with + cash sales - cash refunds + deposits held - deposits returned + cash put in - cash taken out.
Which till a payment counts at
A payment, refund or deposit recorded while a till is open is linked to that till when it is recorded:
- Send
location_idwith Add a payment or a security deposit record, release or capture, and it counts at the till open at that location. A location from another account returns422. - Without
location_id, it counts at the shop's open till when exactly one is open. With two or more open, it is recorded but counted at no till. - A payment a customer makes online never counts at a till.
Payments recorded before a till was opened, or before tills existed, are not counted.
Required scopes: bookings:read to read, bookings:write to open, move cash and close.
Locations
A shop with one location has one till: leave location_id out. A shop with two or more locations has a till per location, and location_id is required. A location from another account returns 422.
GET /api/v1/till
The till at a location: whether it is open, its figures, and what the next opening would be filled in with.
Query parameters
| Parameter | Type | Description |
|---|---|---|
location_id | integer | The till's location. Required when the shop has two or more locations. |
Response
{
"data": {
"enabled": true,
"location_id": null,
"till": {
"id": 41,
"status": "open",
"location_id": null,
"location_name": null,
"opened_at": "2026-10-16T15:02:00+00:00",
"opened_at_local": "8:02 AM",
"opened_by": "Jane Doe",
"closed_at": null,
"closed_by": null,
"note": null,
"currency": "USD",
"started_with": 200,
"cash_sales": 486.5,
"cash_sales_count": 14,
"cash_refunds": 20,
"cash_refunds_count": 1,
"deposits_held": 100,
"deposits_held_count": 1,
"deposits_returned": 50,
"deposits_returned_count": 1,
"not_counted": 40,
"not_counted_count": 2,
"cash_in": 0,
"cash_out": 15,
"movements_net": -15,
"movements_count": 1,
"movements": [
{ "id": 7, "direction": "out", "amount": 15, "reason": "Postage", "at": "2026-10-16T19:10:00+00:00" }
],
"expected": 701.5,
"counted": null,
"difference": null,
"left_for_tomorrow": null,
"to_bank": null
},
"opened_today": true,
"suggested_float": 200,
"suggested_float_from_last_close": true,
"leave_for_tomorrow_default": 200,
"summary_email_to": "owner@example.test",
"summary_email_to_name": "Jane"
}
}
| Field | Description |
|---|---|
enabled | Whether the shop opens and closes its till (Settings > Point of sale). Off until the owner switches it on. |
till | The open till, or null when none is open here. |
till.deposits_held | Security deposits taken in cash at this till, and deposits_held_count how many. A deposit kept for damage stays in the drawer. |
till.deposits_returned | Cash deposits handed back at this till, and deposits_returned_count how many. Not part of movements. |
till.not_counted | Cash taken today that no till counted (for a closed till, on the day it closed), and not_counted_count how many payments. This happens when cash was taken before a till was open, or when several tills were open and nothing said which one. Every till shows the same figure that day, and it is not part of expected. A team member limited to some locations sees only cash for bookings at those locations. POST /api/v1/till/add-not-counted counts it in a till. |
opened_today | A till was opened here today, in the shop's own day. |
suggested_float | What the opening is filled in with: what was left at the last close, else the shop's opening float, else null. |
leave_for_tomorrow_default | What a close fills in for the cash left for tomorrow: the shop's opening float, else what this till started with. null when no till is open. |
summary_email_to | Where the end-of-day summary goes, and summary_email_to_name who that is. On the staff app, the address is null for anyone without Point of sale settings; they still get the name. |
POST /api/v1/till/open
Open the till. If a till is already open at this location, nothing new is opened and that till comes back with already_open: true and status 200, so a repeated request is safe.
| Field | Type | Description |
|---|---|---|
amount | number | Required. The cash in the drawer. Zero or more. |
location_id | integer | See Locations. |
Returns 201 with the same body as GET /till plus already_open.
POST /api/v1/till/cash
Put cash in or take it out of the open till.
| Field | Type | Description |
|---|---|---|
direction | string | Required. in or out. |
amount | number | Required. Above zero. |
reason | string | Optional. Up to 120 characters, e.g. "Change from the bank". |
location_id | integer | See Locations. |
Returns 201 with the till's new figures. 409 till_not_open when no till is open here.
POST /api/v1/till/close
Close the open till with what was counted. "Should be in the drawer" is saved with it, so a payment edited later never changes a day that was already counted. The end-of-day summary is emailed when the till closes.
| Field | Type | Description |
|---|---|---|
counted | number | Required. The cash counted in the drawer. |
left_for_tomorrow | number | Optional. Cash left in the drawer for the next opening. Left out means nothing is left. Cannot be more than counted. |
note | string | Optional. Up to 1,000 characters. |
location_id | integer | See Locations. |
Response
{
"data": {
"closed": {
"id": 41,
"status": "closed",
"expected": 701.5,
"counted": 697.5,
"difference": -4,
"left_for_tomorrow": 200,
"to_bank": 497.5
},
"summary_emailed_to": "owner@example.test",
"enabled": true,
"till": null,
"opened_today": true,
"suggested_float": 200,
"suggested_float_from_last_close": true
}
}
summary_emailed_to is null when the summary did not go out (no address, or the send failed). The till is closed either way.
difference is counted minus expected: below zero is short, above zero is over. (closed carries every field of a till; some are left out above.)
POST /api/v1/till/add-not-counted
Count today's cash that no till counted (not_counted) in the open till at this location, so expected includes it. A second call moves nothing new, and added.count is 0. Bank transfers and other non-cash payments are never moved.
| Field | Type | Description |
|---|---|---|
location_id | integer | The till's location. Required when you have more than one location. |
{
"data": {
"added": { "amount": 40, "count": 2 },
"till": { "expected": 240, "not_counted": 0, "not_counted_count": 0 }
}
}
The rest of the response is the same as GET /api/v1/till. A till that is not open returns 409 till_not_open. One closed by another device while this request runs returns 409 till_closed.
Errors
| Status | Code | When |
|---|---|---|
409 | till_not_open | Cash in or out, or close, with no till open at this location. |
409 | till_off | The shop has switched opening and closing the till off. |
409 | till_busy | Another device was saving to this till at the same moment. Send the request again. |
422 | validation_error | A missing or negative amount, a location that is not the shop's, or leaving more than was counted. |
Staff app
The staff app calls the same endpoints under /api/v1/mobile/till with the signed-in person's token. It needs the Record payments permission, and opens and closes are written under that person's name. Through an API key they carry no name.