API referenceTenvelo API
https://api.tenvelo.com The contract, as OpenAPI

Contract 2026-10-10. Every path below carries its own major version.

POS · Reference

Devices

Tills and kitchen screens, how far behind each one is, and how a device gets the credential it syncs with.

Operations
14
Base
https://api.tenvelo.com
  1. POST/v1/device-peripheral-outcomes/batch-create

    Report what a till's hardware failed at

    No. batchCreateDevicePeripheralOutcomesAuth devices:write

    One till's own word on its hardware: the failures its peripheral attempts ended in, as its code saw them (devices item 16). The hardware is invisible through a support call, so the device's report is what answers it — a till reports only a failure, never a success.

    Each item carries its own id and stands or falls on its own: an item naming a peripheral the registry has never heard of fails in its result and every other item is still recorded. A retry under the same id is the same outcome, never a second fault for one dead printer.

    Request body application/json · Peripheral outcome batch create request

    • itemsarray of Peripheral outcome batch create request item

      Required

      ≥ 1 items · ≤ 100 items

      • idstring · uuid

        Required

        The device's own id for the report.

      • kindstring · enum

        Required

        Which peripheral the attempt was a trial of.

        receipt_printerkitchen_printerdrawer

      • idempotency_keyItemIdempotencyKey

        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

      • occurred_atstring · date-time

        Required

        When the attempt failed, on the device's clock.

      • outcomestring · enum

        Required

        What the attempt ended in.

        failed

      • reasonstring

        The device's own words for why it failed, or empty when it has none.

        ≤ 300 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.

  2. GET/v1/device-write-offs

    List written-off tills

    No. listDeviceWriteOffsAuth devices:read

    Returns the tills retired as write-offs, newest first, with who signed each off and what was on it.

    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.

    Responses

    • 200

      A page of write-offs.

      DeviceWriteOffPage · application/json
      • has_moreboolean

        Required

      • itemsarray of DeviceWriteOff

        Required

        • idstring · uuid

          Required

        • 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.

        • device_idstring · uuid

          Required

        • device_namestring

          Required

          What the till was called; the device itself is gone.

        • oldest_queued_atstring · date-time

          Nullable

        • queued_countinteger · int32

          Required

          What it last said it held that had not reached the server.

          ≥ 0

        • reasonstring

          Required

        • written_off_bystring · uuid

          Required

          The person who signed it off.

      • 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.

  3. GET/v1/devices

    List devices

    No. listDevicesAuth bearer

    Returns the merchant's tills, each with how far behind the feed it is and what it holds unsent: the first thing to check when a sale is missing from a report.

    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.

    Responses

    • 200

      A page of devices.

      DevicePage · application/json
      • has_moreboolean

        Required

        Whether another page follows.

      • itemsarray of Device

        Required

        • idstring · uuid

          Required

        • typestring · enum

          What the device is for. till (the default) sells and syncs the catalogue; a kitchen only reads the work the kitchen has been told to make (orders and kitchen tickets) and writes bumps, and nothing else — no sales, no drawer; a landing watches the orders arriving from the ordering site and accepts or rejects them, and like a kitchen it neither sells nor opens a drawer; a host shows the day's bookings beside the floor and seats a party, and like a kitchen it neither sells nor opens a drawer. A kitchen, landing or host device's token cannot take a payment or open a drawer, so a stolen screen has the smallest blast radius.

          tillkitchenlandinghost

          default till

        • namestring

          Required

          1–128 chars

        • app_versionstring

          Read only

          POS app version last reported. Old versions in the field are why breaking spec changes are gated in CI.

          ≤ 32 chars

        • browserstring

          Read only

          The browser a virtual device was opened in, as a person names it ("Firefox on Linux"), so two of the same name can be told apart. Empty when it cannot be told.

        • changes_behindinteger · int64

          NullableRead only

          How many changes the merchant's feed holds that this device has not yet confirmed, as it stood when the device was read. null for a device that has never synced.

        • check_sidestring · enum

          Which side of the sale screen the check (the basket) sits, with the menu on the other. right is what a UK counter expects; left is how Toast draws it. A device's, not a location's: the same room can hold a left-handed bartender.

          leftright

          default right

        • clock_offset_msinteger · int64

          NullableRead only

          How far the server's clock is ahead of the device's own, as the device last measured it. null until it has.

        • clock_wrongboolean

          Read only

          Whether the device's own clock is far enough from the server's to be wrong rather than drifting, so it dates what it records offline by the wrong time until it is set right.

        • 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.

        • created_bystring · uuid

          NullableRead only

          The person who added the device, or opened it in their browser.

        • drawer_discrepancyMoney

          RequiredNullableRead only

          The difference between what the till itself thought its latest counted drawer held and what the server derived — a sync discrepancy between the two sides' arithmetic, never a cash figure (the count is judged against the server's figure). Null for a device that has never counted, or on closes recorded before the till's figure was kept.

          • 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.

        • last_seen_atstring · date-time

          NullableRead only

          When this device last reached the API.

        • last_synced_sequenceinteger · int64

          NullableRead only

          How far through the change feed this device has confirmed it is.

        • latest_peripheral_outcomePeripheral outcome

          NullableRead only

          The device's own most recent report of a failed peripheral attempt — which peripheral, what happened — or null when it has never reported one. The hardware is invisible through a support call; the device's own word is what answers it.

          • idstring · uuid

            Required

            The device's own id for the report. A retry under the same id is the same outcome, never a second fault for one dead printer.

          • kindstring · enum

            Required

            Which peripheral the attempt was a trial of.

            receipt_printerkitchen_printerdrawer

          • occurred_atstring · date-time

            Required

            When the attempt failed, on the device's clock — the time the person saw.

          • outcomestring · enum

            Required

            What the attempt ended in. A device reports a failure; silence is the health.

            failed

          • reasonstring

            The device's own words for why it failed, or empty when it has none.

            ≤ 300 chars · default

        • location_idstring · uuid

          Required

          The location this device sells at. Determines which products it syncs.

        • merchant_idstring · uuid

          RequiredRead only

        • modestring · enum

          Nullable

          How this till is worked, which decides what it opens on after sign-on and what its check keeps in view: quick_order for a counter or a bar, table_service for a floor of tables, retail for a shop's counter (scan first). Null means the location decides: retail at a shop, table service where there are tables, quick order otherwise. A mode orders and hides; it never refuses — every place the location has stays reachable from the till's home. Toast's Primary Mode. Meaningless for a kitchen, landing or host device.

          quick_ordertable_serviceretail

        • queued_changesDeviceQueue

          NullableRead only

          What the device last said it holds that has not reached the server yet. null until it has said.

          • countinteger · int32

            Required

            ≥ 0

          • oldest_atstring · date-time

            Nullable

            When the oldest of them was recorded.

        • refusals_outstandinginteger · int32

          Read only

          Items the server refused from this device that nobody has resolved yet.

          ≥ 0

        • station_idstring · uuid

          Nullable

          The kitchen station this device is bound to, when it is a kitchen device: it shows only that station's work. Null for a till or an un-routed kitchen.

        • statusstring · enum

          Required

          An inactive device is refused at sync and cannot take payment. This is the kill switch for a lost or stolen till.

          activeinactive

        • 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.

        • user_agentstring

          Read only

          The User-Agent of the browser a virtual device was opened in; empty for any other.

        • 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

        • virtualboolean

          Read only

          Whether the device is a browser rather than a counter stand.

      • 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.

  4. POST/v1/devices

    Create a device

    No. createDeviceAuth bearer

    Enrols a till.

    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 · Device

    • idstring · uuid

      Required

    • typestring · enum

      What the device is for. till (the default) sells and syncs the catalogue; a kitchen only reads the work the kitchen has been told to make (orders and kitchen tickets) and writes bumps, and nothing else — no sales, no drawer; a landing watches the orders arriving from the ordering site and accepts or rejects them, and like a kitchen it neither sells nor opens a drawer; a host shows the day's bookings beside the floor and seats a party, and like a kitchen it neither sells nor opens a drawer. A kitchen, landing or host device's token cannot take a payment or open a drawer, so a stolen screen has the smallest blast radius.

      tillkitchenlandinghost

      default till

    • namestring

      Required

      1–128 chars

    • check_sidestring · enum

      Which side of the sale screen the check (the basket) sits, with the menu on the other. right is what a UK counter expects; left is how Toast draws it. A device's, not a location's: the same room can hold a left-handed bartender.

      leftright

      default right

    • location_idstring · uuid

      Required

      The location this device sells at. Determines which products it syncs.

    • modestring · enum

      Nullable

      How this till is worked, which decides what it opens on after sign-on and what its check keeps in view: quick_order for a counter or a bar, table_service for a floor of tables, retail for a shop's counter (scan first). Null means the location decides: retail at a shop, table service where there are tables, quick order otherwise. A mode orders and hides; it never refuses — every place the location has stays reachable from the till's home. Toast's Primary Mode. Meaningless for a kitchen, landing or host device.

      quick_ordertable_serviceretail

    • station_idstring · uuid

      Nullable

      The kitchen station this device is bound to, when it is a kitchen device: it shows only that station's work. Null for a till or an un-routed kitchen.

    • statusstring · enum

      Required

      An inactive device is refused at sync and cannot take payment. This is the kill switch for a lost or stolen till.

      activeinactive

    Responses

    • 201

      The device was created.

      Device · application/json
      • idstring · uuid

        Required

      • typestring · enum

        What the device is for. till (the default) sells and syncs the catalogue; a kitchen only reads the work the kitchen has been told to make (orders and kitchen tickets) and writes bumps, and nothing else — no sales, no drawer; a landing watches the orders arriving from the ordering site and accepts or rejects them, and like a kitchen it neither sells nor opens a drawer; a host shows the day's bookings beside the floor and seats a party, and like a kitchen it neither sells nor opens a drawer. A kitchen, landing or host device's token cannot take a payment or open a drawer, so a stolen screen has the smallest blast radius.

        tillkitchenlandinghost

        default till

      • namestring

        Required

        1–128 chars

      • app_versionstring

        Read only

        POS app version last reported. Old versions in the field are why breaking spec changes are gated in CI.

        ≤ 32 chars

      • browserstring

        Read only

        The browser a virtual device was opened in, as a person names it ("Firefox on Linux"), so two of the same name can be told apart. Empty when it cannot be told.

      • changes_behindinteger · int64

        NullableRead only

        How many changes the merchant's feed holds that this device has not yet confirmed, as it stood when the device was read. null for a device that has never synced.

      • check_sidestring · enum

        Which side of the sale screen the check (the basket) sits, with the menu on the other. right is what a UK counter expects; left is how Toast draws it. A device's, not a location's: the same room can hold a left-handed bartender.

        leftright

        default right

      • clock_offset_msinteger · int64

        NullableRead only

        How far the server's clock is ahead of the device's own, as the device last measured it. null until it has.

      • clock_wrongboolean

        Read only

        Whether the device's own clock is far enough from the server's to be wrong rather than drifting, so it dates what it records offline by the wrong time until it is set right.

      • 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.

      • created_bystring · uuid

        NullableRead only

        The person who added the device, or opened it in their browser.

      • drawer_discrepancyMoney

        RequiredNullableRead only

        The difference between what the till itself thought its latest counted drawer held and what the server derived — a sync discrepancy between the two sides' arithmetic, never a cash figure (the count is judged against the server's figure). Null for a device that has never counted, or on closes recorded before the till's figure was kept.

        • 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.

      • last_seen_atstring · date-time

        NullableRead only

        When this device last reached the API.

      • last_synced_sequenceinteger · int64

        NullableRead only

        How far through the change feed this device has confirmed it is.

      • latest_peripheral_outcomePeripheral outcome

        NullableRead only

        The device's own most recent report of a failed peripheral attempt — which peripheral, what happened — or null when it has never reported one. The hardware is invisible through a support call; the device's own word is what answers it.

        • idstring · uuid

          Required

          The device's own id for the report. A retry under the same id is the same outcome, never a second fault for one dead printer.

        • kindstring · enum

          Required

          Which peripheral the attempt was a trial of.

          receipt_printerkitchen_printerdrawer

        • occurred_atstring · date-time

          Required

          When the attempt failed, on the device's clock — the time the person saw.

        • outcomestring · enum

          Required

          What the attempt ended in. A device reports a failure; silence is the health.

          failed

        • reasonstring

          The device's own words for why it failed, or empty when it has none.

          ≤ 300 chars · default

      • location_idstring · uuid

        Required

        The location this device sells at. Determines which products it syncs.

      • merchant_idstring · uuid

        RequiredRead only

      • modestring · enum

        Nullable

        How this till is worked, which decides what it opens on after sign-on and what its check keeps in view: quick_order for a counter or a bar, table_service for a floor of tables, retail for a shop's counter (scan first). Null means the location decides: retail at a shop, table service where there are tables, quick order otherwise. A mode orders and hides; it never refuses — every place the location has stays reachable from the till's home. Toast's Primary Mode. Meaningless for a kitchen, landing or host device.

        quick_ordertable_serviceretail

      • queued_changesDeviceQueue

        NullableRead only

        What the device last said it holds that has not reached the server yet. null until it has said.

        • countinteger · int32

          Required

          ≥ 0

        • oldest_atstring · date-time

          Nullable

          When the oldest of them was recorded.

      • refusals_outstandinginteger · int32

        Read only

        Items the server refused from this device that nobody has resolved yet.

        ≥ 0

      • station_idstring · uuid

        Nullable

        The kitchen station this device is bound to, when it is a kitchen device: it shows only that station's work. Null for a till or an un-routed kitchen.

      • statusstring · enum

        Required

        An inactive device is refused at sync and cannot take payment. This is the kill switch for a lost or stolen till.

        activeinactive

      • 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.

      • user_agentstring

        Read only

        The User-Agent of the browser a virtual device was opened in; empty for any other.

      • 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

      • virtualboolean

        Read only

        Whether the device is a browser rather than a counter stand.

    • 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.

    • 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.

  5. POST/v1/devices/enrol

    Enrol a till

    No. redeemPairingCodeAuth no token

    Exchanges a pairing code for a registered device key.

    Called by a till that has no credential yet, which is the one thing on this API that can be: the pairing code is the credential, it is good once, and it expires in fifteen minutes. A manager reads it off the back office and somebody types it into the till in front of them.

    The device generates a key pair and sends the public half. The private half never leaves it — that is the point of doing this rather than handing out an API key, since a till is unattended hardware in a room full of strangers and anything copyable off it eventually is.

    Enrolling again replaces the key and revokes every token issued against the old one. A replaced or factory-reset till leaves nothing behind it that still works.

    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 · DeviceEnrolment

    • device_idstring · uuid

      The virtual device this browser already holds. A code issued at a location re-keys it, when it is a virtual device of the same type at that location, rather than adding another: each browser is one device, and a second would strand what the first has not yet sent. Ignored for a code issued to a device.

    • pairing_codestring

      Required

      The code the back office issued, as it was read out.

      Case and grouping are ignored: the hyphens exist so it can be read aloud across a bar, and nobody types them reliably. zkmpd67vx and ZKM-PD6-7VX are the same code.

      ≤ 32 chars

    • public_keystring · byte

      Required

      The public half of a key the device generated, base64-encoded SPKI DER.

      P-256, because ES256 is the only signature algorithm this API accepts from a device. Fixed rather than negotiated: letting a client choose its algorithm is how a verifier ends up accepting none.

    Responses

    • 200

      The device is enrolled and may now request tokens.

      Device · application/json
      • idstring · uuid

        Required

      • typestring · enum

        What the device is for. till (the default) sells and syncs the catalogue; a kitchen only reads the work the kitchen has been told to make (orders and kitchen tickets) and writes bumps, and nothing else — no sales, no drawer; a landing watches the orders arriving from the ordering site and accepts or rejects them, and like a kitchen it neither sells nor opens a drawer; a host shows the day's bookings beside the floor and seats a party, and like a kitchen it neither sells nor opens a drawer. A kitchen, landing or host device's token cannot take a payment or open a drawer, so a stolen screen has the smallest blast radius.

        tillkitchenlandinghost

        default till

      • namestring

        Required

        1–128 chars

      • app_versionstring

        Read only

        POS app version last reported. Old versions in the field are why breaking spec changes are gated in CI.

        ≤ 32 chars

      • browserstring

        Read only

        The browser a virtual device was opened in, as a person names it ("Firefox on Linux"), so two of the same name can be told apart. Empty when it cannot be told.

      • changes_behindinteger · int64

        NullableRead only

        How many changes the merchant's feed holds that this device has not yet confirmed, as it stood when the device was read. null for a device that has never synced.

      • check_sidestring · enum

        Which side of the sale screen the check (the basket) sits, with the menu on the other. right is what a UK counter expects; left is how Toast draws it. A device's, not a location's: the same room can hold a left-handed bartender.

        leftright

        default right

      • clock_offset_msinteger · int64

        NullableRead only

        How far the server's clock is ahead of the device's own, as the device last measured it. null until it has.

      • clock_wrongboolean

        Read only

        Whether the device's own clock is far enough from the server's to be wrong rather than drifting, so it dates what it records offline by the wrong time until it is set right.

      • 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.

      • created_bystring · uuid

        NullableRead only

        The person who added the device, or opened it in their browser.

      • drawer_discrepancyMoney

        RequiredNullableRead only

        The difference between what the till itself thought its latest counted drawer held and what the server derived — a sync discrepancy between the two sides' arithmetic, never a cash figure (the count is judged against the server's figure). Null for a device that has never counted, or on closes recorded before the till's figure was kept.

        • 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.

      • last_seen_atstring · date-time

        NullableRead only

        When this device last reached the API.

      • last_synced_sequenceinteger · int64

        NullableRead only

        How far through the change feed this device has confirmed it is.

      • latest_peripheral_outcomePeripheral outcome

        NullableRead only

        The device's own most recent report of a failed peripheral attempt — which peripheral, what happened — or null when it has never reported one. The hardware is invisible through a support call; the device's own word is what answers it.

        • idstring · uuid

          Required

          The device's own id for the report. A retry under the same id is the same outcome, never a second fault for one dead printer.

        • kindstring · enum

          Required

          Which peripheral the attempt was a trial of.

          receipt_printerkitchen_printerdrawer

        • occurred_atstring · date-time

          Required

          When the attempt failed, on the device's clock — the time the person saw.

        • outcomestring · enum

          Required

          What the attempt ended in. A device reports a failure; silence is the health.

          failed

        • reasonstring

          The device's own words for why it failed, or empty when it has none.

          ≤ 300 chars · default

      • location_idstring · uuid

        Required

        The location this device sells at. Determines which products it syncs.

      • merchant_idstring · uuid

        RequiredRead only

      • modestring · enum

        Nullable

        How this till is worked, which decides what it opens on after sign-on and what its check keeps in view: quick_order for a counter or a bar, table_service for a floor of tables, retail for a shop's counter (scan first). Null means the location decides: retail at a shop, table service where there are tables, quick order otherwise. A mode orders and hides; it never refuses — every place the location has stays reachable from the till's home. Toast's Primary Mode. Meaningless for a kitchen, landing or host device.

        quick_ordertable_serviceretail

      • queued_changesDeviceQueue

        NullableRead only

        What the device last said it holds that has not reached the server yet. null until it has said.

        • countinteger · int32

          Required

          ≥ 0

        • oldest_atstring · date-time

          Nullable

          When the oldest of them was recorded.

      • refusals_outstandinginteger · int32

        Read only

        Items the server refused from this device that nobody has resolved yet.

        ≥ 0

      • station_idstring · uuid

        Nullable

        The kitchen station this device is bound to, when it is a kitchen device: it shows only that station's work. Null for a till or an un-routed kitchen.

      • statusstring · enum

        Required

        An inactive device is refused at sync and cannot take payment. This is the kill switch for a lost or stolen till.

        activeinactive

      • 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.

      • user_agentstring

        Read only

        The User-Agent of the browser a virtual device was opened in; empty for any other.

      • 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

      • virtualboolean

        Read only

        Whether the device is a browser rather than a counter stand.

    • 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.

    • 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.

    • 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.

  6. POST/v1/devices/token

    Issue a device access token

    No. createDeviceTokenAuth no token

    The OAuth 2.0 token endpoint a till authenticates against.

    A device authenticates with private_key_jwt — it signs a short-lived JWT assertion with the key it registered at enrolment, following RFC 7523 — and receives an access token in exchange. Nothing replayable crosses the wire: the private key stays on the device, and the assertion's jti is spent on arrival so a captured one is worth nothing even inside its own minute.

    Form-encoded, and answering with an RFC 6749 token response, because that is what the standard says a token endpoint looks like. Everything else in this API is JSON; this is the one place where being recognisable to any OAuth client matters more than being consistent with its neighbours.

    The token is opaque. Nothing outside this system verifies one, and a token that must be looked up is a token that can be revoked the instant a till is deactivated — which for hardware sitting on a counter is worth more than saving a query.

    Request body application/x-www-form-urlencoded · DeviceTokenRequest

    • audiencestring

      The API the token is for.

    • client_assertionstring

      Required

      A JWT signed with the device's enrolled key.

      iss and sub are both the device id, aud is this endpoint's URL, jti is unique and exp is at most a minute out. The audience matters: without it, an assertion a till was persuaded to sign for one service would be a credential at every other service that trusts the same key.

    • client_assertion_typestring · enum

      Required

      urn:ietf:params:oauth:client-assertion-type:jwt-bearer

    • grant_typestring · enum

      Required

      The only grant a device may use. There is no authorization code and no refresh token: a till has a key and asks for a token with it, and every other grant is surface a stolen device could reach for.

      client_credentials

    • scopestring · enum

      Enough to pull the catalogue and push what has been sold, and nothing else. A stolen device's blast radius is this line.

      sync

    Responses

    • 200

      An access token.

      DeviceToken · application/json
      • access_tokenstring

        Required

        Opaque. Present it as a bearer token; it cannot be inspected, and it stops working the moment the device it belongs to is deactivated rather than when it expires.

      • expires_ininteger · int64

        Required

        Seconds. Short, because a till is unattended — but not so short that a shop on bad wifi is locked out by its own credential refresh.

      • scopestring
      • token_typestring · enum

        Required

        bearer

    • 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.

  7. GET/v1/devices/{id}

    Get a device

    No. getDeviceAuth bearer

    Returns a single device.

    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 device.

      Device · application/json
      • idstring · uuid

        Required

      • typestring · enum

        What the device is for. till (the default) sells and syncs the catalogue; a kitchen only reads the work the kitchen has been told to make (orders and kitchen tickets) and writes bumps, and nothing else — no sales, no drawer; a landing watches the orders arriving from the ordering site and accepts or rejects them, and like a kitchen it neither sells nor opens a drawer; a host shows the day's bookings beside the floor and seats a party, and like a kitchen it neither sells nor opens a drawer. A kitchen, landing or host device's token cannot take a payment or open a drawer, so a stolen screen has the smallest blast radius.

        tillkitchenlandinghost

        default till

      • namestring

        Required

        1–128 chars

      • app_versionstring

        Read only

        POS app version last reported. Old versions in the field are why breaking spec changes are gated in CI.

        ≤ 32 chars

      • browserstring

        Read only

        The browser a virtual device was opened in, as a person names it ("Firefox on Linux"), so two of the same name can be told apart. Empty when it cannot be told.

      • changes_behindinteger · int64

        NullableRead only

        How many changes the merchant's feed holds that this device has not yet confirmed, as it stood when the device was read. null for a device that has never synced.

      • check_sidestring · enum

        Which side of the sale screen the check (the basket) sits, with the menu on the other. right is what a UK counter expects; left is how Toast draws it. A device's, not a location's: the same room can hold a left-handed bartender.

        leftright

        default right

      • clock_offset_msinteger · int64

        NullableRead only

        How far the server's clock is ahead of the device's own, as the device last measured it. null until it has.

      • clock_wrongboolean

        Read only

        Whether the device's own clock is far enough from the server's to be wrong rather than drifting, so it dates what it records offline by the wrong time until it is set right.

      • 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.

      • created_bystring · uuid

        NullableRead only

        The person who added the device, or opened it in their browser.

      • drawer_discrepancyMoney

        RequiredNullableRead only

        The difference between what the till itself thought its latest counted drawer held and what the server derived — a sync discrepancy between the two sides' arithmetic, never a cash figure (the count is judged against the server's figure). Null for a device that has never counted, or on closes recorded before the till's figure was kept.

        • 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.

      • last_seen_atstring · date-time

        NullableRead only

        When this device last reached the API.

      • last_synced_sequenceinteger · int64

        NullableRead only

        How far through the change feed this device has confirmed it is.

      • latest_peripheral_outcomePeripheral outcome

        NullableRead only

        The device's own most recent report of a failed peripheral attempt — which peripheral, what happened — or null when it has never reported one. The hardware is invisible through a support call; the device's own word is what answers it.

        • idstring · uuid

          Required

          The device's own id for the report. A retry under the same id is the same outcome, never a second fault for one dead printer.

        • kindstring · enum

          Required

          Which peripheral the attempt was a trial of.

          receipt_printerkitchen_printerdrawer

        • occurred_atstring · date-time

          Required

          When the attempt failed, on the device's clock — the time the person saw.

        • outcomestring · enum

          Required

          What the attempt ended in. A device reports a failure; silence is the health.

          failed

        • reasonstring

          The device's own words for why it failed, or empty when it has none.

          ≤ 300 chars · default

      • location_idstring · uuid

        Required

        The location this device sells at. Determines which products it syncs.

      • merchant_idstring · uuid

        RequiredRead only

      • modestring · enum

        Nullable

        How this till is worked, which decides what it opens on after sign-on and what its check keeps in view: quick_order for a counter or a bar, table_service for a floor of tables, retail for a shop's counter (scan first). Null means the location decides: retail at a shop, table service where there are tables, quick order otherwise. A mode orders and hides; it never refuses — every place the location has stays reachable from the till's home. Toast's Primary Mode. Meaningless for a kitchen, landing or host device.

        quick_ordertable_serviceretail

      • queued_changesDeviceQueue

        NullableRead only

        What the device last said it holds that has not reached the server yet. null until it has said.

        • countinteger · int32

          Required

          ≥ 0

        • oldest_atstring · date-time

          Nullable

          When the oldest of them was recorded.

      • refusals_outstandinginteger · int32

        Read only

        Items the server refused from this device that nobody has resolved yet.

        ≥ 0

      • station_idstring · uuid

        Nullable

        The kitchen station this device is bound to, when it is a kitchen device: it shows only that station's work. Null for a till or an un-routed kitchen.

      • statusstring · enum

        Required

        An inactive device is refused at sync and cannot take payment. This is the kill switch for a lost or stolen till.

        activeinactive

      • 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.

      • user_agentstring

        Read only

        The User-Agent of the browser a virtual device was opened in; empty for any other.

      • 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

      • virtualboolean

        Read only

        Whether the device is a browser rather than a counter stand.

    • 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.

  8. PATCH/v1/devices/{id}

    Update a device

    No. updateDeviceAuth no token

    Merge-patches a device, guarded by If-Match: only the fields present change. Setting status to inactive is the kill switch for a lost or stolen till: it is refused at sync and can no longer take payment.

    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 · DevicePatch

    • typestring · enum

      tillkitchenlandinghost

    • namestring

      1–128 chars

    • check_sidestring · enum

      Which side of the sale screen the check sits.

      leftright

    • location_idstring · uuid

      The location this device sells at. Determines which products it syncs.

    • modestring · enum

      Nullable

      How this till is worked (see the device's mode); null hands the choice back to the location.

      quick_ordertable_serviceretail<nil>

    • station_idstring · uuid

      Nullable

      The kitchen station this device is bound to, when it is a kitchen device; null clears the binding.

    • statusstring · enum

      Setting a device to inactive is the kill switch for a lost or stolen till: it is refused at sync and can no longer take payment.

      activeinactive

    Responses

    • 200

      The updated device.

      Device · application/json
      • idstring · uuid

        Required

      • typestring · enum

        What the device is for. till (the default) sells and syncs the catalogue; a kitchen only reads the work the kitchen has been told to make (orders and kitchen tickets) and writes bumps, and nothing else — no sales, no drawer; a landing watches the orders arriving from the ordering site and accepts or rejects them, and like a kitchen it neither sells nor opens a drawer; a host shows the day's bookings beside the floor and seats a party, and like a kitchen it neither sells nor opens a drawer. A kitchen, landing or host device's token cannot take a payment or open a drawer, so a stolen screen has the smallest blast radius.

        tillkitchenlandinghost

        default till

      • namestring

        Required

        1–128 chars

      • app_versionstring

        Read only

        POS app version last reported. Old versions in the field are why breaking spec changes are gated in CI.

        ≤ 32 chars

      • browserstring

        Read only

        The browser a virtual device was opened in, as a person names it ("Firefox on Linux"), so two of the same name can be told apart. Empty when it cannot be told.

      • changes_behindinteger · int64

        NullableRead only

        How many changes the merchant's feed holds that this device has not yet confirmed, as it stood when the device was read. null for a device that has never synced.

      • check_sidestring · enum

        Which side of the sale screen the check (the basket) sits, with the menu on the other. right is what a UK counter expects; left is how Toast draws it. A device's, not a location's: the same room can hold a left-handed bartender.

        leftright

        default right

      • clock_offset_msinteger · int64

        NullableRead only

        How far the server's clock is ahead of the device's own, as the device last measured it. null until it has.

      • clock_wrongboolean

        Read only

        Whether the device's own clock is far enough from the server's to be wrong rather than drifting, so it dates what it records offline by the wrong time until it is set right.

      • 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.

      • created_bystring · uuid

        NullableRead only

        The person who added the device, or opened it in their browser.

      • drawer_discrepancyMoney

        RequiredNullableRead only

        The difference between what the till itself thought its latest counted drawer held and what the server derived — a sync discrepancy between the two sides' arithmetic, never a cash figure (the count is judged against the server's figure). Null for a device that has never counted, or on closes recorded before the till's figure was kept.

        • 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.

      • last_seen_atstring · date-time

        NullableRead only

        When this device last reached the API.

      • last_synced_sequenceinteger · int64

        NullableRead only

        How far through the change feed this device has confirmed it is.

      • latest_peripheral_outcomePeripheral outcome

        NullableRead only

        The device's own most recent report of a failed peripheral attempt — which peripheral, what happened — or null when it has never reported one. The hardware is invisible through a support call; the device's own word is what answers it.

        • idstring · uuid

          Required

          The device's own id for the report. A retry under the same id is the same outcome, never a second fault for one dead printer.

        • kindstring · enum

          Required

          Which peripheral the attempt was a trial of.

          receipt_printerkitchen_printerdrawer

        • occurred_atstring · date-time

          Required

          When the attempt failed, on the device's clock — the time the person saw.

        • outcomestring · enum

          Required

          What the attempt ended in. A device reports a failure; silence is the health.

          failed

        • reasonstring

          The device's own words for why it failed, or empty when it has none.

          ≤ 300 chars · default

      • location_idstring · uuid

        Required

        The location this device sells at. Determines which products it syncs.

      • merchant_idstring · uuid

        RequiredRead only

      • modestring · enum

        Nullable

        How this till is worked, which decides what it opens on after sign-on and what its check keeps in view: quick_order for a counter or a bar, table_service for a floor of tables, retail for a shop's counter (scan first). Null means the location decides: retail at a shop, table service where there are tables, quick order otherwise. A mode orders and hides; it never refuses — every place the location has stays reachable from the till's home. Toast's Primary Mode. Meaningless for a kitchen, landing or host device.

        quick_ordertable_serviceretail

      • queued_changesDeviceQueue

        NullableRead only

        What the device last said it holds that has not reached the server yet. null until it has said.

        • countinteger · int32

          Required

          ≥ 0

        • oldest_atstring · date-time

          Nullable

          When the oldest of them was recorded.

      • refusals_outstandinginteger · int32

        Read only

        Items the server refused from this device that nobody has resolved yet.

        ≥ 0

      • station_idstring · uuid

        Nullable

        The kitchen station this device is bound to, when it is a kitchen device: it shows only that station's work. Null for a till or an un-routed kitchen.

      • statusstring · enum

        Required

        An inactive device is refused at sync and cannot take payment. This is the kill switch for a lost or stolen till.

        activeinactive

      • 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.

      • user_agentstring

        Read only

        The User-Agent of the browser a virtual device was opened in; empty for any other.

      • 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

      • virtualboolean

        Read only

        Whether the device is a browser rather than a counter stand.

    • 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.

  9. DELETE/v1/devices/{id}

    Delete a device

    No. deleteDeviceAuth bearer

    Soft-deletes a device. Orders it took are retained. A device still holding changes it has not sent is refused with 409, so a sale it holds is not lost; a lost or broken one is retired with write-off instead.

    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.

    Responses

    • 204

      The device was deleted.

    • 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.

    • 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.

  10. POST/v1/devices/{id}/pair

    Issue a pairing code

    No. pairDeviceAuth devices:write

    Issues the code a new till types to become this device, replacing any code before it. Only a person may issue one: a till must not be able to enrol another till.

    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.

    • 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

    • 201

      The code, shown this once.

      DevicePairing · application/json
      • codestring

        RequiredRead only

        Grouped for reading aloud; the till accepts it in any case and without the hyphens.

      • device_idstring · uuid

        Required

      • expires_atstring · date-time

        RequiredRead only

    • 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.

  11. GET/v1/devices/{id}/peripheral-outcomes

    List what a device's hardware failed at

    No. listDevicePeripheralOutcomesAuth devices:read

    The device's recorded peripheral failures, newest first — its own word on its hardware (devices item 16). A device with none has never reported a failure: whether it is silent or healthy is read by its sync state, which the device row already carries.

    A till reads its own health; an owner or a manager at a site they may act in reads the device's. A device at another merchant or another location is as absent as never paired.

    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.

    • 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.

    Responses

    • 200

      A page of peripheral outcomes.

      Peripheral outcomes · application/json
      • has_moreboolean

        Required

        Whether more are recorded than this page carries.

      • itemsarray of Peripheral outcome

        Required

        • idstring · uuid

          Required

          The device's own id for the report. A retry under the same id is the same outcome, never a second fault for one dead printer.

        • kindstring · enum

          Required

          Which peripheral the attempt was a trial of.

          receipt_printerkitchen_printerdrawer

        • occurred_atstring · date-time

          Required

          When the attempt failed, on the device's clock — the time the person saw.

        • outcomestring · enum

          Required

          What the attempt ended in. A device reports a failure; silence is the health.

          failed

        • reasonstring

          The device's own words for why it failed, or empty when it has none.

          ≤ 300 chars · default

      • next_cursorstring

        Nullable

        Pass as cursor for the next page, when has_more is true.

    • 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.

  12. GET/v1/devices/{id}/refusals

    List what was refused a device

    No. listDeviceRefusalsAuth devices:read

    What the server refused one device, one row per item: its latest refusal, the problem the device was answered, and what became of it. Outstanding items come first, then the newest. Paged with the cursor of the previous page.

    A refused sale carries what it was for, so only an owner or a manager at a site they may act in reads it. What to do about an outstanding item is decided on the device that holds it.

    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.

    • 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.

    Responses

    • 200

      A page of refusals.

      Refusal page · application/json
      • has_moreboolean

        Required

        Whether another page may follow.

      • itemsarray of Refusal

        Required

        • idstring · uuid

          Required

        • amountMoney

          RequiredNullable

          How much the item was for, or null when it said nothing a person would count.

          • 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.

        • batchstring

          Required

          The batch the item came in, as its path, such as /v1/orders/batch-create.

        • idempotency_keystring

          Required

          The item's own key, which is how the device names it.

        • problemProblem

          Required

          An error, as defined by RFC 9457 Problem Details for HTTP APIs.

          Every non-2xx response in this API uses this shape and the application/problem+json media type. Clients should branch on type — a stable URI — rather than on title or detail, both of which are human-readable and may be reworded without notice.

          • 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.

        • refused_atstring · date-time

          Required

        • resolutionRefusalOutcome

          RequiredNullable

          What became of the item, or null while it is outstanding.

          • member_idstring · uuid

            RequiredNullable

            The member who resolved it, when a person did and the device knew who.

          • outcomestring · enum

            Required

            resubmitted: the server accepted the same key later. written_off: a sale whose money never reached the books, accounted for. dismissed: a write the world had moved past.

            resubmittedwritten_offdismissed

          • reasonstring

            Required

            The person's own words; empty for a resubmission.

          • resolved_atstring · date-time

            Required

        • subject_idstring · uuid

          RequiredNullable

          The resource the item named, such as the sale, or null when it named none.

      • next_cursorstring

        Nullable

        Pass as cursor for the next page, when has_more is true.

    • 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.

  13. POST/v1/devices/{id}/write-off

    Write a device off

    No. writeOffDeviceAuth devices:write

    Retires a till that is lost, stolen or broken although it still holds changes it never sent. The write-off is signed by the person making it and kept with what was queued on the device (GET /v1/device-write-offs). A device holding nothing is simply deleted.

    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.

    Request body application/json · DeviceWriteOffRequest

    • reasonstring

      Required

      1–500 chars

    Responses

    • 204

      The device was written off.

    • 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.

    • 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.

  14. POST/v1/locations/{id}/pair

    Issue a code that opens a virtual device here

    No. pairLocationAuth devices:write

    Issues the code a browser redeems at /v1/devices/enrol to become a virtual till or kitchen display at this location, signed on as the person who issued it. The office app opens the devices site with it in the URL's fragment, which no server receives.

    Only an owner or a manager may, as for a device's code, and they are checked again when the code is redeemed.

    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.

    • 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 · LocationPairing

    • typestring · enum

      Required

      What the browser becomes. till sells; kitchen shows the kitchen's tickets; host shows the host stand's bookings.

      tillkitchenhost

      default till

    Responses

    • 201

      The code, shown this once.

      LocationPairing · application/json
      • typestring · enum

        Required

        What the browser becomes. till sells; kitchen shows the kitchen's tickets; host shows the host stand's bookings.

        tillkitchenhost

        default till

      • codestring

        RequiredRead only

        Grouped for reading aloud; the till accepts it in any case and without the hyphens.

      • expires_atstring · date-time

        RequiredRead only

      • location_idstring · uuid

        RequiredRead only

    • 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.

    • 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.