Answers the page a card's holder checks its balance on. The link carries a token only its
hash is kept of, so making one replaces any link before it, the one a digital card was
emailed with among them.
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 · GiftCardBalanceLinkCreate
gift_card_idstring · uuid
Required
Responses
201
The link.
GiftCardBalanceLink · application/json
gift_card_idstring · uuid
Required
urlstring · uri
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.
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.
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.
Returns the merchant's gift cards, newest first. This is the only way to enumerate them:
until it existed a caller could fetch a card whose id they already held, or look one up by
its code, and had no way to find out what cards a merchant had issued at all.
A card's balance is what is still spendable, and it is the sum of the card's movements
(GET …/{id}/ledger) rather than a figure stored beside them.
Parameters
cursorquery · string
Opaque cursor from a previous page's next_cursor. Omit to start from the beginning.
Pass it back exactly as received. It is base64 and its structure is not part of this
contract — decoding it, or constructing one by hand, will break without notice.
limitquery · integer · int32
Maximum items to return. The server may return fewer, so always drive the loop from
has_more rather than from the size of the page you got back.
statusquery · string · enum
Only cards in this state. Absent returns every card, which is what a merchant
reconciling their liability wants — an emptied card is still part of the history.
codequery · string
Only the card with this code, matched as a till matches it. Absent returns every card.
Responses
200
A page of gift cards.
GiftCardPage · application/json
has_moreboolean
Required
Whether another page follows.
itemsarray of GiftCard
Required
idstring · uuid
RequiredRead only
balanceMoney
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.
codestring
Required
The code a till accepts at the counter. Case-insensitive; stored upper-cased.
created_atTimestamp
RequiredRead only
Server-assigned RFC 3339 timestamp, always UTC.
Server-assigned because device clocks drift and cannot be trusted for ordering — a till
that has been powered off for a week may come back with a clock hours out. Where the
device's own view of time matters it is carried separately in client_created_at.
expires_atstring · date-time
RequiredNullableRead only
When the card stops being redeemable, set at issue from the merchant's term; null when it does not expire.
merchant_idstring · uuid
RequiredRead only
statusstring · enum
Required
Active cards can be redeemed. A card with nothing left is empty; one a merchant
has voided is inactive.
activeinactiveempty
updated_atTimestamp
RequiredRead only
Server-assigned RFC 3339 timestamp, always UTC.
Server-assigned because device clocks drift and cannot be trusted for ordering — a till
that has been powered off for a week may come back with a clock hours out. Where the
device's own view of time matters it is carried separately in client_created_at.
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
next_cursorstring
Nullable
Opaque cursor for the next page, or null on the last page. Pass it back verbatim as
the cursor query parameter; its contents are not part of this contract.
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.
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.
Issues a gift card against a code, or tops an existing one up, under the
gift-cards:write scope for integrations. A card is stored value, so creating one or
adding to it is the merchant's write — a manager's call in the back office, an
integration's scope here.
Each item carries its own idempotency_key and stands or falls on its own: an invalid
item, a code already live, a top-up from a stale version — each fails in its own result
and every other item is still applied. Only a malformed envelope refuses the request as
a whole. Every change writes a ledger row, so the liability figure stays reconcilable.
Request body application/json · GiftCardBatchUpsertRequest
itemsarray of GiftCardBatchUpsertItem
Required
≥ 1 items · ≤ 500 items
amountMoney
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.
codestring
The card's code, as a till accepts it. Case-insensitive; stored upper-cased. Absent on
an issue, a code is generated, and is then the whole secret. Required to top up.
1–64 chars
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
intentstring · enum
Required
issue creates a card with code and amount as its initial balance; it is
refused with a conflict when the code is already live on the merchant. top_up
adds amount to the balance of the card with code, and must carry its current
version.
issuetop_up
pinstring
The card's scratch-off PIN (gift cards item 20). Required when intent is issue
and the code is merchant-supplied (not one this system generated): a number on a
rack is readable, so it is not proof of possession. Only a hash is stored.
Ignored for top_up.
4–12 chars
recipient_emailstring · email
Who an issued digital card is emailed to, with a link to its balance minted when it is
sent. Ignored for top_up.
≤ 254 chars
versioninteger · int64
The version of the card this write was made from. Required to top up; refused as
412 when somebody else has written since.
≥ 1
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.
Returns the gift card for a code a customer presents, so a till can validate it and
show the balance before charging a sale against it. The card is not debited here —
that happens when a voucher payment referencing the code is submitted on an order.
Parameters
codequery · string
Required
The code on the card. Normalised (case and spacing) before matching.
pinquery · string
The card's PIN (gift cards item 20). A card whose number is readable on a rack
carries one; without it the lookup answers 403 pin_required so the till can
prompt, and a wrong PIN answers 403 pin_mismatch. A card whose code this
system generated needs neither. The server keeps only a hash of the PIN.
Responses
200
The gift card.
GiftCard · application/json
idstring · uuid
RequiredRead only
balanceMoney
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.
codestring
Required
The code a till accepts at the counter. Case-insensitive; stored upper-cased.
created_atTimestamp
RequiredRead only
Server-assigned RFC 3339 timestamp, always UTC.
Server-assigned because device clocks drift and cannot be trusted for ordering — a till
that has been powered off for a week may come back with a clock hours out. Where the
device's own view of time matters it is carried separately in client_created_at.
expires_atstring · date-time
RequiredNullableRead only
When the card stops being redeemable, set at issue from the merchant's term; null when it does not expire.
merchant_idstring · uuid
RequiredRead only
statusstring · enum
Required
Active cards can be redeemed. A card with nothing left is empty; one a merchant
has voided is inactive.
activeinactiveempty
updated_atTimestamp
RequiredRead only
Server-assigned RFC 3339 timestamp, always UTC.
Server-assigned because device clocks drift and cannot be trusted for ordering — a till
that has been powered off for a week may come back with a clock hours out. Where the
device's own view of time matters it is carried separately in client_created_at.
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
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.
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 gift card.
GiftCard · application/json
idstring · uuid
RequiredRead only
balanceMoney
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.
codestring
Required
The code a till accepts at the counter. Case-insensitive; stored upper-cased.
created_atTimestamp
RequiredRead only
Server-assigned RFC 3339 timestamp, always UTC.
Server-assigned because device clocks drift and cannot be trusted for ordering — a till
that has been powered off for a week may come back with a clock hours out. Where the
device's own view of time matters it is carried separately in client_created_at.
expires_atstring · date-time
RequiredNullableRead only
When the card stops being redeemable, set at issue from the merchant's term; null when it does not expire.
merchant_idstring · uuid
RequiredRead only
statusstring · enum
Required
Active cards can be redeemed. A card with nothing left is empty; one a merchant
has voided is inactive.
activeinactiveempty
updated_atTimestamp
RequiredRead only
Server-assigned RFC 3339 timestamp, always UTC.
Server-assigned because device clocks drift and cannot be trusted for ordering — a till
that has been powered off for a week may come back with a clock hours out. Where the
device's own view of time matters it is carried separately in client_created_at.
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
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.
Applies a JSON Merge Patch (RFC 7396) to a gift card, guarded by If-Match. Only the
status may be changed.
inactive voids the card: it stops being redeemable at once, on every till, and a
deactivate movement records when. active restores one voided in error, rather than
issuing a replacement — a replacement gives the guest a new number and leaves the old
balance outstanding.
A balance is not patchable. It is the sum of the card's movements, so value is added by
writing one (POST /v1/gift-cards/batch-upsert, intent: top_up) and never by
assignment: a balance a merchant could set would be stored value with no record of where it
came from, and the liability figure would stop reconciling.
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.
Request body application/merge-patch+json · GiftCardPatch
statusstring · enum
inactive voids the card: it stops being redeemable at once, on every till, and a
deactivate movement records when. active brings one back — a card voided in error is
restored rather than reissued, because reissuing would give the guest a new number and
leave the old balance outstanding.
empty cannot be asked for. A card is empty when nothing is left, which is a fact about
its movements and not a state to be set.
activeinactive
Responses
200
The changed gift card.
GiftCard · application/json
idstring · uuid
RequiredRead only
balanceMoney
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.
codestring
Required
The code a till accepts at the counter. Case-insensitive; stored upper-cased.
created_atTimestamp
RequiredRead only
Server-assigned RFC 3339 timestamp, always UTC.
Server-assigned because device clocks drift and cannot be trusted for ordering — a till
that has been powered off for a week may come back with a clock hours out. Where the
device's own view of time matters it is carried separately in client_created_at.
expires_atstring · date-time
RequiredNullableRead only
When the card stops being redeemable, set at issue from the merchant's term; null when it does not expire.
merchant_idstring · uuid
RequiredRead only
statusstring · enum
Required
Active cards can be redeemed. A card with nothing left is empty; one a merchant
has voided is inactive.
activeinactiveempty
updated_atTimestamp
RequiredRead only
Server-assigned RFC 3339 timestamp, always UTC.
Server-assigned because device clocks drift and cannot be trusted for ordering — a till
that has been powered off for a week may come back with a clock hours out. Where the
device's own view of time matters it is carried separately in client_created_at.
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
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.
409
The request conflicts with current state — a uniqueness constraint, or an attempt
to mutate an order that is already completed or voided.
Unlike 412, re-reading and retrying unchanged will not resolve it; the request
itself has to change.
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.
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.
Holds part of a card's balance for a tender a till is about to make, so that two tills
cannot spend the same value. A hold moves nothing: it takes its amount out of what the
card has available until the sale that tenders the card settles it, the till voids it,
or it expires, fifteen minutes after it was taken.
The sale settles the hold by naming it as its voucher payment's gift_card_hold_id. A
till that is offline takes no hold and tenders the card against what it last knew.
Each item's id is minted by the client. Retrying it answers the same hold rather than
holding twice. Each item stands or falls on its own, and is refused with 422 when the
card cannot be held for the amount (insufficient, inactive, expired,
wrong_location) or carries a PIN the item does not prove (pin_required,
pin_mismatch), as a voucher payment is.
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.
Request body application/json · GiftCardHoldBatchCreateRequest
itemsarray of GiftCardHoldBatchCreateItem
Required
≥ 1 items · ≤ 10 items
gift_card_holdGiftCardHoldCreate
Required
Holds value on a gift card for a tender. The id is minted by the client, so retrying
with the same id never holds twice.
idstring · uuid
Required
Minted by the client; a retry under the same id answers the same hold.
amountMoney
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
Where the card is being tendered. A card sold at one location is held only there.
pinstring
The card's PIN, required when the card carries one, as it is to spend it. Only its
hash is kept, and it is never on the hold.
≤ 12 chars
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
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.
Gives back value held for a tender the till did not make after all. Voiding a hold
already voided answers it again. Refused with 409 when the sale that tendered the card
has settled the hold: that value has been spent.
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.
hold_idpath · string · uuid
Required
UUIDv7 of the gift card hold.
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 · GiftCardHoldVoidRequest
Responses
200
The hold was voided.
GiftCardHold · application/json
idstring · uuid
Required
amountMoney
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.
expires_atstring · date-time
Required
When the hold stops counting against the card if nothing has ended it.
gift_card_idstring · uuid
Required
statusstring · enum
Required
held until it ends. settled once the sale that tendered the card has spent it,
voided once the till gave it back, and expired once expires_at passed with
neither.
heldsettledvoidedexpired
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.
409
The request conflicts with current state — a uniqueness constraint, or an attempt
to mutate an order that is already completed or voided.
Unlike 412, re-reading and retrying unchanged will not resolve it; the request
itself has to change.
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.
Returns every movement on one card, newest first: issued, topped up, redeemed, reversed,
refunded, voided — each with the sale it was part of where there was one.
This is what makes a balance reconcilable rather than believed. Each amount is signed and
the kind says only why, so the balance is their sum and a kind this contract adds later
cannot break that arithmetic. It is also the answer to a guest saying the card had more on
it than the till says.
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 card's movements.
GiftCardLedgerPage · application/json
itemsarray of GiftCardMovement
Required
idstring · uuid
Required
kindstring · enum
Required
Why the balance moved, never which way. issue the card was created with value;
top_up value was added; redeem it paid for a sale; reverse a sale that redeemed
it was voided, so the value came back; refund a refund credited it; store_credit
the card was issued at the counter as a refund's destination; deactivate the card was
voided, which moves nothing and is recorded so the history says when.
Signed, in the card's own currency: negative for value spent, positive for value
added, zero for a movement that only records something. Sum them for the balance.
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.
created_atTimestamp
RequiredRead only
Server-assigned RFC 3339 timestamp, always UTC.
Server-assigned because device clocks drift and cannot be trusted for ordering — a till
that has been powered off for a week may come back with a clock hours out. Where the
device's own view of time matters it is carried separately in client_created_at.
notestring
What a person typed when they made the movement by hand. Empty otherwise.
order_idstring · uuid
Nullable
The sale this movement was part of, or null for one made in the back office.
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.
Loads pre-printed card numbers, each with the PIN printed beside it, as inactive cards
with no balance: none is redeemable until a sale activates it. A code already loaded or
live is skipped, so a file can be loaded again after a partial failure. The answer carries
each newly loaded card's balance link, for the printer, this once.
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 · PrintedGiftCardLoadCreate
cardsarray of PrintedGiftCard
Required
≥ 1 items · ≤ 5000 items
codestring
Required
1–64 chars
pinstring
Required
A number on a rack is readable, so the PIN is the proof of possession. Only a hash is kept.
4–12 chars
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
201
What was loaded.
PrintedGiftCardLoad · application/json
cardsarray of PrintedGiftCardLoaded
Required
balance_urlstring · uri
Required
codestring
Required
loadedinteger
Required
How many cards were newly loaded; a code already loaded or live is not counted.
≥ 0
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.
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.
Streams every card the merchant has issued, newest first: its code, what is left on it in
minor units beside its currency, its status and when it was issued. A card's code spends
it, so the file is a gift card holder's to keep safe. A support visit takes no copy away.
Responses
200
The cards, one row each.
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.