Contract 2026-10-10. Every path below carries its own major version.
Staff · Reference
Labor
What the hours a shift records cost, and what the tips on them come to: pay rates, a
location's tip-pooling rule, and the allocation approving a period wrote. Kept apart from
Shifts because a shift is who worked and this is what they are paid for it — which is one
person's business and their manager's, and has scopes of its own rather than the inclusive
shift ones a staff member may hold.
Returns whether a location pools its tips, how each role's hours are weighted in the split,
and its written tipping policy.
A location that has never been configured answers enabled: false with even weights rather
than 404: there is always an answer to how a site shares tips, and the default — each
member keeps the tips on the orders they took — is a deliberate one a cafe with a jar should
be able to stay on.
Reading this needs the labour scope, because a policy and its weights say what colleagues are
paid. Setting it is POST /v1/tip-pool-rules/batch-upsert.
Parameters
idpath · string · uuid
Required
UUIDv7 of the resource.
For resources a device can originate — orders, and their line items — this id is minted
by the client before the record is ever synced, which is what makes an offline create
idempotent.
Responses
200
How the location shares its tips.
TipPoolRule · application/json
enabledboolean
Required
Whether the location pools its tips and divides them by hours worked. False means each
member keeps the tips on the orders they took.
location_idstring · uuid
Required
policystring
The location's written tipping policy, stored with the rule it describes and shown to
staff. The Act requires one wherever tips are more than occasional. Absent when the
location has not written one.
role_weightsarray of TipRoleWeight
Required
How each role's hours are weighted in the split. A role absent from the list weighs one,
so an empty list is an even split by hours.
rolestring · enum
Required
ownermanagerstaff
weightinteger · int32
Required
1–100
400
The request could not be parsed. Malformed JSON, a bad query parameter, or a
header that failed to decode, among them an API-Version later than the server speaks or not
a date (api-version-unknown, with the server's own in api_version).
Distinct from 422: a 400 never reached validation, so invalid_params is
absent. Retrying without changing the request will fail again, except that a request refused
for its API-Version goes once the server is updated.
401
No credentials, or credentials that could not be verified — missing bearer token,
malformed JWT, expired token, or a signature that failed against the issuer's
JWKS.
Refresh the token and retry once. Repeated 401 after a refresh means the device
needs to re-enrol.
403
Authenticated, but not permitted. The token is valid and its merchant was
resolved, but the caller's role does not allow this operation, or the target
belongs to a different merchant.
Cross-tenant access is reported as 403 on a resource the caller may not see,
and as 404 where even confirming existence would leak information. Retrying
will not help.
404
No such resource, or none visible to this caller.
Also returned instead of 403 where acknowledging existence would itself leak
information — so a 404 does not prove the id is unused globally, only that it is
not yours.
410
The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in
it was read. api_version names the version the server speaks: the client is behind, and sends
the request again once it is updated.
429
Rate limit exceeded. Honour Retry-After rather than applying your own backoff.
A fleet of tills coming back online together after an outage is the realistic cause,
which is exactly when uncoordinated client-side retry turns a brief outage into a
sustained one. The limit is per process, and per site for what records trade there —
a request spends its site's budget, and a till's own smaller one inside it — and per
(client, merchant) for everything else; no RateLimit field is sent, so this response
and its Retry-After are the whole of the contract.
500
Something failed on our side. trace_id identifies the request in our tracing —
quote it in a support ticket.
The body never carries internal detail: no SQL, no stack traces, no upstream error
strings. Safe to retry with the same Idempotency-Key.
503
Temporarily unable to serve the request — a dependency is down or the service is
shedding load.
Retry with the same Idempotency-Key after Retry-After. This is always safe: a
request that did take effect before the failure will be recognised as a replay.
Returns every hourly rate a member has been given, per location, newest first, and where
each stands today at its location: in force, replaced by a later one, or still to start.
A rate is never changed once set. A raise is a new rate from a date, so what past shifts
cost stays as they were worked. A manager held to particular locations reads the rates at
those and no others. Pay needs the labour scope, so a staff member, a till and a support
visit are refused.
Parameters
idpath · string · uuid
Required
UUIDv7 of the resource.
For resources a device can originate — orders, and their line items — this id is minted
by the client before the record is ever synced, which is what makes an offline create
idempotent.
Responses
200
The member's pay rates.
MemberPayRates · application/json
member_idstring · uuid
Required
ratesarray of MemberPayRate
Required
effective_fromstring · date
Required
The first business date a shift there costs this.
location_idstring · uuid
Required
rateMoney
Required
A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its
currency.
The amount is always an integer count of the currency's minor unit — pence for
GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal
to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while
{"amount": 500, "currency": "GBP"} is £5.00.
Floating point is never used for money anywhere in this API. Binary floats cannot
represent most decimal fractions exactly, so accumulating them across the line items of
an order produces totals that disagree with the printed receipt by a penny. Integer
minor units make every arithmetic operation exact.
amountinteger · int64
Required
The amount, as a whole number of the currency's minor unit. May be negative to
express a refund, discount or credit.
currencyCurrency
Required
ISO 4217 currency code.
Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.
One of 182 values; see the schema.
standingstring · enum
Required
Where the rate stands on today's business date at its location: in_force is what a
shift today costs, replaced has been superseded by a later rate that has started, and
scheduled has not started yet.
in_forcereplacedscheduled
400
The request could not be parsed. Malformed JSON, a bad query parameter, or a
header that failed to decode, among them an API-Version later than the server speaks or not
a date (api-version-unknown, with the server's own in api_version).
Distinct from 422: a 400 never reached validation, so invalid_params is
absent. Retrying without changing the request will fail again, except that a request refused
for its API-Version goes once the server is updated.
401
No credentials, or credentials that could not be verified — missing bearer token,
malformed JWT, expired token, or a signature that failed against the issuer's
JWKS.
Refresh the token and retry once. Repeated 401 after a refresh means the device
needs to re-enrol.
403
Authenticated, but not permitted. The token is valid and its merchant was
resolved, but the caller's role does not allow this operation, or the target
belongs to a different merchant.
Cross-tenant access is reported as 403 on a resource the caller may not see,
and as 404 where even confirming existence would leak information. Retrying
will not help.
404
No such resource, or none visible to this caller.
Also returned instead of 403 where acknowledging existence would itself leak
information — so a 404 does not prove the id is unused globally, only that it is
not yours.
410
The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in
it was read. api_version names the version the server speaks: the client is behind, and sends
the request again once it is updated.
429
Rate limit exceeded. Honour Retry-After rather than applying your own backoff.
A fleet of tills coming back online together after an outage is the realistic cause,
which is exactly when uncoordinated client-side retry turns a brief outage into a
sustained one. The limit is per process, and per site for what records trade there —
a request spends its site's budget, and a till's own smaller one inside it — and per
(client, merchant) for everything else; no RateLimit field is sent, so this response
and its Retry-After are the whole of the contract.
500
Something failed on our side. trace_id identifies the request in our tracing —
quote it in a support ticket.
The body never carries internal detail: no SQL, no stack traces, no upstream error
strings. Safe to retry with the same Idempotency-Key.
503
Temporarily unable to serve the request — a dependency is down or the service is
shedding load.
Retry with the same Idempotency-Key after Retry-After. This is always safe: a
request that did take effect before the failure will be recognised as a replay.
Returns what one member was actually allocated in tips, per location and approved period —
the record the Employment (Allocation of Tips) Act 2023 gives a worker the right to ask for,
reached by the manager who has to answer them.
Only approval writes an allocation, so a period nobody has approved is absent rather than
estimated, and changing a location's pooling rule afterwards does not rewrite a period already
approved. The figures are what was decided at the time.
Totals are per currency. A member who works at sites trading in different currencies was
allocated each in its own, and one summed figure would be an amount in neither.
A manager held to particular locations reads what those sites allocated and not what another
site paid. Pay needs the labour scope: a staff member reading a colleague's record, a till and
a support visit are all refused.
Parameters
idpath · string · uuid
Required
UUIDv7 of the resource.
For resources a device can originate — orders, and their line items — this id is minted
by the client before the record is ever synced, which is what makes an offline create
idempotent.
fromquery · string · date
The first business date, inclusive. Defaults to six days before today.
toquery · string · date
The last business date, inclusive. Defaults to today.
Responses
200
The member's recorded allocations over the range.
MemberTipRecord · application/json
namestring
The member's name as the roster has it now. Absent if they are no longer on it.
allocationsarray of TipAllocation
Required
One entry per location and approved period that allocated this member anything, newest
period first.
location_idstring · uuid
Required
location_namestring
Required
The site that allocated it, so the record can be read without a second call.
period_endstring · date
Required
The last business date of the approved period.
period_startstring · date
Required
The first business date of the approved period.
tipsMoney
Required
What was allocated, in the location's currency — the figure the rule in force at the time
produced, not what today's rule would produce.
amountinteger · int64
Required
The amount, as a whole number of the currency's minor unit. May be negative to
express a refund, discount or credit.
currencyCurrency
Required
ISO 4217 currency code.
Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.
One of 182 values; see the schema.
fromstring · date
Required
The first business date asked for, inclusive.
member_idstring · uuid
Required
tostring · date
Required
The last business date asked for, inclusive.
totalsarray of Money
Required
What the allocations come to, one entry per currency. Empty when nothing was allocated —
which is not the same as zero in the merchant's currency, because a member with no
approved period has no currency to be owed in.
amountinteger · int64
Required
The amount, as a whole number of the currency's minor unit. May be negative to
express a refund, discount or credit.
currencyCurrency
Required
ISO 4217 currency code.
Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.
One of 182 values; see the schema.
400
The request could not be parsed. Malformed JSON, a bad query parameter, or a
header that failed to decode, among them an API-Version later than the server speaks or not
a date (api-version-unknown, with the server's own in api_version).
Distinct from 422: a 400 never reached validation, so invalid_params is
absent. Retrying without changing the request will fail again, except that a request refused
for its API-Version goes once the server is updated.
401
No credentials, or credentials that could not be verified — missing bearer token,
malformed JWT, expired token, or a signature that failed against the issuer's
JWKS.
Refresh the token and retry once. Repeated 401 after a refresh means the device
needs to re-enrol.
403
Authenticated, but not permitted. The token is valid and its merchant was
resolved, but the caller's role does not allow this operation, or the target
belongs to a different merchant.
Cross-tenant access is reported as 403 on a resource the caller may not see,
and as 404 where even confirming existence would leak information. Retrying
will not help.
404
No such resource, or none visible to this caller.
Also returned instead of 403 where acknowledging existence would itself leak
information — so a 404 does not prove the id is unused globally, only that it is
not yours.
410
The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in
it was read. api_version names the version the server speaks: the client is behind, and sends
the request again once it is updated.
422
The request parsed but failed validation.
invalid_params lists every failing field with a JSON Pointer, not just the
first, so a client can mark up a whole form in one pass rather than making the
user fix errors one round trip at a time.
Also returned when an Idempotency-Key is replayed with a different body, and when
device-computed order totals disagree with the server's recomputation.
429
Rate limit exceeded. Honour Retry-After rather than applying your own backoff.
A fleet of tills coming back online together after an outage is the realistic cause,
which is exactly when uncoordinated client-side retry turns a brief outage into a
sustained one. The limit is per process, and per site for what records trade there —
a request spends its site's budget, and a till's own smaller one inside it — and per
(client, merchant) for everything else; no RateLimit field is sent, so this response
and its Retry-After are the whole of the contract.
500
Something failed on our side. trace_id identifies the request in our tracing —
quote it in a support ticket.
The body never carries internal detail: no SQL, no stack traces, no upstream error
strings. Safe to retry with the same Idempotency-Key.
503
Temporarily unable to serve the request — a dependency is down or the service is
shedding load.
Retry with the same Idempotency-Key after Retry-After. This is always safe: a
request that did take effect before the failure will be recognised as a replay.
Returns a member's hours this week so far and the two weeks before, Monday to Sunday at the
estate's clock, each with where it stands on its way to payroll: still open, waiting for a
manager's approval, approved, or nothing worked.
The hours are the timesheet's: closed shifts net of breaks with the latest correction
applied, so the figure here is the one payroll pays. A manager held to particular
locations counts the shifts at those and no others. Hours need the labour scope.
Parameters
idpath · string · uuid
Required
UUIDv7 of the resource.
For resources a device can originate — orders, and their line items — this id is minted
by the client before the record is ever synced, which is what makes an offline create
idempotent.
Responses
200
The member's weeks.
MemberWeeks · application/json
member_idstring · uuid
Required
weeksarray of MemberWeek
Required
minutes_workedinteger · int64
Required
≥ 0
shiftsinteger
Required
The shifts closed in the week. One still open is not counted.
≥ 0
standingstring · enum
Required
open is the week now running; to_approve a past week with hours no approval covers;
approved one an approval covers; nothing_worked a past week with no closed shift.
opento_approveapprovednothing_worked
starts_onstring · date
Required
The Monday the week starts on.
400
The request could not be parsed. Malformed JSON, a bad query parameter, or a
header that failed to decode, among them an API-Version later than the server speaks or not
a date (api-version-unknown, with the server's own in api_version).
Distinct from 422: a 400 never reached validation, so invalid_params is
absent. Retrying without changing the request will fail again, except that a request refused
for its API-Version goes once the server is updated.
401
No credentials, or credentials that could not be verified — missing bearer token,
malformed JWT, expired token, or a signature that failed against the issuer's
JWKS.
Refresh the token and retry once. Repeated 401 after a refresh means the device
needs to re-enrol.
403
Authenticated, but not permitted. The token is valid and its merchant was
resolved, but the caller's role does not allow this operation, or the target
belongs to a different merchant.
Cross-tenant access is reported as 403 on a resource the caller may not see,
and as 404 where even confirming existence would leak information. Retrying
will not help.
404
No such resource, or none visible to this caller.
Also returned instead of 403 where acknowledging existence would itself leak
information — so a 404 does not prove the id is unused globally, only that it is
not yours.
410
The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in
it was read. api_version names the version the server speaks: the client is behind, and sends
the request again once it is updated.
429
Rate limit exceeded. Honour Retry-After rather than applying your own backoff.
A fleet of tills coming back online together after an outage is the realistic cause,
which is exactly when uncoordinated client-side retry turns a brief outage into a
sustained one. The limit is per process, and per site for what records trade there —
a request spends its site's budget, and a till's own smaller one inside it — and per
(client, merchant) for everything else; no RateLimit field is sent, so this response
and its Retry-After are the whole of the contract.
500
Something failed on our side. trace_id identifies the request in our tracing —
quote it in a support ticket.
The body never carries internal detail: no SQL, no stack traces, no upstream error
strings. Safe to retry with the same Idempotency-Key.
503
Temporarily unable to serve the request — a dependency is down or the service is
shedding load.
Retry with the same Idempotency-Key after Retry-After. This is always safe: a
request that did take effect before the failure will be recognised as a replay.
Records what members are paid per hour at a location, from a date. One item per member and
location, judged on its own, so a whole roster's review goes in one call and a single bad item
fails itself.
A rate is never edited: a raise is a new item with a later effective_from, and the rate a
past shift was costed at stays readable. That is what lets a payroll run be reproduced and a
question about an old payslip be answered. Sending a date that already carries a rate replaces
that one decision and leaves every other date alone.
The rate must be in the location's currency. An hour costed in euros cannot be added to a
sterling labour cost, and a rate whose currency is not the site's is refused rather than
summed into a total that is wrong in both.
This needs the labour write scope: setting what somebody is paid is a different act from
reading what they were paid.
Parameters
Idempotency-Keyheader · string
Required
A client-generated key making this request safe to retry.
Required on every mutation that creates a resource or moves money. A till on bad wifi
cannot tell a lost response from a lost request; without a key its only options are to
risk a duplicate sale or to drop a real one. With a key it retries freely: the first
request to arrive is processed, and every replay returns that same stored response.
Rules:
Mint the key once, when the operation is first queued, and reuse it for every
retry. A fresh key per attempt defeats the mechanism entirely.
Keys are scoped per merchant and per operation, and are retained for 30 days — long
enough that a till switched off mid-sync can retry with the same key however long it
stayed on the shelf.
Replaying a key with a different request body is a client bug and is rejected
with 422, rather than silently returning the first result — quietly serving the old
response would hide a real inconsistency in the caller.
A UUIDv7 or ULID is a good choice.
Request body application/json · PayRateBatchCreateRequest
itemsarray of PayRateBatchCreateItem
Required
≥ 1 items · ≤ 500 items
effective_fromstring · date
Required
The first business date the rate applies to. Sending a date that already has a rate
replaces that decision; every other date keeps the rate it had.
idempotency_keyItemIdempotencyKey
Required
A key for one item in a batch, minted by the client when it queues the item and reused
on every attempt to send it. The item, not the request, is the unit of work: a retried
batch returns the stored result for every item already applied under its key. A fresh
key per attempt defeats the whole mechanism.
Scoped per merchant, so keys need only be unique within a merchant's own traffic.
16–255 chars
location_idstring · uuid
Required
Where they are paid it. A location of another merchant is refused.
member_idstring · uuid
Required
The member being paid. Somebody who is not on this merchant's roster is refused.
rateMoney
Required
The hourly rate, which must be in the location's currency and may not be negative. Zero is
allowed, and means an unpaid role rather than an unknown one.
amountinteger · int64
Required
The amount, as a whole number of the currency's minor unit. May be negative to
express a refund, discount or credit.
currencyCurrency
Required
ISO 4217 currency code.
Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.
One of 182 values; see the schema.
Responses
200
The batch was processed. results says what happened to each item.
BatchResponse · application/json
resultsarray of BatchResult
Required
idstring · uuid
The resource the item created or changed. Absent when it failed.
idempotency_keystring
Required
The item's key, echoed so a result can be matched by key as well as position.
problemProblem
Nullable
Why the item failed, as an RFC 9457 problem. A 422 means the item is malformed and
will never succeed — drop it and show it to staff rather than retrying for ever. A
409 or a 5xx means try again later.
typestring · uri
Required
A stable URI identifying the problem kind. This is the field to branch on.
default about:blank
api_versionstring
Present on an api-version-retired or api-version-unknown refusal: the API-Version
this server speaks (docs/decisions/0011).
detailstring
A human-readable explanation specific to this occurrence. Safe to show to a
developer; never contains internal state, SQL, or stack traces.
instancestring · uri-reference
A URI reference identifying the specific occurrence.
invalid_paramsarray of InvalidParam
Present on 422 responses. Lists every field that failed validation, rather than
only the first, so a client can highlight an entire form in one pass.
codestring
Stable machine-readable code for this failure, for clients that localise their own
messages rather than showing reason.
pointerstring
Required
RFC 6901 JSON Pointer to the offending
value within the request body. A pointer rather than a field name so that failures
nested inside arrays — the third line item of an order — are addressable at all.
reasonstring
Required
Why this value was rejected, in human-readable form.
refusalsarray of Location refusal
Present on 409 responses to a withdrawal refused by the state it depends on
(deleting a location with tills still assigned, open orders, …). Lists every reason
at once, each with how much stands in the way, so a client can say what to put right
and link to it rather than walking a person through one refusal after another.
kindstring
Required
A stable refusal kind — devices, open_orders, drawer_sessions, bookings or
stock — that a client links to the screen that puts it right. Branch on this field,
never on the surrounding prose.
countinteger
Required
How many things stand in the way of the withdrawal, so a client can repeat the number rather than re-read prose.
≥ 1
statusinteger
Required
The HTTP status code, repeated here so the body is self-contained.
100–599
titlestring
Required
A short, human-readable summary of the problem kind.
trace_idstring
The OpenTelemetry trace id for this request. Quote it in a support ticket and the
whole request can be pulled up.
statusstring · enum
Required
created — the item made a new resource. updated — it changed an existing one.
unchanged — this key was already applied and the original outcome stands. A
success, not an error: it is what a client retrying a batch it was unsure landed
should expect, and it clears the item from its queue exactly as created does.
failed — the item was refused, and nothing else in the batch was affected. See
problem.
createdupdatedunchangedfailed
versioninteger · int64
The resource's version after the item. Absent when it failed.
400
The request could not be parsed. Malformed JSON, a bad query parameter, or a
header that failed to decode, among them an API-Version later than the server speaks or not
a date (api-version-unknown, with the server's own in api_version).
Distinct from 422: a 400 never reached validation, so invalid_params is
absent. Retrying without changing the request will fail again, except that a request refused
for its API-Version goes once the server is updated.
401
No credentials, or credentials that could not be verified — missing bearer token,
malformed JWT, expired token, or a signature that failed against the issuer's
JWKS.
Refresh the token and retry once. Repeated 401 after a refresh means the device
needs to re-enrol.
403
Authenticated, but not permitted. The token is valid and its merchant was
resolved, but the caller's role does not allow this operation, or the target
belongs to a different merchant.
Cross-tenant access is reported as 403 on a resource the caller may not see,
and as 404 where even confirming existence would leak information. Retrying
will not help.
410
The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in
it was read. api_version names the version the server speaks: the client is behind, and sends
the request again once it is updated.
415
The request Content-Type is not supported. This API accepts
application/json on all mutations.
422
The request parsed but failed validation.
invalid_params lists every failing field with a JSON Pointer, not just the
first, so a client can mark up a whole form in one pass rather than making the
user fix errors one round trip at a time.
Also returned when an Idempotency-Key is replayed with a different body, and when
device-computed order totals disagree with the server's recomputation.
429
Rate limit exceeded. Honour Retry-After rather than applying your own backoff.
A fleet of tills coming back online together after an outage is the realistic cause,
which is exactly when uncoordinated client-side retry turns a brief outage into a
sustained one. The limit is per process, and per site for what records trade there —
a request spends its site's budget, and a till's own smaller one inside it — and per
(client, merchant) for everything else; no RateLimit field is sent, so this response
and its Retry-After are the whole of the contract.
500
Something failed on our side. trace_id identifies the request in our tracing —
quote it in a support ticket.
The body never carries internal detail: no SQL, no stack traces, no upstream error
strings. Safe to retry with the same Idempotency-Key.
503
Temporarily unable to serve the request — a dependency is down or the service is
shedding load.
Retry with the same Idempotency-Key after Retry-After. This is always safe: a
request that did take effect before the failure will be recognised as a replay.
The signed-in person's own record: their hours after breaks this week and last, Monday to
Sunday at the business's home site, and the tips recorded for them over the last five weeks
with the rule each was split under, the record the Employment (Allocation of Tips) Act 2023
lets them ask for. It is always the caller's own; there is no member to name, and a device
or an integration has no record.
Responses
200
The record.
MyRecord · application/json
plannedarray of MyPlannedShift
Required
The caller's upcoming scheduled shifts from the rota, earliest first. A member who is not on the rota sees an empty list — expected, not an error.
idstring · uuid
Required
ends_atstring · date-time
Required
location_idstring · uuid
Required
location_namestring
Required
The site's name, so a worker sees where they are expected without a second call.
starts_atstring · date-time
Required
tip_totalsarray of Money
Required
The tips recorded, one total per currency.
amountinteger · int64
Required
The amount, as a whole number of the currency's minor unit. May be negative to
express a refund, discount or credit.
currencyCurrency
Required
ISO 4217 currency code.
Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.
One of 182 values; see the schema.
tipsarray of MyTipAllocation
Required
location_idstring · uuid
Required
location_namestring
Required
period_endstring · date
Required
period_startstring · date
Required
policystring
Required
The location's written tipping policy at the time.
pooledboolean
Required
Whether the period's tips were pooled; when not, each kept what was taken on their own orders.
role_weightsarray of TipRoleWeight
Required
rolestring · enum
Required
ownermanagerstaff
weightinteger · int32
Required
1–100
tipsMoney
Required
A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its
currency.
The amount is always an integer count of the currency's minor unit — pence for
GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal
to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while
{"amount": 500, "currency": "GBP"} is £5.00.
Floating point is never used for money anywhere in this API. Binary floats cannot
represent most decimal fractions exactly, so accumulating them across the line items of
an order produces totals that disagree with the printed receipt by a penny. Integer
minor units make every arithmetic operation exact.
amountinteger · int64
Required
The amount, as a whole number of the currency's minor unit. May be negative to
express a refund, discount or credit.
currencyCurrency
Required
ISO 4217 currency code.
Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.
One of 182 values; see the schema.
weeksarray of MyWeek
Required
This week, then last week.
minutes_workedinteger
Required
≥ 0
shift_entriesarray of MyShiftEntry
Required
The closed shifts of the week, with the times the timesheet pays them.
ended_atstring · date-time
Required
started_atstring · date-time
Required
shiftsinteger
Required
≥ 0
starts_onstring · date
Required
The Monday the week starts on.
400
The request could not be parsed. Malformed JSON, a bad query parameter, or a
header that failed to decode, among them an API-Version later than the server speaks or not
a date (api-version-unknown, with the server's own in api_version).
Distinct from 422: a 400 never reached validation, so invalid_params is
absent. Retrying without changing the request will fail again, except that a request refused
for its API-Version goes once the server is updated.
401
No credentials, or credentials that could not be verified — missing bearer token,
malformed JWT, expired token, or a signature that failed against the issuer's
JWKS.
Refresh the token and retry once. Repeated 401 after a refresh means the device
needs to re-enrol.
403
Authenticated, but not permitted. The token is valid and its merchant was
resolved, but the caller's role does not allow this operation, or the target
belongs to a different merchant.
Cross-tenant access is reported as 403 on a resource the caller may not see,
and as 404 where even confirming existence would leak information. Retrying
will not help.
410
The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in
it was read. api_version names the version the server speaks: the client is behind, and sends
the request again once it is updated.
429
Rate limit exceeded. Honour Retry-After rather than applying your own backoff.
A fleet of tills coming back online together after an outage is the realistic cause,
which is exactly when uncoordinated client-side retry turns a brief outage into a
sustained one. The limit is per process, and per site for what records trade there —
a request spends its site's budget, and a till's own smaller one inside it — and per
(client, merchant) for everything else; no RateLimit field is sent, so this response
and its Retry-After are the whole of the contract.
500
Something failed on our side. trace_id identifies the request in our tracing —
quote it in a support ticket.
The body never carries internal detail: no SQL, no stack traces, no upstream error
strings. Safe to retry with the same Idempotency-Key.
The shifts planned to start on the days from from to to, each day as its location
counts it, earliest first, with what each will cost at its member's rate. Beside them are
the weeks that go over a location's limits: a member planned past its weekly overtime
threshold, and a week's planned pay past its labour budget. A week runs Monday to Sunday at
the location.
It names people and prices their pay, so only those who may see pay read it, and a person
scoped to locations sees theirs.
Parameters
fromquery · string · date
Required
The first day, inclusive.
toquery · string · date
Required
The last day, inclusive.
location_idquery · string · uuid
Only this location's shifts and limits. Absent reads every location the caller holds.
Responses
200
The rota.
RotaReport · application/json
fromstring · date
Required
over_budgetarray of RotaOverBudget
Required
budgetMoney
Required
A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its
currency.
The amount is always an integer count of the currency's minor unit — pence for
GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal
to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while
{"amount": 500, "currency": "GBP"} is £5.00.
Floating point is never used for money anywhere in this API. Binary floats cannot
represent most decimal fractions exactly, so accumulating them across the line items of
an order produces totals that disagree with the printed receipt by a penny. Integer
minor units make every arithmetic operation exact.
amountinteger · int64
Required
The amount, as a whole number of the currency's minor unit. May be negative to
express a refund, discount or credit.
currencyCurrency
Required
ISO 4217 currency code.
Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.
One of 182 values; see the schema.
location_idstring · uuid
Required
plannedMoney
Required
A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its
currency.
The amount is always an integer count of the currency's minor unit — pence for
GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal
to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while
{"amount": 500, "currency": "GBP"} is £5.00.
Floating point is never used for money anywhere in this API. Binary floats cannot
represent most decimal fractions exactly, so accumulating them across the line items of
an order produces totals that disagree with the printed receipt by a penny. Integer
minor units make every arithmetic operation exact.
amountinteger · int64
Required
The amount, as a whole number of the currency's minor unit. May be negative to
express a refund, discount or credit.
currencyCurrency
Required
ISO 4217 currency code.
Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.
One of 182 values; see the schema.
week_ofstring · date
Required
The Monday the week starts on, at the location.
overtimearray of RotaOvertime
Required
location_idstring · uuid
Required
member_idstring · uuid
Required
member_namestring
Required
planned_minutesinteger
Required
≥ 0
threshold_hoursinteger
Required
≥ 1
week_ofstring · date
Required
The Monday the week starts on, at the location.
shiftsarray of RotaShift
Required
idstring · uuid
Required
costMoney
RequiredNullable
The member's hourly rate in force on the shift's day at its location, over the planned
hours, or null when no rate is set. Breaks are not planned, so this is what the shift
costs at most.
amountinteger · int64
Required
The amount, as a whole number of the currency's minor unit. May be negative to
express a refund, discount or credit.
currencyCurrency
Required
ISO 4217 currency code.
Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.
One of 182 values; see the schema.
ends_atstring · date-time
Required
location_idstring · uuid
Required
member_idstring · uuid
Required
The member's user id.
member_namestring
Required
notestring
Required
starts_atstring · date-time
Required
versionVersion
RequiredRead only
Optimistic concurrency version, raised by the server on every write. A version is never lower
than one already returned for the resource, though it may rise by more than one.
Returned in the ETag response header as well as in the body. Send it back in
If-Match on an update and the write is rejected with 412 if anyone else has written
since — which is what stops two tills silently overwriting each other's edit to the same
product when both come back online at once.
≥ 1
tostring · date
Required
400
The request could not be parsed. Malformed JSON, a bad query parameter, or a
header that failed to decode, among them an API-Version later than the server speaks or not
a date (api-version-unknown, with the server's own in api_version).
Distinct from 422: a 400 never reached validation, so invalid_params is
absent. Retrying without changing the request will fail again, except that a request refused
for its API-Version goes once the server is updated.
401
No credentials, or credentials that could not be verified — missing bearer token,
malformed JWT, expired token, or a signature that failed against the issuer's
JWKS.
Refresh the token and retry once. Repeated 401 after a refresh means the device
needs to re-enrol.
403
Authenticated, but not permitted. The token is valid and its merchant was
resolved, but the caller's role does not allow this operation, or the target
belongs to a different merchant.
Cross-tenant access is reported as 403 on a resource the caller may not see,
and as 404 where even confirming existence would leak information. Retrying
will not help.
410
The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in
it was read. api_version names the version the server speaks: the client is behind, and sends
the request again once it is updated.
422
The request parsed but failed validation.
invalid_params lists every failing field with a JSON Pointer, not just the
first, so a client can mark up a whole form in one pass rather than making the
user fix errors one round trip at a time.
Also returned when an Idempotency-Key is replayed with a different body, and when
device-computed order totals disagree with the server's recomputation.
429
Rate limit exceeded. Honour Retry-After rather than applying your own backoff.
A fleet of tills coming back online together after an outage is the realistic cause,
which is exactly when uncoordinated client-side retry turns a brief outage into a
sustained one. The limit is per process, and per site for what records trade there —
a request spends its site's budget, and a till's own smaller one inside it — and per
(client, merchant) for everything else; no RateLimit field is sent, so this response
and its Retry-After are the whole of the contract.
500
Something failed on our side. trace_id identifies the request in our tracing —
quote it in a support ticket.
The body never carries internal detail: no SQL, no stack traces, no upstream error
strings. Safe to retry with the same Idempotency-Key.
Puts shifts on the rota at client-minted ids. A planned shift is never edited: cancel it
and plan another. Each item carries its own idempotency_key and stands or falls on its
own. Only those who may change pay plan the rota, at the locations they may act in.
Request body application/json · ScheduledShiftBatchCreateRequest
itemsarray of ScheduledShiftBatchCreateItem
Required
≥ 1 items · ≤ 500 items
idempotency_keyItemIdempotencyKey
Required
A key for one item in a batch, minted by the client when it queues the item and reused
on every attempt to send it. The item, not the request, is the unit of work: a retried
batch returns the stored result for every item already applied under its key. A fresh
key per attempt defeats the whole mechanism.
Scoped per merchant, so keys need only be unique within a merchant's own traffic.
16–255 chars
scheduled_shiftScheduledShift
Required
A shift on the rota: who is planned to work, when, and where. The plan, not the fact: a till
records the shift worked, and comparing the two is how a manager sees who did not show.
idstring · uuid
Required
ends_atstring · date-time
Required
After starts_at.
location_idstring · uuid
Required
member_idstring · uuid
Required
The member's user id.
notestring
Required
≤ 500 chars
starts_atstring · date-time
Required
Responses
200
The batch was processed. results says what happened to each item.
BatchResponse · application/json
resultsarray of BatchResult
Required
idstring · uuid
The resource the item created or changed. Absent when it failed.
idempotency_keystring
Required
The item's key, echoed so a result can be matched by key as well as position.
problemProblem
Nullable
Why the item failed, as an RFC 9457 problem. A 422 means the item is malformed and
will never succeed — drop it and show it to staff rather than retrying for ever. A
409 or a 5xx means try again later.
typestring · uri
Required
A stable URI identifying the problem kind. This is the field to branch on.
default about:blank
api_versionstring
Present on an api-version-retired or api-version-unknown refusal: the API-Version
this server speaks (docs/decisions/0011).
detailstring
A human-readable explanation specific to this occurrence. Safe to show to a
developer; never contains internal state, SQL, or stack traces.
instancestring · uri-reference
A URI reference identifying the specific occurrence.
invalid_paramsarray of InvalidParam
Present on 422 responses. Lists every field that failed validation, rather than
only the first, so a client can highlight an entire form in one pass.
codestring
Stable machine-readable code for this failure, for clients that localise their own
messages rather than showing reason.
pointerstring
Required
RFC 6901 JSON Pointer to the offending
value within the request body. A pointer rather than a field name so that failures
nested inside arrays — the third line item of an order — are addressable at all.
reasonstring
Required
Why this value was rejected, in human-readable form.
refusalsarray of Location refusal
Present on 409 responses to a withdrawal refused by the state it depends on
(deleting a location with tills still assigned, open orders, …). Lists every reason
at once, each with how much stands in the way, so a client can say what to put right
and link to it rather than walking a person through one refusal after another.
kindstring
Required
A stable refusal kind — devices, open_orders, drawer_sessions, bookings or
stock — that a client links to the screen that puts it right. Branch on this field,
never on the surrounding prose.
countinteger
Required
How many things stand in the way of the withdrawal, so a client can repeat the number rather than re-read prose.
≥ 1
statusinteger
Required
The HTTP status code, repeated here so the body is self-contained.
100–599
titlestring
Required
A short, human-readable summary of the problem kind.
trace_idstring
The OpenTelemetry trace id for this request. Quote it in a support ticket and the
whole request can be pulled up.
statusstring · enum
Required
created — the item made a new resource. updated — it changed an existing one.
unchanged — this key was already applied and the original outcome stands. A
success, not an error: it is what a client retrying a batch it was unsure landed
should expect, and it clears the item from its queue exactly as created does.
failed — the item was refused, and nothing else in the batch was affected. See
problem.
createdupdatedunchangedfailed
versioninteger · int64
The resource's version after the item. Absent when it failed.
400
The request could not be parsed. Malformed JSON, a bad query parameter, or a
header that failed to decode, among them an API-Version later than the server speaks or not
a date (api-version-unknown, with the server's own in api_version).
Distinct from 422: a 400 never reached validation, so invalid_params is
absent. Retrying without changing the request will fail again, except that a request refused
for its API-Version goes once the server is updated.
401
No credentials, or credentials that could not be verified — missing bearer token,
malformed JWT, expired token, or a signature that failed against the issuer's
JWKS.
Refresh the token and retry once. Repeated 401 after a refresh means the device
needs to re-enrol.
403
Authenticated, but not permitted. The token is valid and its merchant was
resolved, but the caller's role does not allow this operation, or the target
belongs to a different merchant.
Cross-tenant access is reported as 403 on a resource the caller may not see,
and as 404 where even confirming existence would leak information. Retrying
will not help.
410
The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in
it was read. api_version names the version the server speaks: the client is behind, and sends
the request again once it is updated.
415
The request Content-Type is not supported. This API accepts
application/json on all mutations.
422
The request parsed but failed validation.
invalid_params lists every failing field with a JSON Pointer, not just the
first, so a client can mark up a whole form in one pass rather than making the
user fix errors one round trip at a time.
Also returned when an Idempotency-Key is replayed with a different body, and when
device-computed order totals disagree with the server's recomputation.
429
Rate limit exceeded. Honour Retry-After rather than applying your own backoff.
A fleet of tills coming back online together after an outage is the realistic cause,
which is exactly when uncoordinated client-side retry turns a brief outage into a
sustained one. The limit is per process, and per site for what records trade there —
a request spends its site's budget, and a till's own smaller one inside it — and per
(client, merchant) for everything else; no RateLimit field is sent, so this response
and its Retry-After are the whole of the contract.
500
Something failed on our side. trace_id identifies the request in our tracing —
quote it in a support ticket.
The body never carries internal detail: no SQL, no stack traces, no upstream error
strings. Safe to retry with the same Idempotency-Key.
503
Temporarily unable to serve the request — a dependency is down or the service is
shedding load.
Retry with the same Idempotency-Key after Retry-After. This is always safe: a
request that did take effect before the failure will be recognised as a replay.
Takes a shift off the rota from the version read. It is kept as canceled, not erased.
Parameters
idpath · string · uuid
Required
UUIDv7 of the resource.
For resources a device can originate — orders, and their line items — this id is minted
by the client before the record is ever synced, which is what makes an offline create
idempotent.
If-Matchheader · string
Required
The ETag from the read that produced the value you are updating.
Required on updates, not optional, because "last write wins" is the wrong default for a
catalogue two managers might edit at once. If anything has been written since your read,
the request is rejected with 412 and you re-read and re-apply.
Use * to mean "write regardless of current version" — an explicit, greppable override
rather than the silent default.
Idempotency-Keyheader · string
Required
A client-generated key making this request safe to retry.
Required on every mutation that creates a resource or moves money. A till on bad wifi
cannot tell a lost response from a lost request; without a key its only options are to
risk a duplicate sale or to drop a real one. With a key it retries freely: the first
request to arrive is processed, and every replay returns that same stored response.
Rules:
Mint the key once, when the operation is first queued, and reuse it for every
retry. A fresh key per attempt defeats the mechanism entirely.
Keys are scoped per merchant and per operation, and are retained for 30 days — long
enough that a till switched off mid-sync can retry with the same key however long it
stayed on the shelf.
Replaying a key with a different request body is a client bug and is rejected
with 422, rather than silently returning the first result — quietly serving the old
response would hide a real inconsistency in the caller.
A UUIDv7 or ULID is a good choice.
Responses
204
The shift is off the rota.
400
The request could not be parsed. Malformed JSON, a bad query parameter, or a
header that failed to decode, among them an API-Version later than the server speaks or not
a date (api-version-unknown, with the server's own in api_version).
Distinct from 422: a 400 never reached validation, so invalid_params is
absent. Retrying without changing the request will fail again, except that a request refused
for its API-Version goes once the server is updated.
401
No credentials, or credentials that could not be verified — missing bearer token,
malformed JWT, expired token, or a signature that failed against the issuer's
JWKS.
Refresh the token and retry once. Repeated 401 after a refresh means the device
needs to re-enrol.
403
Authenticated, but not permitted. The token is valid and its merchant was
resolved, but the caller's role does not allow this operation, or the target
belongs to a different merchant.
Cross-tenant access is reported as 403 on a resource the caller may not see,
and as 404 where even confirming existence would leak information. Retrying
will not help.
404
No such resource, or none visible to this caller.
Also returned instead of 403 where acknowledging existence would itself leak
information — so a 404 does not prove the id is unused globally, only that it is
not yours.
410
The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in
it was read. api_version names the version the server speaks: the client is behind, and sends
the request again once it is updated.
412
The If-Match version did not match the resource's current version: someone else
wrote since you read.
This is the expected, healthy outcome of two clients editing the same record — not
an error condition to suppress. Re-read the resource, re-apply your change on top
of the new state, and retry with the fresh ETag.
429
Rate limit exceeded. Honour Retry-After rather than applying your own backoff.
A fleet of tills coming back online together after an outage is the realistic cause,
which is exactly when uncoordinated client-side retry turns a brief outage into a
sustained one. The limit is per process, and per site for what records trade there —
a request spends its site's budget, and a till's own smaller one inside it — and per
(client, merchant) for everything else; no RateLimit field is sent, so this response
and its Retry-After are the whole of the contract.
500
Something failed on our side. trace_id identifies the request in our tracing —
quote it in a support ticket.
The body never carries internal detail: no SQL, no stack traces, no upstream error
strings. Safe to retry with the same Idempotency-Key.
Records a manager's corrections to shifts' times. One item per shift, judged on its own, so a
shift inside an approved period fails only its own item.
A shift's times are the till's: device-authoritative, written offline if they had to be, and
never edited in place. A correction is its own append-only record carrying the corrected times,
the reason and the manager who made it, and the reports that pay from these hours apply the
latest one over the shift. Editing the shift row instead would leave the till's account and the
report identical and nothing to say which had changed.
A correction to a shift inside an approved period is refused with 409. Un-approve the period
first (POST /v1/timesheet-approvals/batch-withdraw), which is deliberately a separate,
explicit act: an approved period has allocated its tips and may have been paid.
Changing what somebody is paid needs the labour write scope.
Parameters
Idempotency-Keyheader · string
Required
A client-generated key making this request safe to retry.
Required on every mutation that creates a resource or moves money. A till on bad wifi
cannot tell a lost response from a lost request; without a key its only options are to
risk a duplicate sale or to drop a real one. With a key it retries freely: the first
request to arrive is processed, and every replay returns that same stored response.
Rules:
Mint the key once, when the operation is first queued, and reuse it for every
retry. A fresh key per attempt defeats the mechanism entirely.
Keys are scoped per merchant and per operation, and are retained for 30 days — long
enough that a till switched off mid-sync can retry with the same key however long it
stayed on the shelf.
Replaying a key with a different request body is a client bug and is rejected
with 422, rather than silently returning the first result — quietly serving the old
response would hide a real inconsistency in the caller.
A UUIDv7 or ULID is a good choice.
Request body application/json · ShiftCorrectionBatchCreateRequest
itemsarray of ShiftCorrectionBatchCreateItem
Required
≥ 1 items · ≤ 500 items
clock_in_atstring · date-time
Required
The time the manager says the member started.
clock_out_atstring · date-time
Nullable
The time the manager says they stopped, or null to leave the shift open. Before
clock_in_at is refused.
idempotency_keyItemIdempotencyKey
Required
A key for one item in a batch, minted by the client when it queues the item and reused
on every attempt to send it. The item, not the request, is the unit of work: a retried
batch returns the stored result for every item already applied under its key. A fresh
key per attempt defeats the whole mechanism.
Scoped per merchant, so keys need only be unique within a merchant's own traffic.
16–255 chars
reasonstring
Required
Why the times were changed, and required. A correction moves what somebody is paid, so an
unexplained one is indistinguishable from an error — and the worker whose hours changed is
entitled to be told why.
1–500 chars
shift_idstring · uuid
Required
The shift being corrected. A shift of another merchant is refused.
Responses
200
The batch was processed. results says what happened to each item.
BatchResponse · application/json
resultsarray of BatchResult
Required
idstring · uuid
The resource the item created or changed. Absent when it failed.
idempotency_keystring
Required
The item's key, echoed so a result can be matched by key as well as position.
problemProblem
Nullable
Why the item failed, as an RFC 9457 problem. A 422 means the item is malformed and
will never succeed — drop it and show it to staff rather than retrying for ever. A
409 or a 5xx means try again later.
typestring · uri
Required
A stable URI identifying the problem kind. This is the field to branch on.
default about:blank
api_versionstring
Present on an api-version-retired or api-version-unknown refusal: the API-Version
this server speaks (docs/decisions/0011).
detailstring
A human-readable explanation specific to this occurrence. Safe to show to a
developer; never contains internal state, SQL, or stack traces.
instancestring · uri-reference
A URI reference identifying the specific occurrence.
invalid_paramsarray of InvalidParam
Present on 422 responses. Lists every field that failed validation, rather than
only the first, so a client can highlight an entire form in one pass.
codestring
Stable machine-readable code for this failure, for clients that localise their own
messages rather than showing reason.
pointerstring
Required
RFC 6901 JSON Pointer to the offending
value within the request body. A pointer rather than a field name so that failures
nested inside arrays — the third line item of an order — are addressable at all.
reasonstring
Required
Why this value was rejected, in human-readable form.
refusalsarray of Location refusal
Present on 409 responses to a withdrawal refused by the state it depends on
(deleting a location with tills still assigned, open orders, …). Lists every reason
at once, each with how much stands in the way, so a client can say what to put right
and link to it rather than walking a person through one refusal after another.
kindstring
Required
A stable refusal kind — devices, open_orders, drawer_sessions, bookings or
stock — that a client links to the screen that puts it right. Branch on this field,
never on the surrounding prose.
countinteger
Required
How many things stand in the way of the withdrawal, so a client can repeat the number rather than re-read prose.
≥ 1
statusinteger
Required
The HTTP status code, repeated here so the body is self-contained.
100–599
titlestring
Required
A short, human-readable summary of the problem kind.
trace_idstring
The OpenTelemetry trace id for this request. Quote it in a support ticket and the
whole request can be pulled up.
statusstring · enum
Required
created — the item made a new resource. updated — it changed an existing one.
unchanged — this key was already applied and the original outcome stands. A
success, not an error: it is what a client retrying a batch it was unsure landed
should expect, and it clears the item from its queue exactly as created does.
failed — the item was refused, and nothing else in the batch was affected. See
problem.
createdupdatedunchangedfailed
versioninteger · int64
The resource's version after the item. Absent when it failed.
400
The request could not be parsed. Malformed JSON, a bad query parameter, or a
header that failed to decode, among them an API-Version later than the server speaks or not
a date (api-version-unknown, with the server's own in api_version).
Distinct from 422: a 400 never reached validation, so invalid_params is
absent. Retrying without changing the request will fail again, except that a request refused
for its API-Version goes once the server is updated.
401
No credentials, or credentials that could not be verified — missing bearer token,
malformed JWT, expired token, or a signature that failed against the issuer's
JWKS.
Refresh the token and retry once. Repeated 401 after a refresh means the device
needs to re-enrol.
403
Authenticated, but not permitted. The token is valid and its merchant was
resolved, but the caller's role does not allow this operation, or the target
belongs to a different merchant.
Cross-tenant access is reported as 403 on a resource the caller may not see,
and as 404 where even confirming existence would leak information. Retrying
will not help.
410
The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in
it was read. api_version names the version the server speaks: the client is behind, and sends
the request again once it is updated.
415
The request Content-Type is not supported. This API accepts
application/json on all mutations.
422
The request parsed but failed validation.
invalid_params lists every failing field with a JSON Pointer, not just the
first, so a client can mark up a whole form in one pass rather than making the
user fix errors one round trip at a time.
Also returned when an Idempotency-Key is replayed with a different body, and when
device-computed order totals disagree with the server's recomputation.
429
Rate limit exceeded. Honour Retry-After rather than applying your own backoff.
A fleet of tills coming back online together after an outage is the realistic cause,
which is exactly when uncoordinated client-side retry turns a brief outage into a
sustained one. The limit is per process, and per site for what records trade there —
a request spends its site's budget, and a till's own smaller one inside it — and per
(client, merchant) for everything else; no RateLimit field is sent, so this response
and its Retry-After are the whole of the contract.
500
Something failed on our side. trace_id identifies the request in our tracing —
quote it in a support ticket.
The body never carries internal detail: no SQL, no stack traces, no upstream error
strings. Safe to retry with the same Idempotency-Key.
503
Temporarily unable to serve the request — a dependency is down or the service is
shedding load.
Retry with the same Idempotency-Key after Retry-After. This is always safe: a
request that did take effect before the failure will be recognised as a replay.
Records a manager's approval of each member's period. One item per member, so a fortnight for a
whole roster is one call and a member whose hours are still wrong fails only their own item.
Approving does three things, and the third is the one to know about: it freezes the period
against further corrections, it is what the payroll export counts, and it writes the tip
allocation the Employment (Allocation of Tips) Act 2023 requires — each member's share as the
pooling rule stood at that moment, kept unchanged whatever the rule is changed to later. Nothing
is allocated until a period is approved, so an unapproved period reads as no tips rather than as
an estimate.
batch-create because an approval is never edited. Putting a period back is
POST /v1/timesheet-approvals/batch-withdraw, which discards the allocation with it.
Needs the labour write scope: approving is a decision about pay.
Parameters
Idempotency-Keyheader · string
Required
A client-generated key making this request safe to retry.
Required on every mutation that creates a resource or moves money. A till on bad wifi
cannot tell a lost response from a lost request; without a key its only options are to
risk a duplicate sale or to drop a real one. With a key it retries freely: the first
request to arrive is processed, and every replay returns that same stored response.
Rules:
Mint the key once, when the operation is first queued, and reuse it for every
retry. A fresh key per attempt defeats the mechanism entirely.
Keys are scoped per merchant and per operation, and are retained for 30 days — long
enough that a till switched off mid-sync can retry with the same key however long it
stayed on the shelf.
Replaying a key with a different request body is a client bug and is rejected
with 422, rather than silently returning the first result — quietly serving the old
response would hide a real inconsistency in the caller.
A UUIDv7 or ULID is a good choice.
Request body application/json · TimesheetApprovalBatchCreateRequest
itemsarray of TimesheetApprovalBatchCreateItem
Required
≥ 1 items · ≤ 500 items
idempotency_keyItemIdempotencyKey
Required
A key for one item in a batch, minted by the client when it queues the item and reused
on every attempt to send it. The item, not the request, is the unit of work: a retried
batch returns the stored result for every item already applied under its key. A fresh
key per attempt defeats the whole mechanism.
Scoped per merchant, so keys need only be unique within a merchant's own traffic.
16–255 chars
member_idstring · uuid
Required
The member whose hours are being approved. Somebody off this roster is refused.
period_endstring · date
Required
The last business date, inclusive. Before period_start is refused.
period_startstring · date
Required
The first business date of the period, inclusive.
Responses
200
The batch was processed. results says what happened to each item.
BatchResponse · application/json
resultsarray of BatchResult
Required
idstring · uuid
The resource the item created or changed. Absent when it failed.
idempotency_keystring
Required
The item's key, echoed so a result can be matched by key as well as position.
problemProblem
Nullable
Why the item failed, as an RFC 9457 problem. A 422 means the item is malformed and
will never succeed — drop it and show it to staff rather than retrying for ever. A
409 or a 5xx means try again later.
typestring · uri
Required
A stable URI identifying the problem kind. This is the field to branch on.
default about:blank
api_versionstring
Present on an api-version-retired or api-version-unknown refusal: the API-Version
this server speaks (docs/decisions/0011).
detailstring
A human-readable explanation specific to this occurrence. Safe to show to a
developer; never contains internal state, SQL, or stack traces.
instancestring · uri-reference
A URI reference identifying the specific occurrence.
invalid_paramsarray of InvalidParam
Present on 422 responses. Lists every field that failed validation, rather than
only the first, so a client can highlight an entire form in one pass.
codestring
Stable machine-readable code for this failure, for clients that localise their own
messages rather than showing reason.
pointerstring
Required
RFC 6901 JSON Pointer to the offending
value within the request body. A pointer rather than a field name so that failures
nested inside arrays — the third line item of an order — are addressable at all.
reasonstring
Required
Why this value was rejected, in human-readable form.
refusalsarray of Location refusal
Present on 409 responses to a withdrawal refused by the state it depends on
(deleting a location with tills still assigned, open orders, …). Lists every reason
at once, each with how much stands in the way, so a client can say what to put right
and link to it rather than walking a person through one refusal after another.
kindstring
Required
A stable refusal kind — devices, open_orders, drawer_sessions, bookings or
stock — that a client links to the screen that puts it right. Branch on this field,
never on the surrounding prose.
countinteger
Required
How many things stand in the way of the withdrawal, so a client can repeat the number rather than re-read prose.
≥ 1
statusinteger
Required
The HTTP status code, repeated here so the body is self-contained.
100–599
titlestring
Required
A short, human-readable summary of the problem kind.
trace_idstring
The OpenTelemetry trace id for this request. Quote it in a support ticket and the
whole request can be pulled up.
statusstring · enum
Required
created — the item made a new resource. updated — it changed an existing one.
unchanged — this key was already applied and the original outcome stands. A
success, not an error: it is what a client retrying a batch it was unsure landed
should expect, and it clears the item from its queue exactly as created does.
failed — the item was refused, and nothing else in the batch was affected. See
problem.
createdupdatedunchangedfailed
versioninteger · int64
The resource's version after the item. Absent when it failed.
400
The request could not be parsed. Malformed JSON, a bad query parameter, or a
header that failed to decode, among them an API-Version later than the server speaks or not
a date (api-version-unknown, with the server's own in api_version).
Distinct from 422: a 400 never reached validation, so invalid_params is
absent. Retrying without changing the request will fail again, except that a request refused
for its API-Version goes once the server is updated.
401
No credentials, or credentials that could not be verified — missing bearer token,
malformed JWT, expired token, or a signature that failed against the issuer's
JWKS.
Refresh the token and retry once. Repeated 401 after a refresh means the device
needs to re-enrol.
403
Authenticated, but not permitted. The token is valid and its merchant was
resolved, but the caller's role does not allow this operation, or the target
belongs to a different merchant.
Cross-tenant access is reported as 403 on a resource the caller may not see,
and as 404 where even confirming existence would leak information. Retrying
will not help.
410
The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in
it was read. api_version names the version the server speaks: the client is behind, and sends
the request again once it is updated.
415
The request Content-Type is not supported. This API accepts
application/json on all mutations.
422
The request parsed but failed validation.
invalid_params lists every failing field with a JSON Pointer, not just the
first, so a client can mark up a whole form in one pass rather than making the
user fix errors one round trip at a time.
Also returned when an Idempotency-Key is replayed with a different body, and when
device-computed order totals disagree with the server's recomputation.
429
Rate limit exceeded. Honour Retry-After rather than applying your own backoff.
A fleet of tills coming back online together after an outage is the realistic cause,
which is exactly when uncoordinated client-side retry turns a brief outage into a
sustained one. The limit is per process, and per site for what records trade there —
a request spends its site's budget, and a till's own smaller one inside it — and per
(client, merchant) for everything else; no RateLimit field is sent, so this response
and its Retry-After are the whole of the contract.
500
Something failed on our side. trace_id identifies the request in our tracing —
quote it in a support ticket.
The body never carries internal detail: no SQL, no stack traces, no upstream error
strings. Safe to retry with the same Idempotency-Key.
503
Temporarily unable to serve the request — a dependency is down or the service is
shedding load.
Retry with the same Idempotency-Key after Retry-After. This is always safe: a
request that did take effect before the failure will be recognised as a replay.
Puts each named period back to its pre-approval state: the approval is removed and the tip
allocations it wrote are discarded, so the tips screen computes the period again from the rule
in force and a correction to a shift inside it is allowed once more.
withdraw rather than a DELETE on an approval because it is that compound act, not the
removal of a row — and it is addressed by member and period rather than by an approval's id,
because the period is the thing being put back. A period nobody approved comes back unchanged:
un-approving twice is the same request sent twice.
It is destructive in the way that matters, and stated here rather than left to be discovered: the
Act's record for that period is gone, so anyone who has already been paid from it has been paid
from something no longer readable. Approve again once the hours are right, and the allocation is
written afresh under whatever rule is in force then — which may not be the rule the first
allocation used.
Needs the labour write scope.
Parameters
Idempotency-Keyheader · string
Required
A client-generated key making this request safe to retry.
Required on every mutation that creates a resource or moves money. A till on bad wifi
cannot tell a lost response from a lost request; without a key its only options are to
risk a duplicate sale or to drop a real one. With a key it retries freely: the first
request to arrive is processed, and every replay returns that same stored response.
Rules:
Mint the key once, when the operation is first queued, and reuse it for every
retry. A fresh key per attempt defeats the mechanism entirely.
Keys are scoped per merchant and per operation, and are retained for 30 days — long
enough that a till switched off mid-sync can retry with the same key however long it
stayed on the shelf.
Replaying a key with a different request body is a client bug and is rejected
with 422, rather than silently returning the first result — quietly serving the old
response would hide a real inconsistency in the caller.
A UUIDv7 or ULID is a good choice.
Request body application/json · TimesheetApprovalBatchWithdrawRequest
itemsarray of TimesheetApprovalBatchWithdrawItem
Required
≥ 1 items · ≤ 500 items
idempotency_keyItemIdempotencyKey
Required
A key for one item in a batch, minted by the client when it queues the item and reused
on every attempt to send it. The item, not the request, is the unit of work: a retried
batch returns the stored result for every item already applied under its key. A fresh
key per attempt defeats the whole mechanism.
Scoped per merchant, so keys need only be unique within a merchant's own traffic.
16–255 chars
member_idstring · uuid
Required
The member whose period is being put back.
period_endstring · date
Required
The last business date, inclusive.
period_startstring · date
Required
The first business date of the approved period, inclusive.
Responses
200
The batch was processed. results says what happened to each item.
BatchResponse · application/json
resultsarray of BatchResult
Required
idstring · uuid
The resource the item created or changed. Absent when it failed.
idempotency_keystring
Required
The item's key, echoed so a result can be matched by key as well as position.
problemProblem
Nullable
Why the item failed, as an RFC 9457 problem. A 422 means the item is malformed and
will never succeed — drop it and show it to staff rather than retrying for ever. A
409 or a 5xx means try again later.
typestring · uri
Required
A stable URI identifying the problem kind. This is the field to branch on.
default about:blank
api_versionstring
Present on an api-version-retired or api-version-unknown refusal: the API-Version
this server speaks (docs/decisions/0011).
detailstring
A human-readable explanation specific to this occurrence. Safe to show to a
developer; never contains internal state, SQL, or stack traces.
instancestring · uri-reference
A URI reference identifying the specific occurrence.
invalid_paramsarray of InvalidParam
Present on 422 responses. Lists every field that failed validation, rather than
only the first, so a client can highlight an entire form in one pass.
codestring
Stable machine-readable code for this failure, for clients that localise their own
messages rather than showing reason.
pointerstring
Required
RFC 6901 JSON Pointer to the offending
value within the request body. A pointer rather than a field name so that failures
nested inside arrays — the third line item of an order — are addressable at all.
reasonstring
Required
Why this value was rejected, in human-readable form.
refusalsarray of Location refusal
Present on 409 responses to a withdrawal refused by the state it depends on
(deleting a location with tills still assigned, open orders, …). Lists every reason
at once, each with how much stands in the way, so a client can say what to put right
and link to it rather than walking a person through one refusal after another.
kindstring
Required
A stable refusal kind — devices, open_orders, drawer_sessions, bookings or
stock — that a client links to the screen that puts it right. Branch on this field,
never on the surrounding prose.
countinteger
Required
How many things stand in the way of the withdrawal, so a client can repeat the number rather than re-read prose.
≥ 1
statusinteger
Required
The HTTP status code, repeated here so the body is self-contained.
100–599
titlestring
Required
A short, human-readable summary of the problem kind.
trace_idstring
The OpenTelemetry trace id for this request. Quote it in a support ticket and the
whole request can be pulled up.
statusstring · enum
Required
created — the item made a new resource. updated — it changed an existing one.
unchanged — this key was already applied and the original outcome stands. A
success, not an error: it is what a client retrying a batch it was unsure landed
should expect, and it clears the item from its queue exactly as created does.
failed — the item was refused, and nothing else in the batch was affected. See
problem.
createdupdatedunchangedfailed
versioninteger · int64
The resource's version after the item. Absent when it failed.
400
The request could not be parsed. Malformed JSON, a bad query parameter, or a
header that failed to decode, among them an API-Version later than the server speaks or not
a date (api-version-unknown, with the server's own in api_version).
Distinct from 422: a 400 never reached validation, so invalid_params is
absent. Retrying without changing the request will fail again, except that a request refused
for its API-Version goes once the server is updated.
401
No credentials, or credentials that could not be verified — missing bearer token,
malformed JWT, expired token, or a signature that failed against the issuer's
JWKS.
Refresh the token and retry once. Repeated 401 after a refresh means the device
needs to re-enrol.
403
Authenticated, but not permitted. The token is valid and its merchant was
resolved, but the caller's role does not allow this operation, or the target
belongs to a different merchant.
Cross-tenant access is reported as 403 on a resource the caller may not see,
and as 404 where even confirming existence would leak information. Retrying
will not help.
410
The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in
it was read. api_version names the version the server speaks: the client is behind, and sends
the request again once it is updated.
415
The request Content-Type is not supported. This API accepts
application/json on all mutations.
422
The request parsed but failed validation.
invalid_params lists every failing field with a JSON Pointer, not just the
first, so a client can mark up a whole form in one pass rather than making the
user fix errors one round trip at a time.
Also returned when an Idempotency-Key is replayed with a different body, and when
device-computed order totals disagree with the server's recomputation.
429
Rate limit exceeded. Honour Retry-After rather than applying your own backoff.
A fleet of tills coming back online together after an outage is the realistic cause,
which is exactly when uncoordinated client-side retry turns a brief outage into a
sustained one. The limit is per process, and per site for what records trade there —
a request spends its site's budget, and a till's own smaller one inside it — and per
(client, merchant) for everything else; no RateLimit field is sent, so this response
and its Retry-After are the whole of the contract.
500
Something failed on our side. trace_id identifies the request in our tracing —
quote it in a support ticket.
The body never carries internal detail: no SQL, no stack traces, no upstream error
strings. Safe to retry with the same Idempotency-Key.
503
Temporarily unable to serve the request — a dependency is down or the service is
shedding load.
Retry with the same Idempotency-Key after Retry-After. This is always safe: a
request that did take effect before the failure will be recognised as a replay.
Sets whether each named location pools its tips, how it weights each role's hours, and its
written tipping policy. One item per location, judged on its own: a location another merchant
holds fails in its own result and every other item still applies.
A whole rule is sent rather than a patch. There is one row per location and it is replaced, so
an item with enabled and no role_weights sets the weights back to even — which is what a
caller reading the rule, changing a field and sending it back will do correctly, and what a
caller assembling an item from nothing has to mean deliberately.
Changing a rule is not retroactive. Approving a period writes each member's share as the
rule stood then, and that record is what the Employment (Allocation of Tips) Act 2023 requires;
a rule set today changes what the next approval allocates and nothing that has already been
allocated.
This needs the labour write scope. Reading a policy and changing who gets paid what are
different acts, and a credential trusted to see a payroll figure is not thereby trusted to
move it.
Parameters
Idempotency-Keyheader · string
Required
A client-generated key making this request safe to retry.
Required on every mutation that creates a resource or moves money. A till on bad wifi
cannot tell a lost response from a lost request; without a key its only options are to
risk a duplicate sale or to drop a real one. With a key it retries freely: the first
request to arrive is processed, and every replay returns that same stored response.
Rules:
Mint the key once, when the operation is first queued, and reuse it for every
retry. A fresh key per attempt defeats the mechanism entirely.
Keys are scoped per merchant and per operation, and are retained for 30 days — long
enough that a till switched off mid-sync can retry with the same key however long it
stayed on the shelf.
Replaying a key with a different request body is a client bug and is rejected
with 422, rather than silently returning the first result — quietly serving the old
response would hide a real inconsistency in the caller.
A UUIDv7 or ULID is a good choice.
Request body application/json · TipPoolRuleBatchCreateRequest
itemsarray of TipPoolRuleBatchCreateItem
Required
≥ 1 items · ≤ 100 items
enabledboolean
Required
Whether this location pools its tips and divides them by hours worked.
idempotency_keyItemIdempotencyKey
Required
A key for one item in a batch, minted by the client when it queues the item and reused
on every attempt to send it. The item, not the request, is the unit of work: a retried
batch returns the stored result for every item already applied under its key. A fresh
key per attempt defeats the whole mechanism.
Scoped per merchant, so keys need only be unique within a merchant's own traffic.
16–255 chars
location_idstring · uuid
Required
The location whose rule this is. A location of another merchant is refused.
policystring
The written tipping policy for this location. Omitted leaves the location with none,
which the Act permits only where tips are occasional.
≤ 4000 chars
role_weightsarray of TipRoleWeight
How each role's hours are weighted. Omitted or empty is an even split by hours, which is
the default a location starts on.
≤ 8 items
rolestring · enum
Required
ownermanagerstaff
weightinteger · int32
Required
1–100
Responses
200
The batch was processed. results says what happened to each item.
BatchResponse · application/json
resultsarray of BatchResult
Required
idstring · uuid
The resource the item created or changed. Absent when it failed.
idempotency_keystring
Required
The item's key, echoed so a result can be matched by key as well as position.
problemProblem
Nullable
Why the item failed, as an RFC 9457 problem. A 422 means the item is malformed and
will never succeed — drop it and show it to staff rather than retrying for ever. A
409 or a 5xx means try again later.
typestring · uri
Required
A stable URI identifying the problem kind. This is the field to branch on.
default about:blank
api_versionstring
Present on an api-version-retired or api-version-unknown refusal: the API-Version
this server speaks (docs/decisions/0011).
detailstring
A human-readable explanation specific to this occurrence. Safe to show to a
developer; never contains internal state, SQL, or stack traces.
instancestring · uri-reference
A URI reference identifying the specific occurrence.
invalid_paramsarray of InvalidParam
Present on 422 responses. Lists every field that failed validation, rather than
only the first, so a client can highlight an entire form in one pass.
codestring
Stable machine-readable code for this failure, for clients that localise their own
messages rather than showing reason.
pointerstring
Required
RFC 6901 JSON Pointer to the offending
value within the request body. A pointer rather than a field name so that failures
nested inside arrays — the third line item of an order — are addressable at all.
reasonstring
Required
Why this value was rejected, in human-readable form.
refusalsarray of Location refusal
Present on 409 responses to a withdrawal refused by the state it depends on
(deleting a location with tills still assigned, open orders, …). Lists every reason
at once, each with how much stands in the way, so a client can say what to put right
and link to it rather than walking a person through one refusal after another.
kindstring
Required
A stable refusal kind — devices, open_orders, drawer_sessions, bookings or
stock — that a client links to the screen that puts it right. Branch on this field,
never on the surrounding prose.
countinteger
Required
How many things stand in the way of the withdrawal, so a client can repeat the number rather than re-read prose.
≥ 1
statusinteger
Required
The HTTP status code, repeated here so the body is self-contained.
100–599
titlestring
Required
A short, human-readable summary of the problem kind.
trace_idstring
The OpenTelemetry trace id for this request. Quote it in a support ticket and the
whole request can be pulled up.
statusstring · enum
Required
created — the item made a new resource. updated — it changed an existing one.
unchanged — this key was already applied and the original outcome stands. A
success, not an error: it is what a client retrying a batch it was unsure landed
should expect, and it clears the item from its queue exactly as created does.
failed — the item was refused, and nothing else in the batch was affected. See
problem.
createdupdatedunchangedfailed
versioninteger · int64
The resource's version after the item. Absent when it failed.
400
The request could not be parsed. Malformed JSON, a bad query parameter, or a
header that failed to decode, among them an API-Version later than the server speaks or not
a date (api-version-unknown, with the server's own in api_version).
Distinct from 422: a 400 never reached validation, so invalid_params is
absent. Retrying without changing the request will fail again, except that a request refused
for its API-Version goes once the server is updated.
401
No credentials, or credentials that could not be verified — missing bearer token,
malformed JWT, expired token, or a signature that failed against the issuer's
JWKS.
Refresh the token and retry once. Repeated 401 after a refresh means the device
needs to re-enrol.
403
Authenticated, but not permitted. The token is valid and its merchant was
resolved, but the caller's role does not allow this operation, or the target
belongs to a different merchant.
Cross-tenant access is reported as 403 on a resource the caller may not see,
and as 404 where even confirming existence would leak information. Retrying
will not help.
410
The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in
it was read. api_version names the version the server speaks: the client is behind, and sends
the request again once it is updated.
415
The request Content-Type is not supported. This API accepts
application/json on all mutations.
422
The request parsed but failed validation.
invalid_params lists every failing field with a JSON Pointer, not just the
first, so a client can mark up a whole form in one pass rather than making the
user fix errors one round trip at a time.
Also returned when an Idempotency-Key is replayed with a different body, and when
device-computed order totals disagree with the server's recomputation.
429
Rate limit exceeded. Honour Retry-After rather than applying your own backoff.
A fleet of tills coming back online together after an outage is the realistic cause,
which is exactly when uncoordinated client-side retry turns a brief outage into a
sustained one. The limit is per process, and per site for what records trade there —
a request spends its site's budget, and a till's own smaller one inside it — and per
(client, merchant) for everything else; no RateLimit field is sent, so this response
and its Retry-After are the whole of the contract.
500
Something failed on our side. trace_id identifies the request in our tracing —
quote it in a support ticket.
The body never carries internal detail: no SQL, no stack traces, no upstream error
strings. Safe to retry with the same Idempotency-Key.
503
Temporarily unable to serve the request — a dependency is down or the service is
shedding load.
Retry with the same Idempotency-Key after Retry-After. This is always safe: a
request that did take effect before the failure will be recognised as a replay.