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

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

Catalog · Reference

Catalog

Products, categories, tax rates and modifiers. Server-authoritative — devices pull these.

Operations
42
Base
https://api.tenvelo.com
  1. POST/v1/catalog-images

    Upload a product's photo

    No. createCatalogImageAuth catalog:write

    Stores a photo for a product, as Square's CreateCatalogImage does: a multipart/form-data body naming the product in product_id and carrying the picture in image_file. It writes no product: the answer's url goes in the product's image_url on its next update, from the version it was read at. Every upload is an image of its own, so one uploaded and abandoned never replaces the photo the tills are showing.

    A JPEG, PNG, WebP or GIF of at most 5 MB, judged by its bytes rather than by the type the client names; anything else is a 422 on /image_file. Only somebody who may change the catalog may upload, and only for a product of their own business.

    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 multipart/form-data · CatalogImageUpload

    • image_filestring · binary

      Required

      The picture, at most 5 MB.

    • product_idstring · uuid

      Required

      The product the photo is for.

    Responses

    • 201

      The photo is stored, at url.

      CatalogImage · application/json
      • idstring · uuid

        RequiredRead only

      • urlstring · uri

        RequiredRead only

        Where the photo is served, for a product's image_url.

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

  2. GET/v1/catalog-images/{id}

    Fetch a product's photo

    No. getCatalogImageAuth no token

    The photo's bytes, for a till's tile, a guest's menu or the back office. Public, since an <img> sends no token, and the same forever: an image's id names one upload, so a browser or any cache between keeps it for good.

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

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

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

  3. POST/v1/catalog-imports

    Import a catalog

    No. createCatalogImportAuth catalog:write

    Bulk-loads products, categories and tax rates. Asynchronous.

    Returns 202 immediately with a Location pointing at a Job to poll. The work is durable: it is enqueued in the same database transaction that accepts the request, so it cannot be accepted and then lost, and it survives the pod being evicted mid-import — which on a rolling deploy is routine rather than exceptional.

    Asynchronous because a ten-thousand-row import cannot finish inside a request without holding a connection open long past any sensible timeout. This is the durability threshold, not a speed one: work goes on a queue when losing it would be unacceptable, not merely when it is slow.

    The Idempotency-Key covers the enqueue. Retrying a request whose response you never saw returns the same job rather than starting a second import.

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

    • categoriesarray of Category
      • idstring · uuid

        Required

      • namestring

        Required

        1–128 chars

      • colorstring · enum

        How this category is coloured on a till.

        A fixed set rather than a free colour value. A merchant given a colour picker will eventually choose two nobody can tell apart, or one that fails contrast against the till's dark ground, and the failure lands on staff mid-service rather than on whoever picked it.

        The hues are Okabe–Ito, chosen because they stay distinguishable under the common forms of colour blindness — roughly one man in twelve, which across a merchant's whole staff is a certainty rather than a risk. Colour is always redundant with the category's name: it makes a known category faster to find, and never carries meaning on its own.

        graybrownorangeyellowgreenskybluepurple

        default gray

      • descriptionstring

        ≤ 1024 chars

      • parent_idstring · uuid

        Nullable

        The category this one sits under, or null for a top-level category.

        Categories form a tree because that is how a till is laid out: staff drill from Drinks to Hot to Coffee rather than hunting a flat grid of several hundred products. Tapping a category shows its sub-categories and any products attached directly to it, so a merchant is not forced to invent a Misc child for the two items that do not fit a sub-category.

        Nesting is capped at 20 levels. No real menu approaches that; the cap exists so a mistake cannot produce a tree that is unbounded to walk. A cycle — making a category a descendant of itself — is rejected with 422, because a till navigating one would loop forever.

      • preparedboolean

        False marks this category not prepared: its lines make no kitchen ticket and no printed round — a can of Coke rung at a table must not sit on the pass next to the burgers. True by default; a location that marks nothing keeps its current behaviour (kitchen-display item 10). A product may override it (prepared_override).

        default true

      • sort_orderinteger · int32

        Ascending display order on the till. Ties break on name.

        ≥ 0 · default 0

      • statusstring · enum

        Required

        activeinactive

    • on_conflictstring · enum

      What to do when an imported row collides with an existing one. Defaults to fail so an accidental re-import cannot silently rewrite a live catalogue — destroying priced products is not a safe default.

      skipoverwritefail

      default fail

    • productsarray of Product

      Required

      ≥ 1 items · ≤ 10000 items

      • idstring · uuid

        Required

      • typestring · enum

        What the product is. A gift_card product is sold as a line that is not revenue and is not taxed on sale — the merchant has taken money for a promise — while everything else is standard. A combo product is a set at a price (menu item 18): its components are the products the guest chooses, sold as their own lines, with the set price apportioned across them at their own rates. Defaults to standard when unstated.

        standardgift_cardcombo

        default standard

      • namestring

        Required

        1–255 chars

      • allergensarray of string

        The allergens a product contains, as the merchant names them (e.g. "gluten", "dairy"). Shown on the till and in the back office. Written against the merchant's allergen vocabulary (server-enforced, canonicalised to the vocabulary's spelling); the words are for display, and allergen_ids carries their identity.

        ≤ 20 items

      • allergens_declaredboolean

        Whether the merchant has declared what the product contains. A product with a non-empty allergens list is declared; a product genuinely free of every allergen is declared with an empty list and allergens_declared: true. Absent or false with an empty list means no declaration at all — the safety feature cannot read a product it knows nothing about, and a merchant can insist on a declaration through a field policy (field-policies item 9). The till never assumes; a declared-none product matches nothing and reads as clean, an undeclared one is named by the conformance view.

        default false

      • barcodesarray of string

        The barcodes this product can be scanned or searched as. A product may have none; each entry must be non-blank and unique. The till's search matches any of them.

      • category_idstring · uuid

        Nullable

        The category this product belongs to, if any.

      • channel_pricesarray of ProductChannelPrice

        Per-channel prices, the exceptions to price. A product may be priced differently on one channel than another (menu item 19); where no override exists the base price applies. Each entry is a channel with a price, and a channel may appear at most once. Set server-authoritative, like the rest of the catalogue.

        • channelstring · enum

          Required

          The channel this price applies on.

          postableonlineintegration

        • effective_fromstring · date

          The date this price takes effect (locations-and-groups item 14). Absent means the price is in force now; a future date is a scheduled change resolved at the business day's boundary. At most one future entry exists per channel.

        • priceMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

      • componentsarray of object

        The products a combo is made of (menu item 18), only present when type is combo. The guest chooses one of each at the till; the set price is apportioned across the chosen components at their own rates and each is added as its own line, so the kitchen, stock depletion and the mix report behave as for any line. A component may appear at most once and may not be the combo itself.

        ≤ 50 items

        • category_idstring · uuid

          Nullable

          The category this slot chooses one item of at the till (mix-and-match, retail promotions): the operator picks one active product of the category, and the set price is apportioned across the chosen items as any combo's is. Exactly one of product_id or category_id is present.

        • product_idstring · uuid

          Nullable

          The fixed product this slot contains. Exactly one of product_id or category_id is present.

        • quantityQuantity

          Required

          How much of something, as a decimal string in the unit_of_measure beside it (each where there is none), as Square's quantity is: three croissants are "3", 1.4 kg of beans is "1.4". A string because a JSON number is a binary float to most parsers.

          It has at most as many decimal places as its unit's base unit holds — three in each, g and ml, six in kg and l — and a finer one is refused, never rounded. Tenvelo holds every quantity as an integer of that base unit (thousandths of an each, milligrams, microlitres; docs/decisions/0063); a client that computes with one should read the digits the same way, never through a float.

          ≤ 26 chars · pattern ^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$

      • costMoney

        Nullable

        What the product costs the merchant, in the product's currency, if one is known. Inventory values stock at it and the product screen shows the margin (price minus cost). NULL until a merchant sets it; never negative when present.

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

      • daypartsarray of DaypartWindow

        Time-of-day windows a product is sold in, in minutes since midnight in the location's timezone. A product with no windows is sold all day; one with windows is shown on the till only while the location's clock is inside a window. A window may cross midnight (start > end). A window opens on the weekdays in days (1 = Monday .. 7 = Sunday); an empty or absent days is a daily window on every day.

        ≤ 8 items

        • daysarray of integer

          The weekdays this window opens on, 1 (Monday) through 7 (Sunday). An empty or absent list is every day.

          ≤ 7 items

        • end_minuteinteger

          Required

          1–1440

        • start_minuteinteger

          Required

          0–1439

      • descriptionstring

        ≤ 2048 chars

      • image_urlstring

        A URL to a photo of the product, shown on the till menu and in the back office.

        ≤ 2048 chars

      • location_idsarray of string · uuid

        Locations at which this product is sold. A device only syncs products available at its own location.

      • location_sortsarray of ProductLocationSort

        Arrangement overrides — where this product sits within its category at one site (menu item 12, docs/decisions/0121). The base sort_order applies where no entry exists, so a site may put desserts before mains and another not; a location appears at most once, and a device never writes one.

        • location_idstring · uuid

          Required

          The site this arrangement applies at.

        • sort_orderinteger · int32

          Required

          Where the product sits within its category at this site, lower first; equal orders sort by name.

          ≥ 0

      • minimum_ageinteger · int32

        The youngest buyer the law allows for this product, in years (retail item: age-restriction prompts). 0 is no restriction, so a shop only sets this for the off-licence shelf. The till asks the operator to confirm the guest is at at least this old before a sale carrying the product completes, and the sale records the confirmation (the order's age_confirmed and age_minimum). Off by default; a presentation of the product, never a required entry.

        0–100 · default 0

      • modifier_group_idsarray of string · uuid

        The modifier groups this product offers, in menu order. A product may belong to several groups (milk, extras) and a group may be shared across products.

      • multibuyMultibuyRule

        Nullable

        The product's multibuy deal, or null for none: buy every units and the last unit of each run is charged at the rule's price instead of the line's. Counted per line on one invoice (retail promotions). The rule rides a sale line as a snapshot, so changing it later rewrites nothing already sold.

        • everyinteger

          Required

          How many units make a run; 3 is the ordinary buy-two-get-one.

          2–1000

        • priceMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

      • online_location_idsarray of string · uuid

        The locations, from location_ids, whose online menu offers this product while sold_online is on. A site can sell a product at the counter and keep it off its ordering page. Leave it out to keep the locations the product is already online at; send [] to take it offline everywhere.

      • packProductPack

        Nullable

        What one pack of this product holds, if the merchant counts it in packs; null when it is counted only in its own unit.

        • labelstring

          Required

          What a pack is called, for display — "bottle", "case", "bag". Never arithmetic.

          1–32 chars

        • sizeQuantity

          Required

          How much of something, as a decimal string in the unit_of_measure beside it (each where there is none), as Square's quantity is: three croissants are "3", 1.4 kg of beans is "1.4". A string because a JSON number is a binary float to most parsers.

          It has at most as many decimal places as its unit's base unit holds — three in each, g and ml, six in kg and l — and a finer one is refused, never rounded. Tenvelo holds every quantity as an integer of that base unit (thousandths of an each, milligrams, microlitres; docs/decisions/0063); a client that computes with one should read the digits the same way, never through a float.

          ≤ 26 chars · pattern ^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$

        • unitstring · enum

          Required

          The unit size is in.

          eachkgglml

      • perishableboolean

        Whether this product is perishable (retail item 10): a shop never takes one back, so a return of it is exempt from the returns window the shop set for goods. Off by default, and a presentation of the product, never a required entry.

        default false

      • prepared_overrideboolean

        Nullable

        The one product that is the exception to its category: the category is not prepared but this product is, or the reverse. null (or omitted) follows the category (kitchen-display item 10).

      • priceMoney

        Required

        A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

        The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

        Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

        • amountinteger · int64

          Required

          The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

        • currencyCurrency

          Required

          ISO 4217 currency code.

          Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

          One of 182 values; see the schema.

      • price_overridesarray of ProductLocationPrice

        Prices that override price. A product may be priced differently at one site than another, and on one channel than another (menu item 19): the most specific override — a location-and-channel one beats a location-only one — applies, then the base price. Each entry is a location (and optionally a channel) with a price; a (location, channel) pair may appear at most once. Set server-authoritative, like the rest of the catalogue.

        • channelstring

          The order channel this price applies to, or absent for every channel at that location.

        • effective_fromstring · date

          The date this price takes effect (locations-and-groups item 14). Absent means the price is in force now; a future date is a scheduled change every till receives ahead of time and resolves at its own business day's boundary. At most one future entry exists per location and channel.

        • effective_untilstring · date

          The last day this price is charged (retail promotions: dated sale prices). A location override with an end is a sale — "£2.50 each until Sunday" — and the resolver reverts to the price beneath it after that day. Absent means the override stands.

        • location_idstring · uuid

          Required

          The site this price applies at.

        • priceMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

      • price_schedulearray of ProductScheduledPrice

        Pending base-price changes (locations-and-groups item 14): the price the product sells at from a future date, resolved by the latest entry at or before the business date, falling back to price when none is in force. At most one future entry exists — a new schedule supersedes the previous one. price stays the price in force today.

        • effective_fromstring · date

          Required

          The business date this price takes effect on, in the location's own clock.

        • priceMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

      • recipeProductRecipe

        Nullable

        What this made item consumes (inventory item 13): one sold unit is made from its ingredients, each depleting its own stock level in the amount given, in the ingredient's own unit — 40 ml of vodka is "40". A product with a recipe is not stocked as itself. Only an item sold per each may carry one. null or absent means the product is stocked and sold as itself.

        • linesarray of object

          The ingredients of the made item, by product and amount per unit.

          ≥ 1 items

          • product_idstring · uuid

            Required

            The ingredient product, same merchant, and not the made item itself.

          • quantityQuantity

            Required

            How much of something, as a decimal string in the unit_of_measure beside it (each where there is none), as Square's quantity is: three croissants are "3", 1.4 kg of beans is "1.4". A string because a JSON number is a binary float to most parsers.

            It has at most as many decimal places as its unit's base unit holds — three in each, g and ml, six in kg and l — and a finer one is refused, never rounded. Tenvelo holds every quantity as an integer of that base unit (thousandths of an each, milligrams, microlitres; docs/decisions/0063); a client that computes with one should read the digits the same way, never through a float.

            ≤ 26 chars · pattern ^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$

          • unit_of_measurestring · enum

            The unit quantity is in, which must measure what the ingredient does. Absent means each.

            eachkgglml

      • skustring

        Merchant's own stock-keeping unit, if they use one.

        ≤ 64 chars

      • sold_onlineboolean

        Whether this product is offered on the online storefront, and so part of the location's public online menu. Off by default; a per-location 86 still withdraws it there.

        default false

      • sort_orderinteger · int32

        Where this product sits on the till's menu within its category. Lower comes first; equal orders sort by name.

        ≥ 0

      • statusstring · enum

        Required

        An inactive product stops appearing on tills but is never deleted, because orders already reference it and a receipt must remain reproducible.

        activeinactive

      • supplier_idstring · uuid

        Nullable

        The supplier this product is reordered from, if any: a suggested order groups products by it. Square calls the same link a variation's vendor (vendor_id); a product here has one.

      • tax_class_idstring · uuid

        Nullable

        The tax class this product belongs to, and the only way it is taxed. A class and a fulfilment type resolve to one or several rates through a tax rule, so a cold sandwich is zero-rated as takeaway and standard-rated eat-in without the menu being duplicated (docs/decisions/0014). Square lists an item's taxes directly (tax_ids); the class stands between them so the set can vary by fulfilment. Null means no tax applies — a deliberate statement, not an omission. A classed product with no rule for a fulfilment is a configuration error the back office shows, never a silent "no tax".

      • track_inventoryboolean

        Whether inventory is counted for this product. Coffee made to order is not tracked; a bag of beans on a shelf is.

        default false

      • track_serialboolean

        Whether the product's units carry serial numbers (retail item: serial numbers): each unit is one physical thing with its own code, and the till asks for it when the product is rung up, recording it on the line. Off by default; a presentation of the product, never a required entry.

        default false

      • translationsobject

        The product's name and description in other languages (0017, Merchant content; menu item 20), keyed by BCP 47 tag (es, fr, en-GB). The base name and description are the source language. They ride the change feed and the ETag like any catalogue field, and a guest reading the product is answered in the language they asked for — never the base text in a language they did not ask for. Each entry carries a source_digest the server computed from the base fields at write time; the back office compares it to a later edit and flags a translation left behind.

      • unit_of_measurestring · enum

        The unit the product is sold and counted in (inventory item 16). A café or bar that stocks by weight or volume names the unit so a fractional quantity is meaningful. Defaults to each.

        eachkgglml

        default each

      • variantsarray of object

        The named options a product is sold as — a size, a strength. Each carries its own absolute price, which replaces the base price when chosen. A product with variants requires one to be picked; the till offers them before the line is rung up.

        ≤ 50 items

        • idstring · uuid

          Required

        • namestring

          Required

          1–64 chars

        • barcodesarray of string

          The barcodes this variant can be scanned or searched as, in addition to the product's own. Each entry must be non-blank and unique; a code may not repeat across a product and its variants, because a scan must name one sellable unit. The till matches any of them.

        • costMoney

          Nullable

          What this variant costs the merchant, in the variant's currency, when one is known. A sale of the variant snapshots this rather than the product's cost, so a later cost change does not restate a past period. NULL until a merchant sets it; never negative when present.

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

        • priceMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

        • skustring

          The merchant's own stock-keeping unit for this variant, if they use one. The till's search matches it like the product's own SKU.

          ≤ 64 chars

    • tax_ratesarray of TaxRate
      • idstring · uuid

        Required

      • kindstring · enum

        What the rate is: percentage of the price, or per_unit — a fixed amount per unit sold, for a levy that is not a percentage: a bag levy, a bottle deposit, a per-drink US tax (markets item 16). A per-unit rate carries per_unit and a percentage of zero; it is charged amount × quantity, exact, on top of the price, never compounding, and only with inclusive: false. Nothing else changes: it is a rate like any other, named by a rule per class and fulfilment type.

        percentageper_unit

        default percentage

      • namestring

        Required

        1–128 chars

      • effective_fromstring · date

        The date the rate takes effect, as a calendar date. A rate change is scheduled by creating a rate (and a rule naming it) with an effective_from in the future: it reaches every till on the change feed ahead of time, and a sale is taxed at the rate in force on its business date, not the current one (docs/decisions/0014).

      • inclusiveboolean

        Required

        Whether the tax is already contained in the product's price (true, the norm in the UK and EU) or added at the till (false, the norm in the US). This changes how a total is computed, so it is required rather than defaulted.

      • per_unitMoney

        A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

        The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

        Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

        • amountinteger · int64

          Required

          The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

        • currencyCurrency

          Required

          ISO 4217 currency code.

          Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

          One of 182 values; see the schema.

      • percentagePercentage

        Required

        A share of a whole, from "0" to "100", as a percentage in a decimal string with at most four decimal places, as Square's percentage and Stripe Tax's percentage_decimal are: UK VAT at 20% is "20", and an 8.875% US sales tax is "8.875".

        A string because a JSON number is a binary float to most parsers, and a float cannot hold most decimal fractions exactly (RFC 7493 §2.2). Tenvelo computes with a percentage as an integer in parts per million ("8.875" is 88750), which every four-place percentage is exactly. A client that computes with one should read the digits the same way, never through a float.

        pattern ^(100(\.0{1,4})?|(0|[1-9][0-9]?)(\.[0-9]{1,4})?)$

      • roundingstring · enum

        How this rate's pennies round. A rate computes on its own base and rounds before the set a rule names is summed — each tax a line carries is a separate calculation with the mode its authority prescribes. half_up is the norm; half_even is what some US states ask for (banker's rounding).

        half_uphalf_even

        default half_up

      • statusstring · enum

        Required

        activeinactive

    Responses

    • 202

      The import was accepted and queued. Poll the job at Location.

      Job · application/json
      • idstring · uuid

        Required

      • typeJobType

        Required

        What a job is doing, named for what started it: a catalog_import is created by POST /v1/catalog-imports. report_export and settlement_run are reserved for work not built yet.

        catalog_importreport_exportsettlement_run

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

      • finished_atstring · date-time

        Nullable

      • problemProblem

        Nullable

        Present when status is failed.

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

      • progressobject

        Nullable

        Best-effort progress. Advisory only — total_units may be revised upward as work is discovered, so never treat completed_units == total_units as completion. status is the only authority on whether the job is done.

        • completed_unitsinteger · int64

          Required

          Units of work finished so far.

          ≥ 0

        • total_unitsinteger · int64

          Required

          Units of work known about. Named total_units rather than total because a bare total beside monetary fields reads as an amount, and in an API where every other total is money that ambiguity is worth spending a word on.

          ≥ 0

      • resultobject

        Nullable

        What the job produced, present when status is succeeded. For an export this carries a time-limited download URL.

      • started_atstring · date-time

        Nullable

      • statusstring · enum

        Required

        pending — accepted and queued, not yet picked up by a worker.

        running — in progress; progress may be populated.

        succeeded — finished; result is populated.

        failed — finished unsuccessfully; problem explains why. Terminal: the job will not be retried on its own, though the request may be re-submitted.

        canceled — stopped before it finished. Terminal.

        pendingrunningsucceededfailedcanceled

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

  4. GET/v1/categories

    List categories

    No. listCategoriesAuth catalog:read

    Returns the merchant's product categories in display order.

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

      CategoryPage · application/json
      • has_moreboolean

        Required

        Whether another page follows.

      • itemsarray of Category

        Required

        • idstring · uuid

          Required

        • namestring

          Required

          1–128 chars

        • colorstring · enum

          How this category is coloured on a till.

          A fixed set rather than a free colour value. A merchant given a colour picker will eventually choose two nobody can tell apart, or one that fails contrast against the till's dark ground, and the failure lands on staff mid-service rather than on whoever picked it.

          The hues are Okabe–Ito, chosen because they stay distinguishable under the common forms of colour blindness — roughly one man in twelve, which across a merchant's whole staff is a certainty rather than a risk. Colour is always redundant with the category's name: it makes a known category faster to find, and never carries meaning on its own.

          graybrownorangeyellowgreenskybluepurple

          default gray

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

        • descriptionstring

          ≤ 1024 chars

        • merchant_idstring · uuid

          RequiredRead only

        • parent_idstring · uuid

          Nullable

          The category this one sits under, or null for a top-level category.

          Categories form a tree because that is how a till is laid out: staff drill from Drinks to Hot to Coffee rather than hunting a flat grid of several hundred products. Tapping a category shows its sub-categories and any products attached directly to it, so a merchant is not forced to invent a Misc child for the two items that do not fit a sub-category.

          Nesting is capped at 20 levels. No real menu approaches that; the cap exists so a mistake cannot produce a tree that is unbounded to walk. A cycle — making a category a descendant of itself — is rejected with 422, because a till navigating one would loop forever.

        • preparedboolean

          False marks this category not prepared: its lines make no kitchen ticket and no printed round — a can of Coke rung at a table must not sit on the pass next to the burgers. True by default; a location that marks nothing keeps its current behaviour (kitchen-display item 10). A product may override it (prepared_override).

          default true

        • sort_orderinteger · int32

          Ascending display order on the till. Ties break on name.

          ≥ 0 · default 0

        • statusstring · enum

          Required

          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.

        • versionVersion

          RequiredRead only

          Optimistic concurrency version, raised by the server on every write. A version is never lower than one already returned for the resource, though it may rise by more than one.

          Returned in the ETag response header as well as in the body. Send it back in If-Match on an update and the write is rejected with 412 if anyone else has written since — which is what stops two tills silently overwriting each other's edit to the same product when both come back online at once.

          ≥ 1

      • next_cursorstring

        Nullable

        Opaque cursor for the next page, or null on the last page. Pass it back verbatim as the cursor query parameter; its contents are not part of this contract.

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  5. POST/v1/categories/batch-upsert

    Create or replace categories

    No. batchUpsertCategoriesAuth catalog:write

    Creates categories at client-minted ids, or replaces existing ones at the version they were read at. A batch of one is how a single category is created.

    Each item carries its own idempotency_key and stands or falls on its own: an invalid item, or one written from a stale version, fails in its result and every other item is still applied. Only a malformed envelope refuses the request as a whole. Every change reaches devices through the change feed.

    Request body application/json · CategoryBatchUpsertRequest

    • itemsarray of CategoryBatchUpsertItem

      Required

      ≥ 1 items · ≤ 500 items

      • categoryCategory

        Required

        A grouping of products, used to lay out the till screen and to break down sales reporting.

        • idstring · uuid

          Required

        • namestring

          Required

          1–128 chars

        • colorstring · enum

          How this category is coloured on a till.

          A fixed set rather than a free colour value. A merchant given a colour picker will eventually choose two nobody can tell apart, or one that fails contrast against the till's dark ground, and the failure lands on staff mid-service rather than on whoever picked it.

          The hues are Okabe–Ito, chosen because they stay distinguishable under the common forms of colour blindness — roughly one man in twelve, which across a merchant's whole staff is a certainty rather than a risk. Colour is always redundant with the category's name: it makes a known category faster to find, and never carries meaning on its own.

          graybrownorangeyellowgreenskybluepurple

          default gray

        • descriptionstring

          ≤ 1024 chars

        • parent_idstring · uuid

          Nullable

          The category this one sits under, or null for a top-level category.

          Categories form a tree because that is how a till is laid out: staff drill from Drinks to Hot to Coffee rather than hunting a flat grid of several hundred products. Tapping a category shows its sub-categories and any products attached directly to it, so a merchant is not forced to invent a Misc child for the two items that do not fit a sub-category.

          Nesting is capped at 20 levels. No real menu approaches that; the cap exists so a mistake cannot produce a tree that is unbounded to walk. A cycle — making a category a descendant of itself — is rejected with 422, because a till navigating one would loop forever.

        • preparedboolean

          False marks this category not prepared: its lines make no kitchen ticket and no printed round — a can of Coke rung at a table must not sit on the pass next to the burgers. True by default; a location that marks nothing keeps its current behaviour (kitchen-display item 10). A product may override it (prepared_override).

          default true

        • sort_orderinteger · int32

          Ascending display order on the till. Ties break on name.

          ≥ 0 · default 0

        • statusstring · enum

          Required

          activeinactive

      • idempotency_keyItemIdempotencyKey

        Required

        A key for one item in a batch, minted by the client when it queues the item and reused on every attempt to send it. The item, not the request, is the unit of work: a retried batch returns the stored result for every item already applied under its key. A fresh key per attempt defeats the whole mechanism.

        Scoped per merchant, so keys need only be unique within a merchant's own traffic.

        16–255 chars

      • versioninteger · int64

        The version this write was made from. Required to replace a category that exists, and refused as 412 when somebody else has written it since.

        ≥ 1

    Responses

    • 200

      The batch was processed. results says what happened to each item.

      BatchResponse · application/json
      • resultsarray of BatchResult

        Required

        • idstring · uuid

          The resource the item created or changed. Absent when it failed.

        • idempotency_keystring

          Required

          The item's key, echoed so a result can be matched by key as well as position.

        • problemProblem

          Nullable

          Why the item failed, as an RFC 9457 problem. A 422 means the item is malformed and will never succeed — drop it and show it to staff rather than retrying for ever. A 409 or a 5xx means try again later.

          • typestring · uri

            Required

            A stable URI identifying the problem kind. This is the field to branch on.

            default about:blank

          • api_versionstring

            Present on an api-version-retired or api-version-unknown refusal: the API-Version this server speaks (docs/decisions/0011).

          • detailstring

            A human-readable explanation specific to this occurrence. Safe to show to a developer; never contains internal state, SQL, or stack traces.

          • instancestring · uri-reference

            A URI reference identifying the specific occurrence.

          • invalid_paramsarray of InvalidParam

            Present on 422 responses. Lists every field that failed validation, rather than only the first, so a client can highlight an entire form in one pass.

            • codestring

              Stable machine-readable code for this failure, for clients that localise their own messages rather than showing reason.

            • pointerstring

              Required

              RFC 6901 JSON Pointer to the offending value within the request body. A pointer rather than a field name so that failures nested inside arrays — the third line item of an order — are addressable at all.

            • reasonstring

              Required

              Why this value was rejected, in human-readable form.

          • refusalsarray of Location refusal

            Present on 409 responses to a withdrawal refused by the state it depends on (deleting a location with tills still assigned, open orders, …). Lists every reason at once, each with how much stands in the way, so a client can say what to put right and link to it rather than walking a person through one refusal after another.

            • kindstring

              Required

              A stable refusal kind — devices, open_orders, drawer_sessions, bookings or stock — that a client links to the screen that puts it right. Branch on this field, never on the surrounding prose.

            • countinteger

              Required

              How many things stand in the way of the withdrawal, so a client can repeat the number rather than re-read prose.

              ≥ 1

          • statusinteger

            Required

            The HTTP status code, repeated here so the body is self-contained.

            100–599

          • titlestring

            Required

            A short, human-readable summary of the problem kind.

          • trace_idstring

            The OpenTelemetry trace id for this request. Quote it in a support ticket and the whole request can be pulled up.

        • statusstring · enum

          Required

          created — the item made a new resource. updated — it changed an existing one.

          unchanged — this key was already applied and the original outcome stands. A success, not an error: it is what a client retrying a batch it was unsure landed should expect, and it clears the item from its queue exactly as created does.

          failed — the item was refused, and nothing else in the batch was affected. See problem.

          createdupdatedunchangedfailed

        • versioninteger · int64

          The resource's version after the item. Absent when it failed.

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 415

      The request Content-Type is not supported. This API accepts application/json on all mutations.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  6. GET/v1/categories/{id}

    Get a category

    No. getCategoryAuth catalog:read

    Returns a single category.

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

      Category · application/json
      • idstring · uuid

        Required

      • namestring

        Required

        1–128 chars

      • colorstring · enum

        How this category is coloured on a till.

        A fixed set rather than a free colour value. A merchant given a colour picker will eventually choose two nobody can tell apart, or one that fails contrast against the till's dark ground, and the failure lands on staff mid-service rather than on whoever picked it.

        The hues are Okabe–Ito, chosen because they stay distinguishable under the common forms of colour blindness — roughly one man in twelve, which across a merchant's whole staff is a certainty rather than a risk. Colour is always redundant with the category's name: it makes a known category faster to find, and never carries meaning on its own.

        graybrownorangeyellowgreenskybluepurple

        default gray

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

      • descriptionstring

        ≤ 1024 chars

      • merchant_idstring · uuid

        RequiredRead only

      • parent_idstring · uuid

        Nullable

        The category this one sits under, or null for a top-level category.

        Categories form a tree because that is how a till is laid out: staff drill from Drinks to Hot to Coffee rather than hunting a flat grid of several hundred products. Tapping a category shows its sub-categories and any products attached directly to it, so a merchant is not forced to invent a Misc child for the two items that do not fit a sub-category.

        Nesting is capped at 20 levels. No real menu approaches that; the cap exists so a mistake cannot produce a tree that is unbounded to walk. A cycle — making a category a descendant of itself — is rejected with 422, because a till navigating one would loop forever.

      • preparedboolean

        False marks this category not prepared: its lines make no kitchen ticket and no printed round — a can of Coke rung at a table must not sit on the pass next to the burgers. True by default; a location that marks nothing keeps its current behaviour (kitchen-display item 10). A product may override it (prepared_override).

        default true

      • sort_orderinteger · int32

        Ascending display order on the till. Ties break on name.

        ≥ 0 · default 0

      • statusstring · enum

        Required

        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.

      • versionVersion

        RequiredRead only

        Optimistic concurrency version, raised by the server on every write. A version is never lower than one already returned for the resource, though it may rise by more than one.

        Returned in the ETag response header as well as in the body. Send it back in If-Match on an update and the write is rejected with 412 if anyone else has written since — which is what stops two tills silently overwriting each other's edit to the same product when both come back online at once.

        ≥ 1

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 404

      No such resource, or none visible to this caller.

      Also returned instead of 403 where acknowledging existence would itself leak information — so a 404 does not prove the id is unused globally, only that it is not yours.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  7. PATCH/v1/categories/{id}

    Change a category

    No. updateCategoryAuth catalog:write

    Applies a JSON Merge Patch (RFC 7396) to a category, guarded by If-Match.

    Send only the fields that change. The patch is refused with 412 if the category has changed since the version you read: re-read, re-apply your change on top of the current state, and retry — do not resend with If-Match: *, which is what silently loses a colleague's edit.

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

    • namestring

      1–128 chars

    • colorstring · enum

      graybrownorangeyellowgreenskybluepurple

    • descriptionstring

      Nullable

      ≤ 1024 chars

    • parent_idstring · uuid

      Nullable

      The new parent, or null to make the category top-level.

    • sort_orderinteger · int32

      ≥ 0

    • statusstring · enum

      activeinactive

    Responses

    • 200

      The changed category.

      Category · application/json
      • idstring · uuid

        Required

      • namestring

        Required

        1–128 chars

      • colorstring · enum

        How this category is coloured on a till.

        A fixed set rather than a free colour value. A merchant given a colour picker will eventually choose two nobody can tell apart, or one that fails contrast against the till's dark ground, and the failure lands on staff mid-service rather than on whoever picked it.

        The hues are Okabe–Ito, chosen because they stay distinguishable under the common forms of colour blindness — roughly one man in twelve, which across a merchant's whole staff is a certainty rather than a risk. Colour is always redundant with the category's name: it makes a known category faster to find, and never carries meaning on its own.

        graybrownorangeyellowgreenskybluepurple

        default gray

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

      • descriptionstring

        ≤ 1024 chars

      • merchant_idstring · uuid

        RequiredRead only

      • parent_idstring · uuid

        Nullable

        The category this one sits under, or null for a top-level category.

        Categories form a tree because that is how a till is laid out: staff drill from Drinks to Hot to Coffee rather than hunting a flat grid of several hundred products. Tapping a category shows its sub-categories and any products attached directly to it, so a merchant is not forced to invent a Misc child for the two items that do not fit a sub-category.

        Nesting is capped at 20 levels. No real menu approaches that; the cap exists so a mistake cannot produce a tree that is unbounded to walk. A cycle — making a category a descendant of itself — is rejected with 422, because a till navigating one would loop forever.

      • preparedboolean

        False marks this category not prepared: its lines make no kitchen ticket and no printed round — a can of Coke rung at a table must not sit on the pass next to the burgers. True by default; a location that marks nothing keeps its current behaviour (kitchen-display item 10). A product may override it (prepared_override).

        default true

      • sort_orderinteger · int32

        Ascending display order on the till. Ties break on name.

        ≥ 0 · default 0

      • statusstring · enum

        Required

        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.

      • versionVersion

        RequiredRead only

        Optimistic concurrency version, raised by the server on every write. A version is never lower than one already returned for the resource, though it may rise by more than one.

        Returned in the ETag response header as well as in the body. Send it back in If-Match on an update and the write is rejected with 412 if anyone else has written since — which is what stops two tills silently overwriting each other's edit to the same product when both come back online at once.

        ≥ 1

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 404

      No such resource, or none visible to this caller.

      Also returned instead of 403 where acknowledging existence would itself leak information — so a 404 does not prove the id is unused globally, only that it is not yours.

    • 409

      The request conflicts with current state — a uniqueness constraint, or an attempt to mutate an order that is already completed or voided.

      Unlike 412, re-reading and retrying unchanged will not resolve it; the request itself has to change.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 412

      The If-Match version did not match the resource's current version: someone else wrote since you read.

      This is the expected, healthy outcome of two clients editing the same record — not an error condition to suppress. Re-read the resource, re-apply your change on top of the new state, and retry with the fresh ETag.

    • 415

      The request Content-Type is not supported. This API accepts application/json on all mutations.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  8. DELETE/v1/categories/{id}

    Delete a category

    No. deleteCategoryAuth catalog:write

    Soft-deletes a category. Products in it are not deleted; they become uncategorised, each by a change of its own on the feed, shortly after the deletion. A category with categories beneath it, or one a combo offers a choice from, is refused with 409; nothing may be filed under, offered from or placed beneath a deleted category.

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

  9. GET/v1/field-policies

    List field policies

    No. listFieldPoliciesAuth catalog:read

    Returns the merchant's field policies, optionally filtered to one location. A field policy is the merchant's own statement about a catalogue field (field-policies item 2); reading it is how an integration learns the merchant's requirements without being refused a write.

    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.

    • location_idquery · string · uuid

      Only the policies scoped to this location.

    Responses

    • 200

      The field policies.

      FieldPolicyPage · application/json
      • has_moreboolean

        Required

        Whether another page follows.

      • itemsarray of FieldPolicy

        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.

        • fieldstring

          Required

          The field the rule is about — e.g. sku.

        • location_idstring · uuid

          Nullable

          The location the rule is scoped to. Null applies the rule to every location.

        • resourcestring

          Required

          The resource the rule is about — e.g. product.

        • rulestring · enum

          Required

          required insists the field is non-empty; unique insists no other row of the resource carries the same value for the field.

          requiredunique

        • statestring · enum

          Required

          What the rule does when a write breaks it.

          offwarnenforce

        • updated_atTimestamp

          RequiredRead only

          Server-assigned RFC 3339 timestamp, always UTC.

          Server-assigned because device clocks drift and cannot be trusted for ordering — a till that has been powered off for a week may come back with a clock hours out. Where the device's own view of time matters it is carried separately in client_created_at.

        • versionVersion

          RequiredRead only

          Optimistic concurrency version, raised by the server on every write. A version is never lower than one already returned for the resource, though it may rise by more than one.

          Returned in the ETag response header as well as in the body. Send it back in If-Match on an update and the write is rejected with 412 if anyone else has written since — which is what stops two tills silently overwriting each other's edit to the same product when both come back online at once.

          ≥ 1

      • next_cursorstring

        Nullable

        Opaque cursor for the next page, or null on the last page. Pass it back verbatim as the cursor query parameter; its contents are not part of this contract.

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 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/field-policies/batch-create

    Add field policies

    No. batchCreateFieldPoliciesAuth catalog:write

    Adds rules at client-minted ids. A rule is never edited: its state moves with a patch, and a different rule is a new one, so this only creates. A rule already held for the same field and scope fails in its own result, as does a field the rule cannot measure.

    Each item carries its own idempotency_key and stands or falls on its own. Only a malformed envelope refuses the request as a whole.

    Request body application/json · FieldPolicyBatchCreateRequest

    • itemsarray of FieldPolicyBatchCreateItem

      Required

      ≥ 1 items · ≤ 500 items

      • field_policyFieldPolicy

        Required

        A field policy is a rule a merchant states about how a catalogue field must be filled in. It is additive and never touches the order path (decision 0050): it either warns — admitting the write and recording a violation the merchant can review — or enforces, refusing the write. off leaves it inert.

        • idstring · uuid

          Required

        • fieldstring

          Required

          The field the rule is about — e.g. sku.

        • location_idstring · uuid

          Nullable

          The location the rule is scoped to. Null applies the rule to every location.

        • resourcestring

          Required

          The resource the rule is about — e.g. product.

        • rulestring · enum

          Required

          required insists the field is non-empty; unique insists no other row of the resource carries the same value for the field.

          requiredunique

        • statestring · enum

          Required

          What the rule does when a write breaks it.

          offwarnenforce

      • idempotency_keyItemIdempotencyKey

        Required

        A key for one item in a batch, minted by the client when it queues the item and reused on every attempt to send it. The item, not the request, is the unit of work: a retried batch returns the stored result for every item already applied under its key. A fresh key per attempt defeats the whole mechanism.

        Scoped per merchant, so keys need only be unique within a merchant's own traffic.

        16–255 chars

    Responses

    • 200

      The batch was processed. results says what happened to each item.

      BatchResponse · application/json
      • resultsarray of BatchResult

        Required

        • idstring · uuid

          The resource the item created or changed. Absent when it failed.

        • idempotency_keystring

          Required

          The item's key, echoed so a result can be matched by key as well as position.

        • problemProblem

          Nullable

          Why the item failed, as an RFC 9457 problem. A 422 means the item is malformed and will never succeed — drop it and show it to staff rather than retrying for ever. A 409 or a 5xx means try again later.

          • typestring · uri

            Required

            A stable URI identifying the problem kind. This is the field to branch on.

            default about:blank

          • api_versionstring

            Present on an api-version-retired or api-version-unknown refusal: the API-Version this server speaks (docs/decisions/0011).

          • detailstring

            A human-readable explanation specific to this occurrence. Safe to show to a developer; never contains internal state, SQL, or stack traces.

          • instancestring · uri-reference

            A URI reference identifying the specific occurrence.

          • invalid_paramsarray of InvalidParam

            Present on 422 responses. Lists every field that failed validation, rather than only the first, so a client can highlight an entire form in one pass.

            • codestring

              Stable machine-readable code for this failure, for clients that localise their own messages rather than showing reason.

            • pointerstring

              Required

              RFC 6901 JSON Pointer to the offending value within the request body. A pointer rather than a field name so that failures nested inside arrays — the third line item of an order — are addressable at all.

            • reasonstring

              Required

              Why this value was rejected, in human-readable form.

          • refusalsarray of Location refusal

            Present on 409 responses to a withdrawal refused by the state it depends on (deleting a location with tills still assigned, open orders, …). Lists every reason at once, each with how much stands in the way, so a client can say what to put right and link to it rather than walking a person through one refusal after another.

            • kindstring

              Required

              A stable refusal kind — devices, open_orders, drawer_sessions, bookings or stock — that a client links to the screen that puts it right. Branch on this field, never on the surrounding prose.

            • countinteger

              Required

              How many things stand in the way of the withdrawal, so a client can repeat the number rather than re-read prose.

              ≥ 1

          • statusinteger

            Required

            The HTTP status code, repeated here so the body is self-contained.

            100–599

          • titlestring

            Required

            A short, human-readable summary of the problem kind.

          • trace_idstring

            The OpenTelemetry trace id for this request. Quote it in a support ticket and the whole request can be pulled up.

        • statusstring · enum

          Required

          created — the item made a new resource. updated — it changed an existing one.

          unchanged — this key was already applied and the original outcome stands. A success, not an error: it is what a client retrying a batch it was unsure landed should expect, and it clears the item from its queue exactly as created does.

          failed — the item was refused, and nothing else in the batch was affected. See problem.

          createdupdatedunchangedfailed

        • versioninteger · int64

          The resource's version after the item. Absent when it failed.

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 415

      The request Content-Type is not supported. This API accepts application/json on all mutations.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  11. GET/v1/field-policies/conformance

    Field-policy conformance

    No. getFieldPoliciesConformanceAuth catalog:read

    Counts, per field-policy rule, how many products would currently refuse a write and names them — the view that turns a warn rule from safe into useful (field-policies item 6). A read, so a manager can see progress before turning a rule on. A rule over a field a product does not carry yet is not counted.

    Responses

    • 200

      Per-rule conformance, one entry per rule in the merchant's set.

      array of FieldPolicyConformance · application/json
      • fieldstring

        Required

        The field the rule is about — e.g. barcode.

      • location_idstring · uuid

        The location the rule is scoped to; absent means every location.

      • policy_idstring · uuid

        Required

        The rule this entry is for.

      • product_idsarray of string · uuid

        The same products' ids, in the same order, for going to fix each.

      • productsarray of string

        The names of the products that would refuse a write, for naming the problem.

      • resourcestring · enum

        Required

        product

      • rulestring · enum

        Required

        requiredunique

      • statestring · enum

        Required

        offwarnenforce

      • violatedinteger · int64

        Required

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

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

  12. PATCH/v1/field-policies/{id}

    Change a field policy

    No. updateFieldPolicyAuth catalog:write

    Applies a JSON Merge Patch (RFC 7396) to a field policy, guarded by If-Match.

    Moving a rule between off, warn and enforce is this patch. It is refused with 412 if the policy has changed since the version you read: two back-office users changing the same rule must not silently overwrite each other. Re-read, re-apply your change on top of the current state, and retry.

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

    • statestring · enum

      offwarnenforce

    Responses

    • 200

      The changed field policy.

      FieldPolicy · application/json
      • 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.

      • fieldstring

        Required

        The field the rule is about — e.g. sku.

      • location_idstring · uuid

        Nullable

        The location the rule is scoped to. Null applies the rule to every location.

      • resourcestring

        Required

        The resource the rule is about — e.g. product.

      • rulestring · enum

        Required

        required insists the field is non-empty; unique insists no other row of the resource carries the same value for the field.

        requiredunique

      • statestring · enum

        Required

        What the rule does when a write breaks it.

        offwarnenforce

      • updated_atTimestamp

        RequiredRead only

        Server-assigned RFC 3339 timestamp, always UTC.

        Server-assigned because device clocks drift and cannot be trusted for ordering — a till that has been powered off for a week may come back with a clock hours out. Where the device's own view of time matters it is carried separately in client_created_at.

      • versionVersion

        RequiredRead only

        Optimistic concurrency version, raised by the server on every write. A version is never lower than one already returned for the resource, though it may rise by more than one.

        Returned in the ETag response header as well as in the body. Send it back in If-Match on an update and the write is rejected with 412 if anyone else has written since — which is what stops two tills silently overwriting each other's edit to the same product when both come back online at once.

        ≥ 1

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 404

      No such resource, or none visible to this caller.

      Also returned instead of 403 where acknowledging existence would itself leak information — so a 404 does not prove the id is unused globally, only that it is not yours.

    • 409

      The request conflicts with current state — a uniqueness constraint, or an attempt to mutate an order that is already completed or voided.

      Unlike 412, re-reading and retrying unchanged will not resolve it; the request itself has to change.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 412

      The If-Match version did not match the resource's current version: someone else wrote since you read.

      This is the expected, healthy outcome of two clients editing the same record — not an error condition to suppress. Re-read the resource, re-apply your change on top of the new state, and retry with the fresh ETag.

    • 415

      The request Content-Type is not supported. This API accepts application/json on all mutations.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  13. DELETE/v1/field-policies/{id}

    Delete a field policy

    No. deleteFieldPolicyAuth catalog:write

    Removes a field policy, guarded by If-Match. A rule is the merchant's own statement and nothing references it, so it is erased rather than soft-deleted. To stop a rule acting without losing it, patch its state to off instead — that is the usual intent.

    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 policy is gone.

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

  14. GET/v1/field-policy-violations

    List field-policy violations

    No. listFieldPolicyViolationsAuth catalog:read

    Returns the writes a rule let through although they broke it, newest first: what a warn rule caught, so a merchant can fix the gaps before enforcing.

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

      FieldPolicyViolationPage · application/json
      • has_moreboolean

        Required

      • itemsarray of FieldPolicyViolation

        Required

        • idstring · uuid

          Required

        • fieldstring

          Required

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

        • policy_idstring · uuid

          Required

          The rule that was broken.

        • reasonstring

          Why an enforce rule was overridden; empty when a warn rule let the write through.

        • resourcestring · enum

          Required

          productproduct_variant

        • resource_idstring · uuid

          Required

          The product, or variant, the write was to.

        • rulestring · enum

          Required

          requiredunique

        • statestring · enum

          Required

          What the rule was doing when the write broke it.

          offwarnenforce

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

  15. GET/v1/modifier-groups

    List modifier groups

    No. listModifierGroupsAuth catalog:read

    Returns a page of the merchant's modifier groups, in menu order.

    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

      The merchant's modifier groups.

      ModifierGroupPage · application/json
      • has_moreboolean

        Required

        Whether another page follows.

      • itemsarray of ModifierGroup

        Required

        • idstring · uuid

          Required

        • namestring

          Required

          1–255 chars

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

        • max_selectionsinteger · int32

          Required

          Maximum number of options a guest may choose. Must be at least min_selections.

          ≥ 0 · default 1

        • merchant_idstring · uuid

          RequiredRead only

        • min_selectionsinteger · int32

          Required

          Minimum number of options a guest must choose.

          ≥ 0 · default 0

        • sort_orderinteger · int32

          Presentation order on the till.

          default 0

        • statusstring · enum

          Required

          An inactive group stops being offered on tills but is never deleted, because sales already reference its options and a receipt must remain reproducible.

          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.

        • versionVersion

          RequiredRead only

          Optimistic concurrency version, raised by the server on every write. A version is never lower than one already returned for the resource, though it may rise by more than one.

          Returned in the ETag response header as well as in the body. Send it back in If-Match on an update and the write is rejected with 412 if anyone else has written since — which is what stops two tills silently overwriting each other's edit to the same product when both come back online at once.

          ≥ 1

      • next_cursorstring

        Nullable

        Opaque cursor for the next page, or null on the last page. Pass it back verbatim as the cursor query parameter; its contents are not part of this contract.

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  16. POST/v1/modifier-groups/batch-upsert

    Create or replace modifier groups

    No. batchUpsertModifierGroupsAuth catalog:write

    Creates modifier groups at client-minted ids, or replaces existing ones at the version they were read at. A batch of one is how a single modifier group is created.

    Each item carries its own idempotency_key and stands or falls on its own: an invalid item, or one written from a stale version, fails in its result and every other item is still applied. Only a malformed envelope refuses the request as a whole. Every change reaches devices through the change feed.

    Request body application/json · ModifierGroupBatchUpsertRequest

    • itemsarray of ModifierGroupBatchUpsertItem

      Required

      ≥ 1 items · ≤ 500 items

      • idempotency_keyItemIdempotencyKey

        Required

        A key for one item in a batch, minted by the client when it queues the item and reused on every attempt to send it. The item, not the request, is the unit of work: a retried batch returns the stored result for every item already applied under its key. A fresh key per attempt defeats the whole mechanism.

        Scoped per merchant, so keys need only be unique within a merchant's own traffic.

        16–255 chars

      • modifier_groupModifierGroup

        Required

        A named set of choices a product may offer, e.g. "Milk" with whole, semi, skim and oat.

        Server-authoritative, like the catalogue: a merchant configures it in the dashboard and it reaches every till through the change feed. A product carrying a modifier is offered with these choices, and a sale's line snapshots which were chosen.

        • idstring · uuid

          Required

        • namestring

          Required

          1–255 chars

        • max_selectionsinteger · int32

          Required

          Maximum number of options a guest may choose. Must be at least min_selections.

          ≥ 0 · default 1

        • min_selectionsinteger · int32

          Required

          Minimum number of options a guest must choose.

          ≥ 0 · default 0

        • sort_orderinteger · int32

          Presentation order on the till.

          default 0

        • statusstring · enum

          Required

          An inactive group stops being offered on tills but is never deleted, because sales already reference its options and a receipt must remain reproducible.

          activeinactive

      • versioninteger · int64

        The version this write was made from. Required to replace one that exists, and refused as 412 when somebody else has written it since.

        ≥ 1

    Responses

    • 200

      The batch was processed. results says what happened to each item.

      BatchResponse · application/json
      • resultsarray of BatchResult

        Required

        • idstring · uuid

          The resource the item created or changed. Absent when it failed.

        • idempotency_keystring

          Required

          The item's key, echoed so a result can be matched by key as well as position.

        • problemProblem

          Nullable

          Why the item failed, as an RFC 9457 problem. A 422 means the item is malformed and will never succeed — drop it and show it to staff rather than retrying for ever. A 409 or a 5xx means try again later.

          • typestring · uri

            Required

            A stable URI identifying the problem kind. This is the field to branch on.

            default about:blank

          • api_versionstring

            Present on an api-version-retired or api-version-unknown refusal: the API-Version this server speaks (docs/decisions/0011).

          • detailstring

            A human-readable explanation specific to this occurrence. Safe to show to a developer; never contains internal state, SQL, or stack traces.

          • instancestring · uri-reference

            A URI reference identifying the specific occurrence.

          • invalid_paramsarray of InvalidParam

            Present on 422 responses. Lists every field that failed validation, rather than only the first, so a client can highlight an entire form in one pass.

            • codestring

              Stable machine-readable code for this failure, for clients that localise their own messages rather than showing reason.

            • pointerstring

              Required

              RFC 6901 JSON Pointer to the offending value within the request body. A pointer rather than a field name so that failures nested inside arrays — the third line item of an order — are addressable at all.

            • reasonstring

              Required

              Why this value was rejected, in human-readable form.

          • refusalsarray of Location refusal

            Present on 409 responses to a withdrawal refused by the state it depends on (deleting a location with tills still assigned, open orders, …). Lists every reason at once, each with how much stands in the way, so a client can say what to put right and link to it rather than walking a person through one refusal after another.

            • kindstring

              Required

              A stable refusal kind — devices, open_orders, drawer_sessions, bookings or stock — that a client links to the screen that puts it right. Branch on this field, never on the surrounding prose.

            • countinteger

              Required

              How many things stand in the way of the withdrawal, so a client can repeat the number rather than re-read prose.

              ≥ 1

          • statusinteger

            Required

            The HTTP status code, repeated here so the body is self-contained.

            100–599

          • titlestring

            Required

            A short, human-readable summary of the problem kind.

          • trace_idstring

            The OpenTelemetry trace id for this request. Quote it in a support ticket and the whole request can be pulled up.

        • statusstring · enum

          Required

          created — the item made a new resource. updated — it changed an existing one.

          unchanged — this key was already applied and the original outcome stands. A success, not an error: it is what a client retrying a batch it was unsure landed should expect, and it clears the item from its queue exactly as created does.

          failed — the item was refused, and nothing else in the batch was affected. See problem.

          createdupdatedunchangedfailed

        • versioninteger · int64

          The resource's version after the item. Absent when it failed.

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 415

      The request Content-Type is not supported. This API accepts application/json on all mutations.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  17. GET/v1/modifier-groups/{id}

    Get a modifier group

    No. getModifierGroupAuth catalog:read

    Returns a single modifier group.

    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 modifier group.

      ModifierGroup · application/json
      • idstring · uuid

        Required

      • namestring

        Required

        1–255 chars

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

      • max_selectionsinteger · int32

        Required

        Maximum number of options a guest may choose. Must be at least min_selections.

        ≥ 0 · default 1

      • merchant_idstring · uuid

        RequiredRead only

      • min_selectionsinteger · int32

        Required

        Minimum number of options a guest must choose.

        ≥ 0 · default 0

      • sort_orderinteger · int32

        Presentation order on the till.

        default 0

      • statusstring · enum

        Required

        An inactive group stops being offered on tills but is never deleted, because sales already reference its options and a receipt must remain reproducible.

        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.

      • versionVersion

        RequiredRead only

        Optimistic concurrency version, raised by the server on every write. A version is never lower than one already returned for the resource, though it may rise by more than one.

        Returned in the ETag response header as well as in the body. Send it back in If-Match on an update and the write is rejected with 412 if anyone else has written since — which is what stops two tills silently overwriting each other's edit to the same product when both come back online at once.

        ≥ 1

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 404

      No such resource, or none visible to this caller.

      Also returned instead of 403 where acknowledging existence would itself leak information — so a 404 does not prove the id is unused globally, only that it is not yours.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  18. PATCH/v1/modifier-groups/{id}

    Change a modifier group

    No. updateModifierGroupAuth catalog:write

    Applies a JSON Merge Patch (RFC 7396) to a modifier group, guarded by If-Match. Send only the fields that change; the patch is refused with 412 if the modifier group has changed since the version you read.

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

    • namestring

      1–255 chars

    • max_selectionsinteger · int32

      ≥ 0

    • min_selectionsinteger · int32

      ≥ 0

    • sort_orderinteger · int32
    • statusstring · enum

      activeinactive

    Responses

    • 200

      The changed modifier group.

      ModifierGroup · application/json
      • idstring · uuid

        Required

      • namestring

        Required

        1–255 chars

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

      • max_selectionsinteger · int32

        Required

        Maximum number of options a guest may choose. Must be at least min_selections.

        ≥ 0 · default 1

      • merchant_idstring · uuid

        RequiredRead only

      • min_selectionsinteger · int32

        Required

        Minimum number of options a guest must choose.

        ≥ 0 · default 0

      • sort_orderinteger · int32

        Presentation order on the till.

        default 0

      • statusstring · enum

        Required

        An inactive group stops being offered on tills but is never deleted, because sales already reference its options and a receipt must remain reproducible.

        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.

      • versionVersion

        RequiredRead only

        Optimistic concurrency version, raised by the server on every write. A version is never lower than one already returned for the resource, though it may rise by more than one.

        Returned in the ETag response header as well as in the body. Send it back in If-Match on an update and the write is rejected with 412 if anyone else has written since — which is what stops two tills silently overwriting each other's edit to the same product when both come back online at once.

        ≥ 1

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 404

      No such resource, or none visible to this caller.

      Also returned instead of 403 where acknowledging existence would itself leak information — so a 404 does not prove the id is unused globally, only that it is not yours.

    • 409

      The request conflicts with current state — a uniqueness constraint, or an attempt to mutate an order that is already completed or voided.

      Unlike 412, re-reading and retrying unchanged will not resolve it; the request itself has to change.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 412

      The If-Match version did not match the resource's current version: someone else wrote since you read.

      This is the expected, healthy outcome of two clients editing the same record — not an error condition to suppress. Re-read the resource, re-apply your change on top of the new state, and retry with the fresh ETag.

    • 415

      The request Content-Type is not supported. This API accepts application/json on all mutations.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  19. DELETE/v1/modifier-groups/{id}

    Delete a modifier group

    No. deleteModifierGroupAuth catalog:write

    Soft-deletes a modifier group, emitting a tombstone on the change feed. Sales already using its options remain reproducible, because each line snapshots what was chosen.

    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 modifier group 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.

  20. GET/v1/modifiers

    List modifiers

    No. listModifiersAuth catalog:read

    Returns a page of the merchant's modifiers, in menu order. Pass modifier_group_id for the choices in one group — the milk options, say — which is how a screen lays a group out; a group that does not exist has no modifiers, so the page is empty.

    Parameters

    • modifier_group_idquery · string · uuid

      Only the modifiers in this group.

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

      ModifierPage · application/json
      • has_moreboolean

        Required

        Whether another page follows.

      • itemsarray of Modifier

        Required

        • idstring · uuid

          Required

        • namestring

          Required

          1–255 chars

        • allergensarray of string

          The allergens this modifier contains, as the merchant names them (e.g. "nuts", "dairy"). Shown on the till when choosing the modifier, and used by the allergen check.

          ≤ 20 items

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

        • merchant_idstring · uuid

          RequiredRead only

        • modifier_group_idstring · uuid

          Required

          The group this modifier belongs to.

        • price_deltaMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

        • sort_orderinteger · int32

          Presentation order within the group.

          default 0

        • statusstring · enum

          Required

          An inactive modifier stops being offered but is never deleted.

          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.

        • versionVersion

          RequiredRead only

          Optimistic concurrency version, raised by the server on every write. A version is never lower than one already returned for the resource, though it may rise by more than one.

          Returned in the ETag response header as well as in the body. Send it back in If-Match on an update and the write is rejected with 412 if anyone else has written since — which is what stops two tills silently overwriting each other's edit to the same product when both come back online at once.

          ≥ 1

      • next_cursorstring

        Nullable

        Opaque cursor for the next page, or null on the last page. Pass it back verbatim as the cursor query parameter; its contents are not part of this contract.

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  21. POST/v1/modifiers/batch-upsert

    Create or replace modifiers

    No. batchUpsertModifiersAuth catalog:write

    Creates modifiers at client-minted ids, or replaces existing ones at the version they were read at. A batch of one is how a single modifier is created.

    Each item carries its own idempotency_key and stands or falls on its own: an invalid item, or one written from a stale version, fails in its result and every other item is still applied. Only a malformed envelope refuses the request as a whole. Every change reaches devices through the change feed.

    Request body application/json · ModifierBatchUpsertRequest

    • itemsarray of ModifierBatchUpsertItem

      Required

      ≥ 1 items · ≤ 500 items

      • idempotency_keyItemIdempotencyKey

        Required

        A key for one item in a batch, minted by the client when it queues the item and reused on every attempt to send it. The item, not the request, is the unit of work: a retried batch returns the stored result for every item already applied under its key. A fresh key per attempt defeats the whole mechanism.

        Scoped per merchant, so keys need only be unique within a merchant's own traffic.

        16–255 chars

      • modifierModifier

        Required

        One choice within a modifier group — "oat milk", "extra shot" — with the price delta it adds.

        The delta is in the merchant's currency and is added to the line's unit price at the till. It is a snapshot never re-resolved here, so editing a menu cannot rewrite what a past sale was charged.

        • idstring · uuid

          Required

        • namestring

          Required

          1–255 chars

        • allergensarray of string

          The allergens this modifier contains, as the merchant names them (e.g. "nuts", "dairy"). Shown on the till when choosing the modifier, and used by the allergen check.

          ≤ 20 items

        • modifier_group_idstring · uuid

          Required

          The group this modifier belongs to.

        • price_deltaMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

        • sort_orderinteger · int32

          Presentation order within the group.

          default 0

        • statusstring · enum

          Required

          An inactive modifier stops being offered but is never deleted.

          activeinactive

      • versioninteger · int64

        The version this write was made from. Required to replace one that exists, and refused as 412 when somebody else has written it since.

        ≥ 1

    Responses

    • 200

      The batch was processed. results says what happened to each item.

      BatchResponse · application/json
      • resultsarray of BatchResult

        Required

        • idstring · uuid

          The resource the item created or changed. Absent when it failed.

        • idempotency_keystring

          Required

          The item's key, echoed so a result can be matched by key as well as position.

        • problemProblem

          Nullable

          Why the item failed, as an RFC 9457 problem. A 422 means the item is malformed and will never succeed — drop it and show it to staff rather than retrying for ever. A 409 or a 5xx means try again later.

          • typestring · uri

            Required

            A stable URI identifying the problem kind. This is the field to branch on.

            default about:blank

          • api_versionstring

            Present on an api-version-retired or api-version-unknown refusal: the API-Version this server speaks (docs/decisions/0011).

          • detailstring

            A human-readable explanation specific to this occurrence. Safe to show to a developer; never contains internal state, SQL, or stack traces.

          • instancestring · uri-reference

            A URI reference identifying the specific occurrence.

          • invalid_paramsarray of InvalidParam

            Present on 422 responses. Lists every field that failed validation, rather than only the first, so a client can highlight an entire form in one pass.

            • codestring

              Stable machine-readable code for this failure, for clients that localise their own messages rather than showing reason.

            • pointerstring

              Required

              RFC 6901 JSON Pointer to the offending value within the request body. A pointer rather than a field name so that failures nested inside arrays — the third line item of an order — are addressable at all.

            • reasonstring

              Required

              Why this value was rejected, in human-readable form.

          • refusalsarray of Location refusal

            Present on 409 responses to a withdrawal refused by the state it depends on (deleting a location with tills still assigned, open orders, …). Lists every reason at once, each with how much stands in the way, so a client can say what to put right and link to it rather than walking a person through one refusal after another.

            • kindstring

              Required

              A stable refusal kind — devices, open_orders, drawer_sessions, bookings or stock — that a client links to the screen that puts it right. Branch on this field, never on the surrounding prose.

            • countinteger

              Required

              How many things stand in the way of the withdrawal, so a client can repeat the number rather than re-read prose.

              ≥ 1

          • statusinteger

            Required

            The HTTP status code, repeated here so the body is self-contained.

            100–599

          • titlestring

            Required

            A short, human-readable summary of the problem kind.

          • trace_idstring

            The OpenTelemetry trace id for this request. Quote it in a support ticket and the whole request can be pulled up.

        • statusstring · enum

          Required

          created — the item made a new resource. updated — it changed an existing one.

          unchanged — this key was already applied and the original outcome stands. A success, not an error: it is what a client retrying a batch it was unsure landed should expect, and it clears the item from its queue exactly as created does.

          failed — the item was refused, and nothing else in the batch was affected. See problem.

          createdupdatedunchangedfailed

        • versioninteger · int64

          The resource's version after the item. Absent when it failed.

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 415

      The request Content-Type is not supported. This API accepts application/json on all mutations.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  22. GET/v1/modifiers/{id}

    Get a modifier

    No. getModifierAuth catalog:read

    Returns a single modifier.

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

      Modifier · application/json
      • idstring · uuid

        Required

      • namestring

        Required

        1–255 chars

      • allergensarray of string

        The allergens this modifier contains, as the merchant names them (e.g. "nuts", "dairy"). Shown on the till when choosing the modifier, and used by the allergen check.

        ≤ 20 items

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

      • merchant_idstring · uuid

        RequiredRead only

      • modifier_group_idstring · uuid

        Required

        The group this modifier belongs to.

      • price_deltaMoney

        Required

        A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

        The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

        Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

        • amountinteger · int64

          Required

          The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

        • currencyCurrency

          Required

          ISO 4217 currency code.

          Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

          One of 182 values; see the schema.

      • sort_orderinteger · int32

        Presentation order within the group.

        default 0

      • statusstring · enum

        Required

        An inactive modifier stops being offered but is never deleted.

        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.

      • versionVersion

        RequiredRead only

        Optimistic concurrency version, raised by the server on every write. A version is never lower than one already returned for the resource, though it may rise by more than one.

        Returned in the ETag response header as well as in the body. Send it back in If-Match on an update and the write is rejected with 412 if anyone else has written since — which is what stops two tills silently overwriting each other's edit to the same product when both come back online at once.

        ≥ 1

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 404

      No such resource, or none visible to this caller.

      Also returned instead of 403 where acknowledging existence would itself leak information — so a 404 does not prove the id is unused globally, only that it is not yours.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  23. PATCH/v1/modifiers/{id}

    Change a modifier

    No. updateModifierAuth catalog:write

    Applies a JSON Merge Patch (RFC 7396) to a modifier, guarded by If-Match. Send only the fields that change; the patch is refused with 412 if the modifier has changed since the version you read.

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

    • namestring

      1–255 chars

    • allergensarray of string

      The allergens this modifier contains. A patch replaces the whole list.

      ≤ 20 items

    • modifier_group_idstring · uuid

      Moves the modifier to another of the merchant's groups.

    • price_deltaMoney

      A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

      The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

      Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

      • amountinteger · int64

        Required

        The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

      • currencyCurrency

        Required

        ISO 4217 currency code.

        Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

        One of 182 values; see the schema.

    • sort_orderinteger · int32
    • statusstring · enum

      activeinactive

    Responses

    • 200

      The changed modifier.

      Modifier · application/json
      • idstring · uuid

        Required

      • namestring

        Required

        1–255 chars

      • allergensarray of string

        The allergens this modifier contains, as the merchant names them (e.g. "nuts", "dairy"). Shown on the till when choosing the modifier, and used by the allergen check.

        ≤ 20 items

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

      • merchant_idstring · uuid

        RequiredRead only

      • modifier_group_idstring · uuid

        Required

        The group this modifier belongs to.

      • price_deltaMoney

        Required

        A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

        The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

        Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

        • amountinteger · int64

          Required

          The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

        • currencyCurrency

          Required

          ISO 4217 currency code.

          Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

          One of 182 values; see the schema.

      • sort_orderinteger · int32

        Presentation order within the group.

        default 0

      • statusstring · enum

        Required

        An inactive modifier stops being offered but is never deleted.

        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.

      • versionVersion

        RequiredRead only

        Optimistic concurrency version, raised by the server on every write. A version is never lower than one already returned for the resource, though it may rise by more than one.

        Returned in the ETag response header as well as in the body. Send it back in If-Match on an update and the write is rejected with 412 if anyone else has written since — which is what stops two tills silently overwriting each other's edit to the same product when both come back online at once.

        ≥ 1

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 404

      No such resource, or none visible to this caller.

      Also returned instead of 403 where acknowledging existence would itself leak information — so a 404 does not prove the id is unused globally, only that it is not yours.

    • 409

      The request conflicts with current state — a uniqueness constraint, or an attempt to mutate an order that is already completed or voided.

      Unlike 412, re-reading and retrying unchanged will not resolve it; the request itself has to change.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 412

      The If-Match version did not match the resource's current version: someone else wrote since you read.

      This is the expected, healthy outcome of two clients editing the same record — not an error condition to suppress. Re-read the resource, re-apply your change on top of the new state, and retry with the fresh ETag.

    • 415

      The request Content-Type is not supported. This API accepts application/json on all mutations.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  24. DELETE/v1/modifiers/{id}

    Delete a modifier

    No. deleteModifierAuth catalog:write

    Soft-deletes a modifier, emitting a tombstone on the change feed. Sales already using it remain reproducible, because each line snapshots what was chosen.

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

  25. GET/v1/product-availability

    List what is 86'd

    No. listProductAvailabilityAuth catalog:read

    Returns the availability flags: which products are 86'd at which locations, and which have been brought back. A product with no flag at a location is available there.

    Parameters

    • location_idquery · string · uuid

      Only the flags at this location.

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

      ProductAvailabilityPage · application/json
      • has_moreboolean

        Required

        Whether another page follows.

      • itemsarray of ProductAvailability

        Required

        • availableboolean

          Required

          false while the product is 86'd at the location.

        • changed_by_namestring

          Required

          Who last took it off or put it back, as the person, the till, or the integration that did. Empty when no principal was named (a job or a migration).

        • location_idstring · uuid

          Required

        • product_idstring · uuid

          Required

        • updated_atTimestamp

          RequiredRead only

          Server-assigned RFC 3339 timestamp, always UTC.

          Server-assigned because device clocks drift and cannot be trusted for ordering — a till that has been powered off for a week may come back with a clock hours out. Where the device's own view of time matters it is carried separately in client_created_at.

        • versionVersion

          RequiredRead only

          Optimistic concurrency version, raised by the server on every write. A version is never lower than one already returned for the resource, though it may rise by more than one.

          Returned in the ETag response header as well as in the body. Send it back in If-Match on an update and the write is rejected with 412 if anyone else has written since — which is what stops two tills silently overwriting each other's edit to the same product when both come back online at once.

          ≥ 1

      • next_cursorstring

        Nullable

        Opaque cursor for the next page, or null on the last page. Pass it back verbatim as the cursor query parameter; its contents are not part of this contract.

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  26. POST/v1/product-availability/batch-upsert

    86 a product, or bring it back

    No. batchUpsertProductAvailabilityAuth pos:orders:write

    Writes availability flags. This is the one catalogue write a device may make: a till marking a dish sold out at its own location, following the back office's toggle. A product is available by default and the flag holds the exception, so a false takes a dish off at one site while it stays on at the next.

    The client mints the id and the flag stands or falls on its own: an item that names a product it cannot reach fails in its place and the rest are still applied, exactly as order batches do. A device must name its own location; a person or integration any location they may write. Writing the same flag twice is a no-op.

    Each write is recorded on the change feed, so every till holding the catalogue sees the 86 and brings the dish back with true.

    Request body application/json · ProductAvailabilityBatchUpsertRequest

    • itemsarray of ProductAvailabilityBatchUpsertItem

      Required

      ≥ 1 items · ≤ 500 items

      • availableboolean

        Required

        false means 86'd (not for sale), true means back on.

      • idempotency_keyItemIdempotencyKey

        Required

        A key for one item in a batch, minted by the client when it queues the item and reused on every attempt to send it. The item, not the request, is the unit of work: a retried batch returns the stored result for every item already applied under its key. A fresh key per attempt defeats the whole mechanism.

        Scoped per merchant, so keys need only be unique within a merchant's own traffic.

        16–255 chars

      • location_idstring · uuid

        Required

        The location the flag applies to.

      • product_idstring · uuid

        Required

        The product being 86'd or brought back.

    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.

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

  27. GET/v1/products

    List products

    No. listProductsAuth catalog:read

    Returns the merchant's products, newest cursor page first.

    Devices should not call this to sync — use listChanges, which returns only what has changed and costs a fraction of the bandwidth on a metered connection.

    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.

    • location_idquery · string · uuid

      Only products available at this location.

    • statusquery · string · enum

      Filter by status. Omit to return both active and inactive.

    Responses

    • 200

      A page of products.

      ProductPage · application/json
      • has_moreboolean

        Required

        Whether another page follows.

      • itemsarray of Product

        Required

        • idstring · uuid

          Required

        • typestring · enum

          What the product is. A gift_card product is sold as a line that is not revenue and is not taxed on sale — the merchant has taken money for a promise — while everything else is standard. A combo product is a set at a price (menu item 18): its components are the products the guest chooses, sold as their own lines, with the set price apportioned across them at their own rates. Defaults to standard when unstated.

          standardgift_cardcombo

          default standard

        • namestring

          Required

          1–255 chars

        • allergen_idsarray of string · uuid

          Read only

          The vocabulary rows the words in allergens name, in the same order. Identity, so a vocabulary rename cannot drift a stored label: a client that matches a product's allergens against a guest's declared set matches by id, and the words are display only. (A row written before id enforcement is absent; its word still matches by word, as ever.)

          ≤ 20 items

        • allergensarray of string

          The allergens a product contains, as the merchant names them (e.g. "gluten", "dairy"). Shown on the till and in the back office. Written against the merchant's allergen vocabulary (server-enforced, canonicalised to the vocabulary's spelling); the words are for display, and allergen_ids carries their identity.

          ≤ 20 items

        • allergens_declaredboolean

          Whether the merchant has declared what the product contains. A product with a non-empty allergens list is declared; a product genuinely free of every allergen is declared with an empty list and allergens_declared: true. Absent or false with an empty list means no declaration at all — the safety feature cannot read a product it knows nothing about, and a merchant can insist on a declaration through a field policy (field-policies item 9). The till never assumes; a declared-none product matches nothing and reads as clean, an undeclared one is named by the conformance view.

          default false

        • barcodesarray of string

          The barcodes this product can be scanned or searched as. A product may have none; each entry must be non-blank and unique. The till's search matches any of them.

        • category_idstring · uuid

          Nullable

          The category this product belongs to, if any.

        • channel_pricesarray of ProductChannelPrice

          Per-channel prices, the exceptions to price. A product may be priced differently on one channel than another (menu item 19); where no override exists the base price applies. Each entry is a channel with a price, and a channel may appear at most once. Set server-authoritative, like the rest of the catalogue.

          • channelstring · enum

            Required

            The channel this price applies on.

            postableonlineintegration

          • effective_fromstring · date

            The date this price takes effect (locations-and-groups item 14). Absent means the price is in force now; a future date is a scheduled change resolved at the business day's boundary. At most one future entry exists per channel.

          • priceMoney

            Required

            A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

            The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

            Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

            • amountinteger · int64

              Required

              The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

            • currencyCurrency

              Required

              ISO 4217 currency code.

              Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

              One of 182 values; see the schema.

        • componentsarray of object

          The products a combo is made of (menu item 18), only present when type is combo. The guest chooses one of each at the till; the set price is apportioned across the chosen components at their own rates and each is added as its own line, so the kitchen, stock depletion and the mix report behave as for any line. A component may appear at most once and may not be the combo itself.

          ≤ 50 items

          • category_idstring · uuid

            Nullable

            The category this slot chooses one item of at the till (mix-and-match, retail promotions): the operator picks one active product of the category, and the set price is apportioned across the chosen items as any combo's is. Exactly one of product_id or category_id is present.

          • product_idstring · uuid

            Nullable

            The fixed product this slot contains. Exactly one of product_id or category_id is present.

          • quantityQuantity

            Required

            How much of something, as a decimal string in the unit_of_measure beside it (each where there is none), as Square's quantity is: three croissants are "3", 1.4 kg of beans is "1.4". A string because a JSON number is a binary float to most parsers.

            It has at most as many decimal places as its unit's base unit holds — three in each, g and ml, six in kg and l — and a finer one is refused, never rounded. Tenvelo holds every quantity as an integer of that base unit (thousandths of an each, milligrams, microlitres; docs/decisions/0063); a client that computes with one should read the digits the same way, never through a float.

            ≤ 26 chars · pattern ^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$

        • costMoney

          Nullable

          What the product costs the merchant, in the product's currency, if one is known. Inventory values stock at it and the product screen shows the margin (price minus cost). NULL until a merchant sets it; never negative when present.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

        • created_atTimestamp

          RequiredRead only

          Server-assigned RFC 3339 timestamp, always UTC.

          Server-assigned because device clocks drift and cannot be trusted for ordering — a till that has been powered off for a week may come back with a clock hours out. Where the device's own view of time matters it is carried separately in client_created_at.

        • daypartsarray of DaypartWindow

          Time-of-day windows a product is sold in, in minutes since midnight in the location's timezone. A product with no windows is sold all day; one with windows is shown on the till only while the location's clock is inside a window. A window may cross midnight (start > end). A window opens on the weekdays in days (1 = Monday .. 7 = Sunday); an empty or absent days is a daily window on every day.

          ≤ 8 items

          • daysarray of integer

            The weekdays this window opens on, 1 (Monday) through 7 (Sunday). An empty or absent list is every day.

            ≤ 7 items

          • end_minuteinteger

            Required

            1–1440

          • start_minuteinteger

            Required

            0–1439

        • descriptionstring

          ≤ 2048 chars

        • has_pending_priceboolean

          Read only

          Whether a scheduled price change for any source is pending for the product — the base schedule, a location's, or a channel's — that is, a row in any price source with a future effective date. Set on the catalogue list so it can mark a product whose price nobody has seen yet is coming. price_schedule carries the base one to show when and for how much; this is what catches a location or channel schedule, which no single field can.

        • image_urlstring

          A URL to a photo of the product, shown on the till menu and in the back office.

          ≤ 2048 chars

        • location_idsarray of string · uuid

          Locations at which this product is sold. A device only syncs products available at its own location.

        • location_sortsarray of ProductLocationSort

          Arrangement overrides — where this product sits within its category at one site (menu item 12, docs/decisions/0121). The base sort_order applies where no entry exists, so a site may put desserts before mains and another not; a location appears at most once, and a device never writes one.

          • location_idstring · uuid

            Required

            The site this arrangement applies at.

          • sort_orderinteger · int32

            Required

            Where the product sits within its category at this site, lower first; equal orders sort by name.

            ≥ 0

        • merchant_idstring · uuid

          RequiredRead only

        • minimum_ageinteger · int32

          The youngest buyer the law allows for this product, in years (retail item: age-restriction prompts). 0 is no restriction, so a shop only sets this for the off-licence shelf. The till asks the operator to confirm the guest is at at least this old before a sale carrying the product completes, and the sale records the confirmation (the order's age_confirmed and age_minimum). Off by default; a presentation of the product, never a required entry.

          0–100 · default 0

        • modifier_group_idsarray of string · uuid

          The modifier groups this product offers, in menu order. A product may belong to several groups (milk, extras) and a group may be shared across products.

        • multibuyMultibuyRule

          Nullable

          The product's multibuy deal, or null for none: buy every units and the last unit of each run is charged at the rule's price instead of the line's. Counted per line on one invoice (retail promotions). The rule rides a sale line as a snapshot, so changing it later rewrites nothing already sold.

          • everyinteger

            Required

            How many units make a run; 3 is the ordinary buy-two-get-one.

            2–1000

          • priceMoney

            Required

            A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

            The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

            Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

            • amountinteger · int64

              Required

              The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

            • currencyCurrency

              Required

              ISO 4217 currency code.

              Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

              One of 182 values; see the schema.

        • online_location_idsarray of string · uuid

          The locations, from location_ids, whose online menu offers this product while sold_online is on. A site can sell a product at the counter and keep it off its ordering page. Leave it out to keep the locations the product is already online at; send [] to take it offline everywhere.

        • packProductPack

          Nullable

          What one pack of this product holds, if the merchant counts it in packs; null when it is counted only in its own unit.

          • labelstring

            Required

            What a pack is called, for display — "bottle", "case", "bag". Never arithmetic.

            1–32 chars

          • sizeQuantity

            Required

            How much of something, as a decimal string in the unit_of_measure beside it (each where there is none), as Square's quantity is: three croissants are "3", 1.4 kg of beans is "1.4". A string because a JSON number is a binary float to most parsers.

            It has at most as many decimal places as its unit's base unit holds — three in each, g and ml, six in kg and l — and a finer one is refused, never rounded. Tenvelo holds every quantity as an integer of that base unit (thousandths of an each, milligrams, microlitres; docs/decisions/0063); a client that computes with one should read the digits the same way, never through a float.

            ≤ 26 chars · pattern ^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$

          • unitstring · enum

            Required

            The unit size is in.

            eachkgglml

        • perishableboolean

          Whether this product is perishable (retail item 10): a shop never takes one back, so a return of it is exempt from the returns window the shop set for goods. Off by default, and a presentation of the product, never a required entry.

          default false

        • prepared_overrideboolean

          Nullable

          The one product that is the exception to its category: the category is not prepared but this product is, or the reverse. null (or omitted) follows the category (kitchen-display item 10).

        • priceMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

        • price_overridesarray of ProductLocationPrice

          Prices that override price. A product may be priced differently at one site than another, and on one channel than another (menu item 19): the most specific override — a location-and-channel one beats a location-only one — applies, then the base price. Each entry is a location (and optionally a channel) with a price; a (location, channel) pair may appear at most once. Set server-authoritative, like the rest of the catalogue.

          • channelstring

            The order channel this price applies to, or absent for every channel at that location.

          • effective_fromstring · date

            The date this price takes effect (locations-and-groups item 14). Absent means the price is in force now; a future date is a scheduled change every till receives ahead of time and resolves at its own business day's boundary. At most one future entry exists per location and channel.

          • effective_untilstring · date

            The last day this price is charged (retail promotions: dated sale prices). A location override with an end is a sale — "£2.50 each until Sunday" — and the resolver reverts to the price beneath it after that day. Absent means the override stands.

          • location_idstring · uuid

            Required

            The site this price applies at.

          • priceMoney

            Required

            A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

            The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

            Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

            • amountinteger · int64

              Required

              The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

            • currencyCurrency

              Required

              ISO 4217 currency code.

              Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

              One of 182 values; see the schema.

        • price_schedulearray of ProductScheduledPrice

          Pending base-price changes (locations-and-groups item 14): the price the product sells at from a future date, resolved by the latest entry at or before the business date, falling back to price when none is in force. At most one future entry exists — a new schedule supersedes the previous one. price stays the price in force today.

          • effective_fromstring · date

            Required

            The business date this price takes effect on, in the location's own clock.

          • priceMoney

            Required

            A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

            The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

            Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

            • amountinteger · int64

              Required

              The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

            • currencyCurrency

              Required

              ISO 4217 currency code.

              Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

              One of 182 values; see the schema.

        • recipeProductRecipe

          Nullable

          What this made item consumes (inventory item 13): one sold unit is made from its ingredients, each depleting its own stock level in the amount given, in the ingredient's own unit — 40 ml of vodka is "40". A product with a recipe is not stocked as itself. Only an item sold per each may carry one. null or absent means the product is stocked and sold as itself.

          • linesarray of object

            The ingredients of the made item, by product and amount per unit.

            ≥ 1 items

            • product_idstring · uuid

              Required

              The ingredient product, same merchant, and not the made item itself.

            • quantityQuantity

              Required

              How much of something, as a decimal string in the unit_of_measure beside it (each where there is none), as Square's quantity is: three croissants are "3", 1.4 kg of beans is "1.4". A string because a JSON number is a binary float to most parsers.

              It has at most as many decimal places as its unit's base unit holds — three in each, g and ml, six in kg and l — and a finer one is refused, never rounded. Tenvelo holds every quantity as an integer of that base unit (thousandths of an each, milligrams, microlitres; docs/decisions/0063); a client that computes with one should read the digits the same way, never through a float.

              ≤ 26 chars · pattern ^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$

            • unit_of_measurestring · enum

              The unit quantity is in, which must measure what the ingredient does. Absent means each.

              eachkgglml

        • skustring

          Merchant's own stock-keeping unit, if they use one.

          ≤ 64 chars

        • sold_onlineboolean

          Whether this product is offered on the online storefront, and so part of the location's public online menu. Off by default; a per-location 86 still withdraws it there.

          default false

        • sort_orderinteger · int32

          Where this product sits on the till's menu within its category. Lower comes first; equal orders sort by name.

          ≥ 0

        • statusstring · enum

          Required

          An inactive product stops appearing on tills but is never deleted, because orders already reference it and a receipt must remain reproducible.

          activeinactive

        • supplier_idstring · uuid

          Nullable

          The supplier this product is reordered from, if any: a suggested order groups products by it. Square calls the same link a variation's vendor (vendor_id); a product here has one.

        • tax_class_idstring · uuid

          Nullable

          The tax class this product belongs to, and the only way it is taxed. A class and a fulfilment type resolve to one or several rates through a tax rule, so a cold sandwich is zero-rated as takeaway and standard-rated eat-in without the menu being duplicated (docs/decisions/0014). Square lists an item's taxes directly (tax_ids); the class stands between them so the set can vary by fulfilment. Null means no tax applies — a deliberate statement, not an omission. A classed product with no rule for a fulfilment is a configuration error the back office shows, never a silent "no tax".

        • track_inventoryboolean

          Whether inventory is counted for this product. Coffee made to order is not tracked; a bag of beans on a shelf is.

          default false

        • track_serialboolean

          Whether the product's units carry serial numbers (retail item: serial numbers): each unit is one physical thing with its own code, and the till asks for it when the product is rung up, recording it on the line. Off by default; a presentation of the product, never a required entry.

          default false

        • translationsobject

          The product's name and description in other languages (0017, Merchant content; menu item 20), keyed by BCP 47 tag (es, fr, en-GB). The base name and description are the source language. They ride the change feed and the ETag like any catalogue field, and a guest reading the product is answered in the language they asked for — never the base text in a language they did not ask for. Each entry carries a source_digest the server computed from the base fields at write time; the back office compares it to a later edit and flags a translation left behind.

        • unit_of_measurestring · enum

          The unit the product is sold and counted in (inventory item 16). A café or bar that stocks by weight or volume names the unit so a fractional quantity is meaningful. Defaults to each.

          eachkgglml

          default each

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

        • variantsarray of object

          The named options a product is sold as — a size, a strength. Each carries its own absolute price, which replaces the base price when chosen. A product with variants requires one to be picked; the till offers them before the line is rung up.

          ≤ 50 items

          • idstring · uuid

            Required

          • namestring

            Required

            1–64 chars

          • barcodesarray of string

            The barcodes this variant can be scanned or searched as, in addition to the product's own. Each entry must be non-blank and unique; a code may not repeat across a product and its variants, because a scan must name one sellable unit. The till matches any of them.

          • costMoney

            Nullable

            What this variant costs the merchant, in the variant's currency, when one is known. A sale of the variant snapshots this rather than the product's cost, so a later cost change does not restate a past period. NULL until a merchant sets it; never negative when present.

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

          • priceMoney

            Required

            A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

            The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

            Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

            • amountinteger · int64

              Required

              The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

            • currencyCurrency

              Required

              ISO 4217 currency code.

              Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

              One of 182 values; see the schema.

          • skustring

            The merchant's own stock-keeping unit for this variant, if they use one. The till's search matches it like the product's own SKU.

            ≤ 64 chars

        • versionVersion

          RequiredRead only

          Optimistic concurrency version, raised by the server on every write. A version is never lower than one already returned for the resource, though it may rise by more than one.

          Returned in the ETag response header as well as in the body. Send it back in If-Match on an update and the write is rejected with 412 if anyone else has written since — which is what stops two tills silently overwriting each other's edit to the same product when both come back online at once.

          ≥ 1

      • next_cursorstring

        Nullable

        Opaque cursor for the next page, or null on the last page. Pass it back verbatim as the cursor query parameter; its contents are not part of this contract.

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  28. POST/v1/products/batch-upsert

    Create or replace products

    No. batchUpsertProductsAuth catalog:write

    Creates products at client-minted ids, or replaces existing ones at the version they were read at. A batch of one is how a single product is created.

    Each item carries its own idempotency_key and stands or falls on its own: an invalid item, or one written from a stale version, fails in its result and every other item is still applied. Only a malformed envelope refuses the request as a whole. Every change reaches devices through the change feed.

    Request body application/json · ProductBatchUpsertRequest

    • itemsarray of ProductBatchUpsertItem

      Required

      ≥ 1 items · ≤ 500 items

      • field_policy_bypassstring

        An Owner or Manager's override note (field-policies item 5). When an enforce rule would refuse this product, a non-empty value admits the write and records the violation with the reason, so what was overridden and why is kept.

      • idempotency_keyItemIdempotencyKey

        Required

        A key for one item in a batch, minted by the client when it queues the item and reused on every attempt to send it. The item, not the request, is the unit of work: a retried batch returns the stored result for every item already applied under its key. A fresh key per attempt defeats the whole mechanism.

        Scoped per merchant, so keys need only be unique within a merchant's own traffic.

        16–255 chars

      • productProduct

        Required

        Something a merchant sells.

        Products are server-authoritative: devices pull them and never push them. A till editing its own catalogue offline would let two tills diverge with no way to reconcile which was right, so catalogue editing happens in the dashboard and flows outward.

        • idstring · uuid

          Required

        • typestring · enum

          What the product is. A gift_card product is sold as a line that is not revenue and is not taxed on sale — the merchant has taken money for a promise — while everything else is standard. A combo product is a set at a price (menu item 18): its components are the products the guest chooses, sold as their own lines, with the set price apportioned across them at their own rates. Defaults to standard when unstated.

          standardgift_cardcombo

          default standard

        • namestring

          Required

          1–255 chars

        • allergensarray of string

          The allergens a product contains, as the merchant names them (e.g. "gluten", "dairy"). Shown on the till and in the back office. Written against the merchant's allergen vocabulary (server-enforced, canonicalised to the vocabulary's spelling); the words are for display, and allergen_ids carries their identity.

          ≤ 20 items

        • allergens_declaredboolean

          Whether the merchant has declared what the product contains. A product with a non-empty allergens list is declared; a product genuinely free of every allergen is declared with an empty list and allergens_declared: true. Absent or false with an empty list means no declaration at all — the safety feature cannot read a product it knows nothing about, and a merchant can insist on a declaration through a field policy (field-policies item 9). The till never assumes; a declared-none product matches nothing and reads as clean, an undeclared one is named by the conformance view.

          default false

        • barcodesarray of string

          The barcodes this product can be scanned or searched as. A product may have none; each entry must be non-blank and unique. The till's search matches any of them.

        • category_idstring · uuid

          Nullable

          The category this product belongs to, if any.

        • channel_pricesarray of ProductChannelPrice

          Per-channel prices, the exceptions to price. A product may be priced differently on one channel than another (menu item 19); where no override exists the base price applies. Each entry is a channel with a price, and a channel may appear at most once. Set server-authoritative, like the rest of the catalogue.

          • channelstring · enum

            Required

            The channel this price applies on.

            postableonlineintegration

          • effective_fromstring · date

            The date this price takes effect (locations-and-groups item 14). Absent means the price is in force now; a future date is a scheduled change resolved at the business day's boundary. At most one future entry exists per channel.

          • priceMoney

            Required

            A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

            The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

            Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

            • amountinteger · int64

              Required

              The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

            • currencyCurrency

              Required

              ISO 4217 currency code.

              Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

              One of 182 values; see the schema.

        • componentsarray of object

          The products a combo is made of (menu item 18), only present when type is combo. The guest chooses one of each at the till; the set price is apportioned across the chosen components at their own rates and each is added as its own line, so the kitchen, stock depletion and the mix report behave as for any line. A component may appear at most once and may not be the combo itself.

          ≤ 50 items

          • category_idstring · uuid

            Nullable

            The category this slot chooses one item of at the till (mix-and-match, retail promotions): the operator picks one active product of the category, and the set price is apportioned across the chosen items as any combo's is. Exactly one of product_id or category_id is present.

          • product_idstring · uuid

            Nullable

            The fixed product this slot contains. Exactly one of product_id or category_id is present.

          • quantityQuantity

            Required

            How much of something, as a decimal string in the unit_of_measure beside it (each where there is none), as Square's quantity is: three croissants are "3", 1.4 kg of beans is "1.4". A string because a JSON number is a binary float to most parsers.

            It has at most as many decimal places as its unit's base unit holds — three in each, g and ml, six in kg and l — and a finer one is refused, never rounded. Tenvelo holds every quantity as an integer of that base unit (thousandths of an each, milligrams, microlitres; docs/decisions/0063); a client that computes with one should read the digits the same way, never through a float.

            ≤ 26 chars · pattern ^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$

        • costMoney

          Nullable

          What the product costs the merchant, in the product's currency, if one is known. Inventory values stock at it and the product screen shows the margin (price minus cost). NULL until a merchant sets it; never negative when present.

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

        • daypartsarray of DaypartWindow

          Time-of-day windows a product is sold in, in minutes since midnight in the location's timezone. A product with no windows is sold all day; one with windows is shown on the till only while the location's clock is inside a window. A window may cross midnight (start > end). A window opens on the weekdays in days (1 = Monday .. 7 = Sunday); an empty or absent days is a daily window on every day.

          ≤ 8 items

          • daysarray of integer

            The weekdays this window opens on, 1 (Monday) through 7 (Sunday). An empty or absent list is every day.

            ≤ 7 items

          • end_minuteinteger

            Required

            1–1440

          • start_minuteinteger

            Required

            0–1439

        • descriptionstring

          ≤ 2048 chars

        • image_urlstring

          A URL to a photo of the product, shown on the till menu and in the back office.

          ≤ 2048 chars

        • location_idsarray of string · uuid

          Locations at which this product is sold. A device only syncs products available at its own location.

        • location_sortsarray of ProductLocationSort

          Arrangement overrides — where this product sits within its category at one site (menu item 12, docs/decisions/0121). The base sort_order applies where no entry exists, so a site may put desserts before mains and another not; a location appears at most once, and a device never writes one.

          • location_idstring · uuid

            Required

            The site this arrangement applies at.

          • sort_orderinteger · int32

            Required

            Where the product sits within its category at this site, lower first; equal orders sort by name.

            ≥ 0

        • minimum_ageinteger · int32

          The youngest buyer the law allows for this product, in years (retail item: age-restriction prompts). 0 is no restriction, so a shop only sets this for the off-licence shelf. The till asks the operator to confirm the guest is at at least this old before a sale carrying the product completes, and the sale records the confirmation (the order's age_confirmed and age_minimum). Off by default; a presentation of the product, never a required entry.

          0–100 · default 0

        • modifier_group_idsarray of string · uuid

          The modifier groups this product offers, in menu order. A product may belong to several groups (milk, extras) and a group may be shared across products.

        • multibuyMultibuyRule

          Nullable

          The product's multibuy deal, or null for none: buy every units and the last unit of each run is charged at the rule's price instead of the line's. Counted per line on one invoice (retail promotions). The rule rides a sale line as a snapshot, so changing it later rewrites nothing already sold.

          • everyinteger

            Required

            How many units make a run; 3 is the ordinary buy-two-get-one.

            2–1000

          • priceMoney

            Required

            A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

            The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

            Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

            • amountinteger · int64

              Required

              The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

            • currencyCurrency

              Required

              ISO 4217 currency code.

              Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

              One of 182 values; see the schema.

        • online_location_idsarray of string · uuid

          The locations, from location_ids, whose online menu offers this product while sold_online is on. A site can sell a product at the counter and keep it off its ordering page. Leave it out to keep the locations the product is already online at; send [] to take it offline everywhere.

        • packProductPack

          Nullable

          What one pack of this product holds, if the merchant counts it in packs; null when it is counted only in its own unit.

          • labelstring

            Required

            What a pack is called, for display — "bottle", "case", "bag". Never arithmetic.

            1–32 chars

          • sizeQuantity

            Required

            How much of something, as a decimal string in the unit_of_measure beside it (each where there is none), as Square's quantity is: three croissants are "3", 1.4 kg of beans is "1.4". A string because a JSON number is a binary float to most parsers.

            It has at most as many decimal places as its unit's base unit holds — three in each, g and ml, six in kg and l — and a finer one is refused, never rounded. Tenvelo holds every quantity as an integer of that base unit (thousandths of an each, milligrams, microlitres; docs/decisions/0063); a client that computes with one should read the digits the same way, never through a float.

            ≤ 26 chars · pattern ^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$

          • unitstring · enum

            Required

            The unit size is in.

            eachkgglml

        • perishableboolean

          Whether this product is perishable (retail item 10): a shop never takes one back, so a return of it is exempt from the returns window the shop set for goods. Off by default, and a presentation of the product, never a required entry.

          default false

        • prepared_overrideboolean

          Nullable

          The one product that is the exception to its category: the category is not prepared but this product is, or the reverse. null (or omitted) follows the category (kitchen-display item 10).

        • priceMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

        • price_overridesarray of ProductLocationPrice

          Prices that override price. A product may be priced differently at one site than another, and on one channel than another (menu item 19): the most specific override — a location-and-channel one beats a location-only one — applies, then the base price. Each entry is a location (and optionally a channel) with a price; a (location, channel) pair may appear at most once. Set server-authoritative, like the rest of the catalogue.

          • channelstring

            The order channel this price applies to, or absent for every channel at that location.

          • effective_fromstring · date

            The date this price takes effect (locations-and-groups item 14). Absent means the price is in force now; a future date is a scheduled change every till receives ahead of time and resolves at its own business day's boundary. At most one future entry exists per location and channel.

          • effective_untilstring · date

            The last day this price is charged (retail promotions: dated sale prices). A location override with an end is a sale — "£2.50 each until Sunday" — and the resolver reverts to the price beneath it after that day. Absent means the override stands.

          • location_idstring · uuid

            Required

            The site this price applies at.

          • priceMoney

            Required

            A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

            The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

            Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

            • amountinteger · int64

              Required

              The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

            • currencyCurrency

              Required

              ISO 4217 currency code.

              Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

              One of 182 values; see the schema.

        • price_schedulearray of ProductScheduledPrice

          Pending base-price changes (locations-and-groups item 14): the price the product sells at from a future date, resolved by the latest entry at or before the business date, falling back to price when none is in force. At most one future entry exists — a new schedule supersedes the previous one. price stays the price in force today.

          • effective_fromstring · date

            Required

            The business date this price takes effect on, in the location's own clock.

          • priceMoney

            Required

            A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

            The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

            Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

            • amountinteger · int64

              Required

              The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

            • currencyCurrency

              Required

              ISO 4217 currency code.

              Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

              One of 182 values; see the schema.

        • recipeProductRecipe

          Nullable

          What this made item consumes (inventory item 13): one sold unit is made from its ingredients, each depleting its own stock level in the amount given, in the ingredient's own unit — 40 ml of vodka is "40". A product with a recipe is not stocked as itself. Only an item sold per each may carry one. null or absent means the product is stocked and sold as itself.

          • linesarray of object

            The ingredients of the made item, by product and amount per unit.

            ≥ 1 items

            • product_idstring · uuid

              Required

              The ingredient product, same merchant, and not the made item itself.

            • quantityQuantity

              Required

              How much of something, as a decimal string in the unit_of_measure beside it (each where there is none), as Square's quantity is: three croissants are "3", 1.4 kg of beans is "1.4". A string because a JSON number is a binary float to most parsers.

              It has at most as many decimal places as its unit's base unit holds — three in each, g and ml, six in kg and l — and a finer one is refused, never rounded. Tenvelo holds every quantity as an integer of that base unit (thousandths of an each, milligrams, microlitres; docs/decisions/0063); a client that computes with one should read the digits the same way, never through a float.

              ≤ 26 chars · pattern ^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$

            • unit_of_measurestring · enum

              The unit quantity is in, which must measure what the ingredient does. Absent means each.

              eachkgglml

        • skustring

          Merchant's own stock-keeping unit, if they use one.

          ≤ 64 chars

        • sold_onlineboolean

          Whether this product is offered on the online storefront, and so part of the location's public online menu. Off by default; a per-location 86 still withdraws it there.

          default false

        • sort_orderinteger · int32

          Where this product sits on the till's menu within its category. Lower comes first; equal orders sort by name.

          ≥ 0

        • statusstring · enum

          Required

          An inactive product stops appearing on tills but is never deleted, because orders already reference it and a receipt must remain reproducible.

          activeinactive

        • supplier_idstring · uuid

          Nullable

          The supplier this product is reordered from, if any: a suggested order groups products by it. Square calls the same link a variation's vendor (vendor_id); a product here has one.

        • tax_class_idstring · uuid

          Nullable

          The tax class this product belongs to, and the only way it is taxed. A class and a fulfilment type resolve to one or several rates through a tax rule, so a cold sandwich is zero-rated as takeaway and standard-rated eat-in without the menu being duplicated (docs/decisions/0014). Square lists an item's taxes directly (tax_ids); the class stands between them so the set can vary by fulfilment. Null means no tax applies — a deliberate statement, not an omission. A classed product with no rule for a fulfilment is a configuration error the back office shows, never a silent "no tax".

        • track_inventoryboolean

          Whether inventory is counted for this product. Coffee made to order is not tracked; a bag of beans on a shelf is.

          default false

        • track_serialboolean

          Whether the product's units carry serial numbers (retail item: serial numbers): each unit is one physical thing with its own code, and the till asks for it when the product is rung up, recording it on the line. Off by default; a presentation of the product, never a required entry.

          default false

        • translationsobject

          The product's name and description in other languages (0017, Merchant content; menu item 20), keyed by BCP 47 tag (es, fr, en-GB). The base name and description are the source language. They ride the change feed and the ETag like any catalogue field, and a guest reading the product is answered in the language they asked for — never the base text in a language they did not ask for. Each entry carries a source_digest the server computed from the base fields at write time; the back office compares it to a later edit and flags a translation left behind.

        • unit_of_measurestring · enum

          The unit the product is sold and counted in (inventory item 16). A café or bar that stocks by weight or volume names the unit so a fractional quantity is meaningful. Defaults to each.

          eachkgglml

          default each

        • variantsarray of object

          The named options a product is sold as — a size, a strength. Each carries its own absolute price, which replaces the base price when chosen. A product with variants requires one to be picked; the till offers them before the line is rung up.

          ≤ 50 items

          • idstring · uuid

            Required

          • namestring

            Required

            1–64 chars

          • barcodesarray of string

            The barcodes this variant can be scanned or searched as, in addition to the product's own. Each entry must be non-blank and unique; a code may not repeat across a product and its variants, because a scan must name one sellable unit. The till matches any of them.

          • costMoney

            Nullable

            What this variant costs the merchant, in the variant's currency, when one is known. A sale of the variant snapshots this rather than the product's cost, so a later cost change does not restate a past period. NULL until a merchant sets it; never negative when present.

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

          • priceMoney

            Required

            A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

            The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

            Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

            • amountinteger · int64

              Required

              The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

            • currencyCurrency

              Required

              ISO 4217 currency code.

              Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

              One of 182 values; see the schema.

          • skustring

            The merchant's own stock-keeping unit for this variant, if they use one. The till's search matches it like the product's own SKU.

            ≤ 64 chars

      • versioninteger · int64

        The version this write was made from. Required to replace a product that exists, and refused as 412 when somebody else has written it since.

        ≥ 1

    Responses

    • 200

      The batch was processed. results says what happened to each item.

      BatchResponse · application/json
      • resultsarray of BatchResult

        Required

        • idstring · uuid

          The resource the item created or changed. Absent when it failed.

        • idempotency_keystring

          Required

          The item's key, echoed so a result can be matched by key as well as position.

        • problemProblem

          Nullable

          Why the item failed, as an RFC 9457 problem. A 422 means the item is malformed and will never succeed — drop it and show it to staff rather than retrying for ever. A 409 or a 5xx means try again later.

          • typestring · uri

            Required

            A stable URI identifying the problem kind. This is the field to branch on.

            default about:blank

          • api_versionstring

            Present on an api-version-retired or api-version-unknown refusal: the API-Version this server speaks (docs/decisions/0011).

          • detailstring

            A human-readable explanation specific to this occurrence. Safe to show to a developer; never contains internal state, SQL, or stack traces.

          • instancestring · uri-reference

            A URI reference identifying the specific occurrence.

          • invalid_paramsarray of InvalidParam

            Present on 422 responses. Lists every field that failed validation, rather than only the first, so a client can highlight an entire form in one pass.

            • codestring

              Stable machine-readable code for this failure, for clients that localise their own messages rather than showing reason.

            • pointerstring

              Required

              RFC 6901 JSON Pointer to the offending value within the request body. A pointer rather than a field name so that failures nested inside arrays — the third line item of an order — are addressable at all.

            • reasonstring

              Required

              Why this value was rejected, in human-readable form.

          • refusalsarray of Location refusal

            Present on 409 responses to a withdrawal refused by the state it depends on (deleting a location with tills still assigned, open orders, …). Lists every reason at once, each with how much stands in the way, so a client can say what to put right and link to it rather than walking a person through one refusal after another.

            • kindstring

              Required

              A stable refusal kind — devices, open_orders, drawer_sessions, bookings or stock — that a client links to the screen that puts it right. Branch on this field, never on the surrounding prose.

            • countinteger

              Required

              How many things stand in the way of the withdrawal, so a client can repeat the number rather than re-read prose.

              ≥ 1

          • statusinteger

            Required

            The HTTP status code, repeated here so the body is self-contained.

            100–599

          • titlestring

            Required

            A short, human-readable summary of the problem kind.

          • trace_idstring

            The OpenTelemetry trace id for this request. Quote it in a support ticket and the whole request can be pulled up.

        • statusstring · enum

          Required

          created — the item made a new resource. updated — it changed an existing one.

          unchanged — this key was already applied and the original outcome stands. A success, not an error: it is what a client retrying a batch it was unsure landed should expect, and it clears the item from its queue exactly as created does.

          failed — the item was refused, and nothing else in the batch was affected. See problem.

          createdupdatedunchangedfailed

        • versioninteger · int64

          The resource's version after the item. Absent when it failed.

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 415

      The request Content-Type is not supported. This API accepts application/json on all mutations.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  29. GET/v1/products/{id}

    Get a product

    No. getProductAuth catalog:read

    Returns a single product.

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

      Product · application/json
      • idstring · uuid

        Required

      • typestring · enum

        What the product is. A gift_card product is sold as a line that is not revenue and is not taxed on sale — the merchant has taken money for a promise — while everything else is standard. A combo product is a set at a price (menu item 18): its components are the products the guest chooses, sold as their own lines, with the set price apportioned across them at their own rates. Defaults to standard when unstated.

        standardgift_cardcombo

        default standard

      • namestring

        Required

        1–255 chars

      • allergen_idsarray of string · uuid

        Read only

        The vocabulary rows the words in allergens name, in the same order. Identity, so a vocabulary rename cannot drift a stored label: a client that matches a product's allergens against a guest's declared set matches by id, and the words are display only. (A row written before id enforcement is absent; its word still matches by word, as ever.)

        ≤ 20 items

      • allergensarray of string

        The allergens a product contains, as the merchant names them (e.g. "gluten", "dairy"). Shown on the till and in the back office. Written against the merchant's allergen vocabulary (server-enforced, canonicalised to the vocabulary's spelling); the words are for display, and allergen_ids carries their identity.

        ≤ 20 items

      • allergens_declaredboolean

        Whether the merchant has declared what the product contains. A product with a non-empty allergens list is declared; a product genuinely free of every allergen is declared with an empty list and allergens_declared: true. Absent or false with an empty list means no declaration at all — the safety feature cannot read a product it knows nothing about, and a merchant can insist on a declaration through a field policy (field-policies item 9). The till never assumes; a declared-none product matches nothing and reads as clean, an undeclared one is named by the conformance view.

        default false

      • barcodesarray of string

        The barcodes this product can be scanned or searched as. A product may have none; each entry must be non-blank and unique. The till's search matches any of them.

      • category_idstring · uuid

        Nullable

        The category this product belongs to, if any.

      • channel_pricesarray of ProductChannelPrice

        Per-channel prices, the exceptions to price. A product may be priced differently on one channel than another (menu item 19); where no override exists the base price applies. Each entry is a channel with a price, and a channel may appear at most once. Set server-authoritative, like the rest of the catalogue.

        • channelstring · enum

          Required

          The channel this price applies on.

          postableonlineintegration

        • effective_fromstring · date

          The date this price takes effect (locations-and-groups item 14). Absent means the price is in force now; a future date is a scheduled change resolved at the business day's boundary. At most one future entry exists per channel.

        • priceMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

      • componentsarray of object

        The products a combo is made of (menu item 18), only present when type is combo. The guest chooses one of each at the till; the set price is apportioned across the chosen components at their own rates and each is added as its own line, so the kitchen, stock depletion and the mix report behave as for any line. A component may appear at most once and may not be the combo itself.

        ≤ 50 items

        • category_idstring · uuid

          Nullable

          The category this slot chooses one item of at the till (mix-and-match, retail promotions): the operator picks one active product of the category, and the set price is apportioned across the chosen items as any combo's is. Exactly one of product_id or category_id is present.

        • product_idstring · uuid

          Nullable

          The fixed product this slot contains. Exactly one of product_id or category_id is present.

        • quantityQuantity

          Required

          How much of something, as a decimal string in the unit_of_measure beside it (each where there is none), as Square's quantity is: three croissants are "3", 1.4 kg of beans is "1.4". A string because a JSON number is a binary float to most parsers.

          It has at most as many decimal places as its unit's base unit holds — three in each, g and ml, six in kg and l — and a finer one is refused, never rounded. Tenvelo holds every quantity as an integer of that base unit (thousandths of an each, milligrams, microlitres; docs/decisions/0063); a client that computes with one should read the digits the same way, never through a float.

          ≤ 26 chars · pattern ^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$

      • costMoney

        Nullable

        What the product costs the merchant, in the product's currency, if one is known. Inventory values stock at it and the product screen shows the margin (price minus cost). NULL until a merchant sets it; never negative when present.

        • amountinteger · int64

          Required

          The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

        • currencyCurrency

          Required

          ISO 4217 currency code.

          Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

          One of 182 values; see the schema.

      • created_atTimestamp

        RequiredRead only

        Server-assigned RFC 3339 timestamp, always UTC.

        Server-assigned because device clocks drift and cannot be trusted for ordering — a till that has been powered off for a week may come back with a clock hours out. Where the device's own view of time matters it is carried separately in client_created_at.

      • daypartsarray of DaypartWindow

        Time-of-day windows a product is sold in, in minutes since midnight in the location's timezone. A product with no windows is sold all day; one with windows is shown on the till only while the location's clock is inside a window. A window may cross midnight (start > end). A window opens on the weekdays in days (1 = Monday .. 7 = Sunday); an empty or absent days is a daily window on every day.

        ≤ 8 items

        • daysarray of integer

          The weekdays this window opens on, 1 (Monday) through 7 (Sunday). An empty or absent list is every day.

          ≤ 7 items

        • end_minuteinteger

          Required

          1–1440

        • start_minuteinteger

          Required

          0–1439

      • descriptionstring

        ≤ 2048 chars

      • has_pending_priceboolean

        Read only

        Whether a scheduled price change for any source is pending for the product — the base schedule, a location's, or a channel's — that is, a row in any price source with a future effective date. Set on the catalogue list so it can mark a product whose price nobody has seen yet is coming. price_schedule carries the base one to show when and for how much; this is what catches a location or channel schedule, which no single field can.

      • image_urlstring

        A URL to a photo of the product, shown on the till menu and in the back office.

        ≤ 2048 chars

      • location_idsarray of string · uuid

        Locations at which this product is sold. A device only syncs products available at its own location.

      • location_sortsarray of ProductLocationSort

        Arrangement overrides — where this product sits within its category at one site (menu item 12, docs/decisions/0121). The base sort_order applies where no entry exists, so a site may put desserts before mains and another not; a location appears at most once, and a device never writes one.

        • location_idstring · uuid

          Required

          The site this arrangement applies at.

        • sort_orderinteger · int32

          Required

          Where the product sits within its category at this site, lower first; equal orders sort by name.

          ≥ 0

      • merchant_idstring · uuid

        RequiredRead only

      • minimum_ageinteger · int32

        The youngest buyer the law allows for this product, in years (retail item: age-restriction prompts). 0 is no restriction, so a shop only sets this for the off-licence shelf. The till asks the operator to confirm the guest is at at least this old before a sale carrying the product completes, and the sale records the confirmation (the order's age_confirmed and age_minimum). Off by default; a presentation of the product, never a required entry.

        0–100 · default 0

      • modifier_group_idsarray of string · uuid

        The modifier groups this product offers, in menu order. A product may belong to several groups (milk, extras) and a group may be shared across products.

      • multibuyMultibuyRule

        Nullable

        The product's multibuy deal, or null for none: buy every units and the last unit of each run is charged at the rule's price instead of the line's. Counted per line on one invoice (retail promotions). The rule rides a sale line as a snapshot, so changing it later rewrites nothing already sold.

        • everyinteger

          Required

          How many units make a run; 3 is the ordinary buy-two-get-one.

          2–1000

        • priceMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

      • online_location_idsarray of string · uuid

        The locations, from location_ids, whose online menu offers this product while sold_online is on. A site can sell a product at the counter and keep it off its ordering page. Leave it out to keep the locations the product is already online at; send [] to take it offline everywhere.

      • packProductPack

        Nullable

        What one pack of this product holds, if the merchant counts it in packs; null when it is counted only in its own unit.

        • labelstring

          Required

          What a pack is called, for display — "bottle", "case", "bag". Never arithmetic.

          1–32 chars

        • sizeQuantity

          Required

          How much of something, as a decimal string in the unit_of_measure beside it (each where there is none), as Square's quantity is: three croissants are "3", 1.4 kg of beans is "1.4". A string because a JSON number is a binary float to most parsers.

          It has at most as many decimal places as its unit's base unit holds — three in each, g and ml, six in kg and l — and a finer one is refused, never rounded. Tenvelo holds every quantity as an integer of that base unit (thousandths of an each, milligrams, microlitres; docs/decisions/0063); a client that computes with one should read the digits the same way, never through a float.

          ≤ 26 chars · pattern ^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$

        • unitstring · enum

          Required

          The unit size is in.

          eachkgglml

      • perishableboolean

        Whether this product is perishable (retail item 10): a shop never takes one back, so a return of it is exempt from the returns window the shop set for goods. Off by default, and a presentation of the product, never a required entry.

        default false

      • prepared_overrideboolean

        Nullable

        The one product that is the exception to its category: the category is not prepared but this product is, or the reverse. null (or omitted) follows the category (kitchen-display item 10).

      • priceMoney

        Required

        A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

        The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

        Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

        • amountinteger · int64

          Required

          The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

        • currencyCurrency

          Required

          ISO 4217 currency code.

          Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

          One of 182 values; see the schema.

      • price_overridesarray of ProductLocationPrice

        Prices that override price. A product may be priced differently at one site than another, and on one channel than another (menu item 19): the most specific override — a location-and-channel one beats a location-only one — applies, then the base price. Each entry is a location (and optionally a channel) with a price; a (location, channel) pair may appear at most once. Set server-authoritative, like the rest of the catalogue.

        • channelstring

          The order channel this price applies to, or absent for every channel at that location.

        • effective_fromstring · date

          The date this price takes effect (locations-and-groups item 14). Absent means the price is in force now; a future date is a scheduled change every till receives ahead of time and resolves at its own business day's boundary. At most one future entry exists per location and channel.

        • effective_untilstring · date

          The last day this price is charged (retail promotions: dated sale prices). A location override with an end is a sale — "£2.50 each until Sunday" — and the resolver reverts to the price beneath it after that day. Absent means the override stands.

        • location_idstring · uuid

          Required

          The site this price applies at.

        • priceMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

      • price_schedulearray of ProductScheduledPrice

        Pending base-price changes (locations-and-groups item 14): the price the product sells at from a future date, resolved by the latest entry at or before the business date, falling back to price when none is in force. At most one future entry exists — a new schedule supersedes the previous one. price stays the price in force today.

        • effective_fromstring · date

          Required

          The business date this price takes effect on, in the location's own clock.

        • priceMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

      • recipeProductRecipe

        Nullable

        What this made item consumes (inventory item 13): one sold unit is made from its ingredients, each depleting its own stock level in the amount given, in the ingredient's own unit — 40 ml of vodka is "40". A product with a recipe is not stocked as itself. Only an item sold per each may carry one. null or absent means the product is stocked and sold as itself.

        • linesarray of object

          The ingredients of the made item, by product and amount per unit.

          ≥ 1 items

          • product_idstring · uuid

            Required

            The ingredient product, same merchant, and not the made item itself.

          • quantityQuantity

            Required

            How much of something, as a decimal string in the unit_of_measure beside it (each where there is none), as Square's quantity is: three croissants are "3", 1.4 kg of beans is "1.4". A string because a JSON number is a binary float to most parsers.

            It has at most as many decimal places as its unit's base unit holds — three in each, g and ml, six in kg and l — and a finer one is refused, never rounded. Tenvelo holds every quantity as an integer of that base unit (thousandths of an each, milligrams, microlitres; docs/decisions/0063); a client that computes with one should read the digits the same way, never through a float.

            ≤ 26 chars · pattern ^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$

          • unit_of_measurestring · enum

            The unit quantity is in, which must measure what the ingredient does. Absent means each.

            eachkgglml

      • skustring

        Merchant's own stock-keeping unit, if they use one.

        ≤ 64 chars

      • sold_onlineboolean

        Whether this product is offered on the online storefront, and so part of the location's public online menu. Off by default; a per-location 86 still withdraws it there.

        default false

      • sort_orderinteger · int32

        Where this product sits on the till's menu within its category. Lower comes first; equal orders sort by name.

        ≥ 0

      • statusstring · enum

        Required

        An inactive product stops appearing on tills but is never deleted, because orders already reference it and a receipt must remain reproducible.

        activeinactive

      • supplier_idstring · uuid

        Nullable

        The supplier this product is reordered from, if any: a suggested order groups products by it. Square calls the same link a variation's vendor (vendor_id); a product here has one.

      • tax_class_idstring · uuid

        Nullable

        The tax class this product belongs to, and the only way it is taxed. A class and a fulfilment type resolve to one or several rates through a tax rule, so a cold sandwich is zero-rated as takeaway and standard-rated eat-in without the menu being duplicated (docs/decisions/0014). Square lists an item's taxes directly (tax_ids); the class stands between them so the set can vary by fulfilment. Null means no tax applies — a deliberate statement, not an omission. A classed product with no rule for a fulfilment is a configuration error the back office shows, never a silent "no tax".

      • track_inventoryboolean

        Whether inventory is counted for this product. Coffee made to order is not tracked; a bag of beans on a shelf is.

        default false

      • track_serialboolean

        Whether the product's units carry serial numbers (retail item: serial numbers): each unit is one physical thing with its own code, and the till asks for it when the product is rung up, recording it on the line. Off by default; a presentation of the product, never a required entry.

        default false

      • translationsobject

        The product's name and description in other languages (0017, Merchant content; menu item 20), keyed by BCP 47 tag (es, fr, en-GB). The base name and description are the source language. They ride the change feed and the ETag like any catalogue field, and a guest reading the product is answered in the language they asked for — never the base text in a language they did not ask for. Each entry carries a source_digest the server computed from the base fields at write time; the back office compares it to a later edit and flags a translation left behind.

      • unit_of_measurestring · enum

        The unit the product is sold and counted in (inventory item 16). A café or bar that stocks by weight or volume names the unit so a fractional quantity is meaningful. Defaults to each.

        eachkgglml

        default each

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

      • variantsarray of object

        The named options a product is sold as — a size, a strength. Each carries its own absolute price, which replaces the base price when chosen. A product with variants requires one to be picked; the till offers them before the line is rung up.

        ≤ 50 items

        • idstring · uuid

          Required

        • namestring

          Required

          1–64 chars

        • barcodesarray of string

          The barcodes this variant can be scanned or searched as, in addition to the product's own. Each entry must be non-blank and unique; a code may not repeat across a product and its variants, because a scan must name one sellable unit. The till matches any of them.

        • costMoney

          Nullable

          What this variant costs the merchant, in the variant's currency, when one is known. A sale of the variant snapshots this rather than the product's cost, so a later cost change does not restate a past period. NULL until a merchant sets it; never negative when present.

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

        • priceMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

        • skustring

          The merchant's own stock-keeping unit for this variant, if they use one. The till's search matches it like the product's own SKU.

          ≤ 64 chars

      • versionVersion

        RequiredRead only

        Optimistic concurrency version, raised by the server on every write. A version is never lower than one already returned for the resource, though it may rise by more than one.

        Returned in the ETag response header as well as in the body. Send it back in If-Match on an update and the write is rejected with 412 if anyone else has written since — which is what stops two tills silently overwriting each other's edit to the same product when both come back online at once.

        ≥ 1

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 404

      No such resource, or none visible to this caller.

      Also returned instead of 403 where acknowledging existence would itself leak information — so a 404 does not prove the id is unused globally, only that it is not yours.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  30. PATCH/v1/products/{id}

    Change a product

    No. updateProductAuth catalog:write

    Applies a JSON Merge Patch (RFC 7396) to a product, guarded by If-Match.

    Send only the fields that change. The patch is refused with 412 if the product has changed since the version you read: re-read, re-apply your change on top of the current state, and retry — do not resend with If-Match: *, which is what silently loses a colleague's edit.

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

    • typestring · enum

      What the product is. A gift_card line is not revenue and is not taxed on sale. A combo sells at a set price and creates one line per component on the order. Reporting reads the type from the product, so changing it reclassifies how the product's lines are reported, like a change of category. A combo is a set at a price (menu item 18); its components are the products the guest chooses. Reporting reads the type from the product, so changing it reclassifies how the product's lines are reported, like a change of category.

      standardgift_cardcombo

    • namestring

      1–255 chars

    • allergensarray of string

      The allergens a product contains. A patch replaces the whole list.

      ≤ 20 items

    • allergens_declaredboolean

      Whether the merchant has declared what the product contains — a list, or none of the vocabulary with an empty list (field-policies item 9). The safety feature cannot check a product it knows nothing about, and a product with no allergens entry and no declaration is exactly that.

    • barcodesarray of string

      The barcodes this product can be scanned or searched as; absent leaves them as they are, a present empty array clears them. Each entry must be non-blank and unique.

    • category_idstring · uuid

      Nullable

    • channel_pricesarray of ProductChannelPrice

      Per-channel prices, the exceptions to price. A patch replaces the in-force list; a row with effective_from is a scheduled change and does not replace the current price. (locations-and-groups item 14)

      • channelstring · enum

        Required

        The channel this price applies on.

        postableonlineintegration

      • effective_fromstring · date

        The date this price takes effect (locations-and-groups item 14). Absent means the price is in force now; a future date is a scheduled change resolved at the business day's boundary. At most one future entry exists per channel.

      • priceMoney

        Required

        A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

        The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

        Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

        • amountinteger · int64

          Required

          The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

        • currencyCurrency

          Required

          ISO 4217 currency code.

          Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

          One of 182 values; see the schema.

    • componentsarray of object

      The products a combo is made of. A patch replaces the whole list; set it to [] when turning a combo back into a standard product. A component may appear at most once and may not be the combo itself.

      ≤ 50 items

      • category_idstring · uuid

        Nullable

        The category this slot chooses one item of at the till (mix-and-match). Exactly one of product_id or category_id is present.

      • product_idstring · uuid

        Nullable

      • quantityQuantity

        Required

        How much of something, as a decimal string in the unit_of_measure beside it (each where there is none), as Square's quantity is: three croissants are "3", 1.4 kg of beans is "1.4". A string because a JSON number is a binary float to most parsers.

        It has at most as many decimal places as its unit's base unit holds — three in each, g and ml, six in kg and l — and a finer one is refused, never rounded. Tenvelo holds every quantity as an integer of that base unit (thousandths of an each, milligrams, microlitres; docs/decisions/0063); a client that computes with one should read the digits the same way, never through a float.

        ≤ 26 chars · pattern ^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$

    • costMoney

      Nullable

      What the product costs the merchant, if one is known. Setting it to null clears it; absent leaves it as it is. Never negative when present.

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

    • daypartsarray of DaypartWindow

      Time-of-day windows a product is sold in. A patch replaces the whole list. A window opens on the weekdays in days (1 = Monday .. 7 = Sunday); an empty or absent days is a daily window on every day.

      ≤ 8 items

      • daysarray of integer

        The weekdays this window opens on, 1 (Monday) through 7 (Sunday). An empty or absent list is every day.

        ≤ 7 items

      • end_minuteinteger

        Required

        1–1440

      • start_minuteinteger

        Required

        0–1439

    • descriptionstring

      Nullable

      ≤ 2048 chars

    • image_urlstring

      Nullable

      A URL to a photo of the product, shown on the till menu and in the back office.

      ≤ 2048 chars

    • location_idsarray of string · uuid
    • location_sortsarray of ProductLocationSort

      Per-location arrangement, the exceptions to sort_order (menu item 12, docs/decisions/0121): a patch replaces the whole list, and the base sort_order applies where no entry exists.

      • location_idstring · uuid

        Required

        The site this arrangement applies at.

      • sort_orderinteger · int32

        Required

        Where the product sits within its category at this site, lower first; equal orders sort by name.

        ≥ 0

    • modifier_group_idsarray of string · uuid
    • multibuyMultibuyRule

      Nullable

      The product's multibuy deal; null clears it. Same shape as the product's multibuy, and counted per line on one invoice (retail promotions).

      • everyinteger

        Required

        How many units make a run; 3 is the ordinary buy-two-get-one.

        2–1000

      • priceMoney

        Required

        A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

        The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

        Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

        • amountinteger · int64

          Required

          The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

        • currencyCurrency

          Required

          ISO 4217 currency code.

          Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

          One of 182 values; see the schema.

    • online_location_idsarray of string · uuid

      The locations, from location_ids, whose online menu offers this product while sold_online is on. A patch replaces the whole list.

    • packProductPack

      Nullable

      What one pack of this product holds. Setting it to null clears it, so the product is counted only in its own unit; absent leaves it as it is.

      • labelstring

        Required

        What a pack is called, for display — "bottle", "case", "bag". Never arithmetic.

        1–32 chars

      • sizeQuantity

        Required

        How much of something, as a decimal string in the unit_of_measure beside it (each where there is none), as Square's quantity is: three croissants are "3", 1.4 kg of beans is "1.4". A string because a JSON number is a binary float to most parsers.

        It has at most as many decimal places as its unit's base unit holds — three in each, g and ml, six in kg and l — and a finer one is refused, never rounded. Tenvelo holds every quantity as an integer of that base unit (thousandths of an each, milligrams, microlitres; docs/decisions/0063); a client that computes with one should read the digits the same way, never through a float.

        ≤ 26 chars · pattern ^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$

      • unitstring · enum

        Required

        The unit size is in.

        eachkgglml

    • perishableboolean

      Whether this product is perishable (retail item 10): a shop never takes one back, so a return of it is exempt from the returns window the shop set for goods.

    • priceMoney

      A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

      The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

      Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

      • amountinteger · int64

        Required

        The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

      • currencyCurrency

        Required

        ISO 4217 currency code.

        Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

        One of 182 values; see the schema.

    • price_overridesarray of ProductLocationPrice

      Per-location prices, the exceptions to price. A patch replaces the in-force list; a row with effective_from is a scheduled change and does not replace the current price — the two are independent until the scheduled one takes effect (locations-and-groups item 14).

      • channelstring

        The order channel this price applies to, or absent for every channel at that location.

      • effective_fromstring · date

        The date this price takes effect (locations-and-groups item 14). Absent means the price is in force now; a future date is a scheduled change every till receives ahead of time and resolves at its own business day's boundary. At most one future entry exists per location and channel.

      • effective_untilstring · date

        The last day this price is charged (retail promotions: dated sale prices). A location override with an end is a sale — "£2.50 each until Sunday" — and the resolver reverts to the price beneath it after that day. Absent means the override stands.

      • location_idstring · uuid

        Required

        The site this price applies at.

      • priceMoney

        Required

        A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

        The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

        Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

        • amountinteger · int64

          Required

          The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

        • currencyCurrency

          Required

          ISO 4217 currency code.

          Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

          One of 182 values; see the schema.

    • price_schedulearray of ProductScheduledPrice

      Pending base-price changes (locations-and-groups item 14): the price the product sells at from a future date. price stays the price in force today; at most one future entry exists, because a new schedule supersedes the previous one.

      • effective_fromstring · date

        Required

        The business date this price takes effect on, in the location's own clock.

      • priceMoney

        Required

        A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

        The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

        Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

        • amountinteger · int64

          Required

          The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

        • currencyCurrency

          Required

          ISO 4217 currency code.

          Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

          One of 182 values; see the schema.

    • recipeProductRecipe

      Nullable

      What this made item consumes (inventory item 13); null when the product is stocked and sold as itself. A patch replaces the whole recipe.

      • linesarray of object

        The ingredients of the made item, by product and amount per unit.

        ≥ 1 items

        • product_idstring · uuid

          Required

          The ingredient product, same merchant, and not the made item itself.

        • quantityQuantity

          Required

          How much of something, as a decimal string in the unit_of_measure beside it (each where there is none), as Square's quantity is: three croissants are "3", 1.4 kg of beans is "1.4". A string because a JSON number is a binary float to most parsers.

          It has at most as many decimal places as its unit's base unit holds — three in each, g and ml, six in kg and l — and a finer one is refused, never rounded. Tenvelo holds every quantity as an integer of that base unit (thousandths of an each, milligrams, microlitres; docs/decisions/0063); a client that computes with one should read the digits the same way, never through a float.

          ≤ 26 chars · pattern ^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$

        • unit_of_measurestring · enum

          The unit quantity is in, which must measure what the ingredient does. Absent means each.

          eachkgglml

    • skustring

      Nullable

      ≤ 64 chars

    • sold_onlineboolean

      Whether this product is offered on the online storefront, and so part of the location's public online menu. Off by default; a per-location 86 still withdraws it there.

    • sort_orderinteger · int32

      Where this product sits on the till's menu within its category.

      ≥ 0

    • statusstring · enum

      activeinactive

    • supplier_idstring · uuid

      Nullable

      The supplier this product is reordered from, if any: a suggested order groups products by it. Square calls the same link a variation's vendor (vendor_id); a product here has one.

    • tax_class_idstring · uuid

      Nullable

      The tax class this product belongs to. Set to null to clear it, and the product is not taxed.

    • track_inventoryboolean
    • translationsobject

      Replaces the whole set of translations: a patch sends the complete translations map, every language the product is sold in, not one entry walked in. Base name and description are the source language (0017, menu item 20). source_digest is computed on write; a sent digest is ignored.

    • variantsarray of object

      The named options a product is sold as. A patch replaces the whole list.

      ≤ 50 items

      • idstring · uuid

        Required

      • namestring

        Required

        1–64 chars

      • barcodesarray of string

        The barcodes this variant can be scanned or searched as, in addition to the product's own. Each entry must be non-blank and unique; a code may not repeat across a product and its variants, because a scan must name one sellable unit.

      • costMoney

        Nullable

        What this variant costs the merchant, in the variant's currency, when one is known. A sale of the variant snapshots this rather than the product's cost, so a later cost change does not restate a past period. NULL until a merchant sets it; never negative when present.

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

      • priceMoney

        Required

        A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

        The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

        Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

        • amountinteger · int64

          Required

          The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

        • currencyCurrency

          Required

          ISO 4217 currency code.

          Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

          One of 182 values; see the schema.

      • skustring

        The merchant's own stock-keeping unit for this variant, if they use one.

        ≤ 64 chars

    Responses

    • 200

      The changed product.

      Product · application/json
      • idstring · uuid

        Required

      • typestring · enum

        What the product is. A gift_card product is sold as a line that is not revenue and is not taxed on sale — the merchant has taken money for a promise — while everything else is standard. A combo product is a set at a price (menu item 18): its components are the products the guest chooses, sold as their own lines, with the set price apportioned across them at their own rates. Defaults to standard when unstated.

        standardgift_cardcombo

        default standard

      • namestring

        Required

        1–255 chars

      • allergen_idsarray of string · uuid

        Read only

        The vocabulary rows the words in allergens name, in the same order. Identity, so a vocabulary rename cannot drift a stored label: a client that matches a product's allergens against a guest's declared set matches by id, and the words are display only. (A row written before id enforcement is absent; its word still matches by word, as ever.)

        ≤ 20 items

      • allergensarray of string

        The allergens a product contains, as the merchant names them (e.g. "gluten", "dairy"). Shown on the till and in the back office. Written against the merchant's allergen vocabulary (server-enforced, canonicalised to the vocabulary's spelling); the words are for display, and allergen_ids carries their identity.

        ≤ 20 items

      • allergens_declaredboolean

        Whether the merchant has declared what the product contains. A product with a non-empty allergens list is declared; a product genuinely free of every allergen is declared with an empty list and allergens_declared: true. Absent or false with an empty list means no declaration at all — the safety feature cannot read a product it knows nothing about, and a merchant can insist on a declaration through a field policy (field-policies item 9). The till never assumes; a declared-none product matches nothing and reads as clean, an undeclared one is named by the conformance view.

        default false

      • barcodesarray of string

        The barcodes this product can be scanned or searched as. A product may have none; each entry must be non-blank and unique. The till's search matches any of them.

      • category_idstring · uuid

        Nullable

        The category this product belongs to, if any.

      • channel_pricesarray of ProductChannelPrice

        Per-channel prices, the exceptions to price. A product may be priced differently on one channel than another (menu item 19); where no override exists the base price applies. Each entry is a channel with a price, and a channel may appear at most once. Set server-authoritative, like the rest of the catalogue.

        • channelstring · enum

          Required

          The channel this price applies on.

          postableonlineintegration

        • effective_fromstring · date

          The date this price takes effect (locations-and-groups item 14). Absent means the price is in force now; a future date is a scheduled change resolved at the business day's boundary. At most one future entry exists per channel.

        • priceMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

      • componentsarray of object

        The products a combo is made of (menu item 18), only present when type is combo. The guest chooses one of each at the till; the set price is apportioned across the chosen components at their own rates and each is added as its own line, so the kitchen, stock depletion and the mix report behave as for any line. A component may appear at most once and may not be the combo itself.

        ≤ 50 items

        • category_idstring · uuid

          Nullable

          The category this slot chooses one item of at the till (mix-and-match, retail promotions): the operator picks one active product of the category, and the set price is apportioned across the chosen items as any combo's is. Exactly one of product_id or category_id is present.

        • product_idstring · uuid

          Nullable

          The fixed product this slot contains. Exactly one of product_id or category_id is present.

        • quantityQuantity

          Required

          How much of something, as a decimal string in the unit_of_measure beside it (each where there is none), as Square's quantity is: three croissants are "3", 1.4 kg of beans is "1.4". A string because a JSON number is a binary float to most parsers.

          It has at most as many decimal places as its unit's base unit holds — three in each, g and ml, six in kg and l — and a finer one is refused, never rounded. Tenvelo holds every quantity as an integer of that base unit (thousandths of an each, milligrams, microlitres; docs/decisions/0063); a client that computes with one should read the digits the same way, never through a float.

          ≤ 26 chars · pattern ^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$

      • costMoney

        Nullable

        What the product costs the merchant, in the product's currency, if one is known. Inventory values stock at it and the product screen shows the margin (price minus cost). NULL until a merchant sets it; never negative when present.

        • amountinteger · int64

          Required

          The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

        • currencyCurrency

          Required

          ISO 4217 currency code.

          Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

          One of 182 values; see the schema.

      • created_atTimestamp

        RequiredRead only

        Server-assigned RFC 3339 timestamp, always UTC.

        Server-assigned because device clocks drift and cannot be trusted for ordering — a till that has been powered off for a week may come back with a clock hours out. Where the device's own view of time matters it is carried separately in client_created_at.

      • daypartsarray of DaypartWindow

        Time-of-day windows a product is sold in, in minutes since midnight in the location's timezone. A product with no windows is sold all day; one with windows is shown on the till only while the location's clock is inside a window. A window may cross midnight (start > end). A window opens on the weekdays in days (1 = Monday .. 7 = Sunday); an empty or absent days is a daily window on every day.

        ≤ 8 items

        • daysarray of integer

          The weekdays this window opens on, 1 (Monday) through 7 (Sunday). An empty or absent list is every day.

          ≤ 7 items

        • end_minuteinteger

          Required

          1–1440

        • start_minuteinteger

          Required

          0–1439

      • descriptionstring

        ≤ 2048 chars

      • has_pending_priceboolean

        Read only

        Whether a scheduled price change for any source is pending for the product — the base schedule, a location's, or a channel's — that is, a row in any price source with a future effective date. Set on the catalogue list so it can mark a product whose price nobody has seen yet is coming. price_schedule carries the base one to show when and for how much; this is what catches a location or channel schedule, which no single field can.

      • image_urlstring

        A URL to a photo of the product, shown on the till menu and in the back office.

        ≤ 2048 chars

      • location_idsarray of string · uuid

        Locations at which this product is sold. A device only syncs products available at its own location.

      • location_sortsarray of ProductLocationSort

        Arrangement overrides — where this product sits within its category at one site (menu item 12, docs/decisions/0121). The base sort_order applies where no entry exists, so a site may put desserts before mains and another not; a location appears at most once, and a device never writes one.

        • location_idstring · uuid

          Required

          The site this arrangement applies at.

        • sort_orderinteger · int32

          Required

          Where the product sits within its category at this site, lower first; equal orders sort by name.

          ≥ 0

      • merchant_idstring · uuid

        RequiredRead only

      • minimum_ageinteger · int32

        The youngest buyer the law allows for this product, in years (retail item: age-restriction prompts). 0 is no restriction, so a shop only sets this for the off-licence shelf. The till asks the operator to confirm the guest is at at least this old before a sale carrying the product completes, and the sale records the confirmation (the order's age_confirmed and age_minimum). Off by default; a presentation of the product, never a required entry.

        0–100 · default 0

      • modifier_group_idsarray of string · uuid

        The modifier groups this product offers, in menu order. A product may belong to several groups (milk, extras) and a group may be shared across products.

      • multibuyMultibuyRule

        Nullable

        The product's multibuy deal, or null for none: buy every units and the last unit of each run is charged at the rule's price instead of the line's. Counted per line on one invoice (retail promotions). The rule rides a sale line as a snapshot, so changing it later rewrites nothing already sold.

        • everyinteger

          Required

          How many units make a run; 3 is the ordinary buy-two-get-one.

          2–1000

        • priceMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

      • online_location_idsarray of string · uuid

        The locations, from location_ids, whose online menu offers this product while sold_online is on. A site can sell a product at the counter and keep it off its ordering page. Leave it out to keep the locations the product is already online at; send [] to take it offline everywhere.

      • packProductPack

        Nullable

        What one pack of this product holds, if the merchant counts it in packs; null when it is counted only in its own unit.

        • labelstring

          Required

          What a pack is called, for display — "bottle", "case", "bag". Never arithmetic.

          1–32 chars

        • sizeQuantity

          Required

          How much of something, as a decimal string in the unit_of_measure beside it (each where there is none), as Square's quantity is: three croissants are "3", 1.4 kg of beans is "1.4". A string because a JSON number is a binary float to most parsers.

          It has at most as many decimal places as its unit's base unit holds — three in each, g and ml, six in kg and l — and a finer one is refused, never rounded. Tenvelo holds every quantity as an integer of that base unit (thousandths of an each, milligrams, microlitres; docs/decisions/0063); a client that computes with one should read the digits the same way, never through a float.

          ≤ 26 chars · pattern ^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$

        • unitstring · enum

          Required

          The unit size is in.

          eachkgglml

      • perishableboolean

        Whether this product is perishable (retail item 10): a shop never takes one back, so a return of it is exempt from the returns window the shop set for goods. Off by default, and a presentation of the product, never a required entry.

        default false

      • prepared_overrideboolean

        Nullable

        The one product that is the exception to its category: the category is not prepared but this product is, or the reverse. null (or omitted) follows the category (kitchen-display item 10).

      • priceMoney

        Required

        A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

        The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

        Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

        • amountinteger · int64

          Required

          The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

        • currencyCurrency

          Required

          ISO 4217 currency code.

          Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

          One of 182 values; see the schema.

      • price_overridesarray of ProductLocationPrice

        Prices that override price. A product may be priced differently at one site than another, and on one channel than another (menu item 19): the most specific override — a location-and-channel one beats a location-only one — applies, then the base price. Each entry is a location (and optionally a channel) with a price; a (location, channel) pair may appear at most once. Set server-authoritative, like the rest of the catalogue.

        • channelstring

          The order channel this price applies to, or absent for every channel at that location.

        • effective_fromstring · date

          The date this price takes effect (locations-and-groups item 14). Absent means the price is in force now; a future date is a scheduled change every till receives ahead of time and resolves at its own business day's boundary. At most one future entry exists per location and channel.

        • effective_untilstring · date

          The last day this price is charged (retail promotions: dated sale prices). A location override with an end is a sale — "£2.50 each until Sunday" — and the resolver reverts to the price beneath it after that day. Absent means the override stands.

        • location_idstring · uuid

          Required

          The site this price applies at.

        • priceMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

      • price_schedulearray of ProductScheduledPrice

        Pending base-price changes (locations-and-groups item 14): the price the product sells at from a future date, resolved by the latest entry at or before the business date, falling back to price when none is in force. At most one future entry exists — a new schedule supersedes the previous one. price stays the price in force today.

        • effective_fromstring · date

          Required

          The business date this price takes effect on, in the location's own clock.

        • priceMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

      • recipeProductRecipe

        Nullable

        What this made item consumes (inventory item 13): one sold unit is made from its ingredients, each depleting its own stock level in the amount given, in the ingredient's own unit — 40 ml of vodka is "40". A product with a recipe is not stocked as itself. Only an item sold per each may carry one. null or absent means the product is stocked and sold as itself.

        • linesarray of object

          The ingredients of the made item, by product and amount per unit.

          ≥ 1 items

          • product_idstring · uuid

            Required

            The ingredient product, same merchant, and not the made item itself.

          • quantityQuantity

            Required

            How much of something, as a decimal string in the unit_of_measure beside it (each where there is none), as Square's quantity is: three croissants are "3", 1.4 kg of beans is "1.4". A string because a JSON number is a binary float to most parsers.

            It has at most as many decimal places as its unit's base unit holds — three in each, g and ml, six in kg and l — and a finer one is refused, never rounded. Tenvelo holds every quantity as an integer of that base unit (thousandths of an each, milligrams, microlitres; docs/decisions/0063); a client that computes with one should read the digits the same way, never through a float.

            ≤ 26 chars · pattern ^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$

          • unit_of_measurestring · enum

            The unit quantity is in, which must measure what the ingredient does. Absent means each.

            eachkgglml

      • skustring

        Merchant's own stock-keeping unit, if they use one.

        ≤ 64 chars

      • sold_onlineboolean

        Whether this product is offered on the online storefront, and so part of the location's public online menu. Off by default; a per-location 86 still withdraws it there.

        default false

      • sort_orderinteger · int32

        Where this product sits on the till's menu within its category. Lower comes first; equal orders sort by name.

        ≥ 0

      • statusstring · enum

        Required

        An inactive product stops appearing on tills but is never deleted, because orders already reference it and a receipt must remain reproducible.

        activeinactive

      • supplier_idstring · uuid

        Nullable

        The supplier this product is reordered from, if any: a suggested order groups products by it. Square calls the same link a variation's vendor (vendor_id); a product here has one.

      • tax_class_idstring · uuid

        Nullable

        The tax class this product belongs to, and the only way it is taxed. A class and a fulfilment type resolve to one or several rates through a tax rule, so a cold sandwich is zero-rated as takeaway and standard-rated eat-in without the menu being duplicated (docs/decisions/0014). Square lists an item's taxes directly (tax_ids); the class stands between them so the set can vary by fulfilment. Null means no tax applies — a deliberate statement, not an omission. A classed product with no rule for a fulfilment is a configuration error the back office shows, never a silent "no tax".

      • track_inventoryboolean

        Whether inventory is counted for this product. Coffee made to order is not tracked; a bag of beans on a shelf is.

        default false

      • track_serialboolean

        Whether the product's units carry serial numbers (retail item: serial numbers): each unit is one physical thing with its own code, and the till asks for it when the product is rung up, recording it on the line. Off by default; a presentation of the product, never a required entry.

        default false

      • translationsobject

        The product's name and description in other languages (0017, Merchant content; menu item 20), keyed by BCP 47 tag (es, fr, en-GB). The base name and description are the source language. They ride the change feed and the ETag like any catalogue field, and a guest reading the product is answered in the language they asked for — never the base text in a language they did not ask for. Each entry carries a source_digest the server computed from the base fields at write time; the back office compares it to a later edit and flags a translation left behind.

      • unit_of_measurestring · enum

        The unit the product is sold and counted in (inventory item 16). A café or bar that stocks by weight or volume names the unit so a fractional quantity is meaningful. Defaults to each.

        eachkgglml

        default each

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

      • variantsarray of object

        The named options a product is sold as — a size, a strength. Each carries its own absolute price, which replaces the base price when chosen. A product with variants requires one to be picked; the till offers them before the line is rung up.

        ≤ 50 items

        • idstring · uuid

          Required

        • namestring

          Required

          1–64 chars

        • barcodesarray of string

          The barcodes this variant can be scanned or searched as, in addition to the product's own. Each entry must be non-blank and unique; a code may not repeat across a product and its variants, because a scan must name one sellable unit. The till matches any of them.

        • costMoney

          Nullable

          What this variant costs the merchant, in the variant's currency, when one is known. A sale of the variant snapshots this rather than the product's cost, so a later cost change does not restate a past period. NULL until a merchant sets it; never negative when present.

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

        • priceMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

        • skustring

          The merchant's own stock-keeping unit for this variant, if they use one. The till's search matches it like the product's own SKU.

          ≤ 64 chars

      • versionVersion

        RequiredRead only

        Optimistic concurrency version, raised by the server on every write. A version is never lower than one already returned for the resource, though it may rise by more than one.

        Returned in the ETag response header as well as in the body. Send it back in If-Match on an update and the write is rejected with 412 if anyone else has written since — which is what stops two tills silently overwriting each other's edit to the same product when both come back online at once.

        ≥ 1

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 404

      No such resource, or none visible to this caller.

      Also returned instead of 403 where acknowledging existence would itself leak information — so a 404 does not prove the id is unused globally, only that it is not yours.

    • 409

      The request conflicts with current state — a uniqueness constraint, or an attempt to mutate an order that is already completed or voided.

      Unlike 412, re-reading and retrying unchanged will not resolve it; the request itself has to change.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 412

      The If-Match version did not match the resource's current version: someone else wrote since you read.

      This is the expected, healthy outcome of two clients editing the same record — not an error condition to suppress. Re-read the resource, re-apply your change on top of the new state, and retry with the fresh ETag.

    • 415

      The request Content-Type is not supported. This API accepts application/json on all mutations.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  31. DELETE/v1/products/{id}

    Delete a product

    No. deleteProductAuth catalog:write

    Soft-deletes a product and emits a deleted change so devices remove it from their tills.

    The row is retained rather than erased, because completed orders reference it and a receipt has to stay reproducible. To take a product off sale without deleting it, set status to inactive instead — that is the usual intent.

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

  32. GET/v1/products/{id}/history

    Get a product's history

    No. getProductHistoryAuth catalog:read

    Returns who changed a product and when, newest first, so an edit to the menu is attributable. Read from the change log, never a copy kept beside the product.

    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 product's history.

      ProductHistory · application/json
      • changesarray of ProductHistoryEntry

        Required

        ≤ 100 items

        • actor_namestring

          Required

          The member's or device's name, or an integration's subject. Empty for a change no principal made, such as a scheduled price taking effect.

        • occurred_atstring · date-time

          Required

        • opstring · enum

          Required

          createdupdateddeleted

        • versioninteger · int64

          Required

          The product's version after the change.

      • product_idstring · uuid

        Required

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 404

      No such resource, or none visible to this caller.

      Also returned instead of 403 where acknowledging existence would itself leak information — so a 404 does not prove the id is unused globally, only that it is not yours.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

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

  33. GET/v1/products/{id}/tax

    Get the tax on a product's price

    No. getProductTaxAuth catalog:read

    Returns the tax the product's class charges on its saved price for each fulfilment, and what the guest pays, rounded as the till rounds it, so a screen shows the tax without working it out itself.

    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 tax on the product's price.

      ProductTax · application/json
      • fulfillmentsarray of FulfillmentTax

        Required

        One entry for each fulfilment the class has a rule for at the product's price, in the order on premises, pickup, delivery. Empty for a product with no tax class.

        • fulfillment_typestring · enum

          Required

          on_premisespickupdelivery

        • taxMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

        • totalMoney

          Required

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

      • product_idstring · uuid

        Required

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 404

      No such resource, or none visible to this caller.

      Also returned instead of 403 where acknowledging existence would itself leak information — so a 404 does not prove the id is unused globally, only that it is not yours.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

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

  34. GET/v1/tax-classes

    List tax classes

    No. listTaxClassesAuth catalog:read

    Returns the merchant's tax classes, ordered by name then id.

    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 tax classes.

      TaxClassPage · application/json
      • has_moreboolean

        Required

        Whether another page follows.

      • itemsarray of TaxClass

        Required

        • idstring · uuid

          Required

        • namestring

          Required

          1–128 chars

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

        • descriptionstring

          The official guidance for this class — what "hot" means under VAT Notice 709/1 and its five tests — so a merchant can apply it correctly rather than guess at the boundary the law draws.

          ≤ 2000 chars

        • merchant_idstring · uuid

          RequiredRead only

        • statusstring · enum

          Required

          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.

        • versionVersion

          RequiredRead only

          Optimistic concurrency version, raised by the server on every write. A version is never lower than one already returned for the resource, though it may rise by more than one.

          Returned in the ETag response header as well as in the body. Send it back in If-Match on an update and the write is rejected with 412 if anyone else has written since — which is what stops two tills silently overwriting each other's edit to the same product when both come back online at once.

          ≥ 1

      • next_cursorstring

        Nullable

        Opaque cursor for the next page, or null on the last page. Pass it back verbatim as the cursor query parameter; its contents are not part of this contract.

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  35. POST/v1/tax-classes/batch-upsert

    Create or replace tax classes

    No. batchUpsertTaxClassesAuth catalog:write

    Creates tax classes at client-minted ids, or replaces existing ones at the version they were read at. A batch of one is how a single tax class is created.

    Each item carries its own idempotency_key and stands or falls on its own: an invalid item, or one written from a stale version, fails in its result and every other item is still applied. Only a malformed envelope refuses the request as a whole. Every change reaches devices through the change feed.

    Request body application/json · TaxClassBatchUpsertRequest

    • itemsarray of TaxClassBatchUpsertItem

      Required

      ≥ 1 items · ≤ 500 items

      • idempotency_keyItemIdempotencyKey

        Required

        A key for one item in a batch, minted by the client when it queues the item and reused on every attempt to send it. The item, not the request, is the unit of work: a retried batch returns the stored result for every item already applied under its key. A fresh key per attempt defeats the whole mechanism.

        Scoped per merchant, so keys need only be unique within a merchant's own traffic.

        16–255 chars

      • tax_classTaxClass

        Required

        What an item is for tax purposes: "hot food", "cold food", "alcohol", "soft drinks". A product has at most one class, and a rule maps a class and a fulfilment type to the rate in force (docs/decisions/0014). This is the merchant's own configuration; a market pack ships a starter set whose descriptions carry the official guidance.

        • idstring · uuid

          Required

        • namestring

          Required

          1–128 chars

        • descriptionstring

          The official guidance for this class — what "hot" means under VAT Notice 709/1 and its five tests — so a merchant can apply it correctly rather than guess at the boundary the law draws.

          ≤ 2000 chars

        • statusstring · enum

          Required

          activeinactive

      • versioninteger · int64

        The version this write was made from. Required to replace a class that exists, and refused as 412 when somebody else has written it since.

        ≥ 1

    Responses

    • 200

      The batch was processed. results says what happened to each item.

      BatchResponse · application/json
      • resultsarray of BatchResult

        Required

        • idstring · uuid

          The resource the item created or changed. Absent when it failed.

        • idempotency_keystring

          Required

          The item's key, echoed so a result can be matched by key as well as position.

        • problemProblem

          Nullable

          Why the item failed, as an RFC 9457 problem. A 422 means the item is malformed and will never succeed — drop it and show it to staff rather than retrying for ever. A 409 or a 5xx means try again later.

          • typestring · uri

            Required

            A stable URI identifying the problem kind. This is the field to branch on.

            default about:blank

          • api_versionstring

            Present on an api-version-retired or api-version-unknown refusal: the API-Version this server speaks (docs/decisions/0011).

          • detailstring

            A human-readable explanation specific to this occurrence. Safe to show to a developer; never contains internal state, SQL, or stack traces.

          • instancestring · uri-reference

            A URI reference identifying the specific occurrence.

          • invalid_paramsarray of InvalidParam

            Present on 422 responses. Lists every field that failed validation, rather than only the first, so a client can highlight an entire form in one pass.

            • codestring

              Stable machine-readable code for this failure, for clients that localise their own messages rather than showing reason.

            • pointerstring

              Required

              RFC 6901 JSON Pointer to the offending value within the request body. A pointer rather than a field name so that failures nested inside arrays — the third line item of an order — are addressable at all.

            • reasonstring

              Required

              Why this value was rejected, in human-readable form.

          • refusalsarray of Location refusal

            Present on 409 responses to a withdrawal refused by the state it depends on (deleting a location with tills still assigned, open orders, …). Lists every reason at once, each with how much stands in the way, so a client can say what to put right and link to it rather than walking a person through one refusal after another.

            • kindstring

              Required

              A stable refusal kind — devices, open_orders, drawer_sessions, bookings or stock — that a client links to the screen that puts it right. Branch on this field, never on the surrounding prose.

            • countinteger

              Required

              How many things stand in the way of the withdrawal, so a client can repeat the number rather than re-read prose.

              ≥ 1

          • statusinteger

            Required

            The HTTP status code, repeated here so the body is self-contained.

            100–599

          • titlestring

            Required

            A short, human-readable summary of the problem kind.

          • trace_idstring

            The OpenTelemetry trace id for this request. Quote it in a support ticket and the whole request can be pulled up.

        • statusstring · enum

          Required

          created — the item made a new resource. updated — it changed an existing one.

          unchanged — this key was already applied and the original outcome stands. A success, not an error: it is what a client retrying a batch it was unsure landed should expect, and it clears the item from its queue exactly as created does.

          failed — the item was refused, and nothing else in the batch was affected. See problem.

          createdupdatedunchangedfailed

        • versioninteger · int64

          The resource's version after the item. Absent when it failed.

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 415

      The request Content-Type is not supported. This API accepts application/json on all mutations.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  36. GET/v1/tax-rates

    List tax rates

    No. listTaxRatesAuth catalog:read

    Returns the merchant's tax rates.

    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 tax rates.

      TaxRatePage · application/json
      • has_moreboolean

        Required

        Whether another page follows.

      • itemsarray of TaxRate

        Required

        • idstring · uuid

          Required

        • kindstring · enum

          What the rate is: percentage of the price, or per_unit — a fixed amount per unit sold, for a levy that is not a percentage: a bag levy, a bottle deposit, a per-drink US tax (markets item 16). A per-unit rate carries per_unit and a percentage of zero; it is charged amount × quantity, exact, on top of the price, never compounding, and only with inclusive: false. Nothing else changes: it is a rate like any other, named by a rule per class and fulfilment type.

          percentageper_unit

          default percentage

        • namestring

          Required

          1–128 chars

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

        • effective_fromstring · date

          The date the rate takes effect, as a calendar date. A rate change is scheduled by creating a rate (and a rule naming it) with an effective_from in the future: it reaches every till on the change feed ahead of time, and a sale is taxed at the rate in force on its business date, not the current one (docs/decisions/0014).

        • inclusiveboolean

          Required

          Whether the tax is already contained in the product's price (true, the norm in the UK and EU) or added at the till (false, the norm in the US). This changes how a total is computed, so it is required rather than defaulted.

        • merchant_idstring · uuid

          RequiredRead only

        • per_unitMoney

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

        • percentagePercentage

          Required

          A share of a whole, from "0" to "100", as a percentage in a decimal string with at most four decimal places, as Square's percentage and Stripe Tax's percentage_decimal are: UK VAT at 20% is "20", and an 8.875% US sales tax is "8.875".

          A string because a JSON number is a binary float to most parsers, and a float cannot hold most decimal fractions exactly (RFC 7493 §2.2). Tenvelo computes with a percentage as an integer in parts per million ("8.875" is 88750), which every four-place percentage is exactly. A client that computes with one should read the digits the same way, never through a float.

          pattern ^(100(\.0{1,4})?|(0|[1-9][0-9]?)(\.[0-9]{1,4})?)$

        • roundingstring · enum

          How this rate's pennies round. A rate computes on its own base and rounds before the set a rule names is summed — each tax a line carries is a separate calculation with the mode its authority prescribes. half_up is the norm; half_even is what some US states ask for (banker's rounding).

          half_uphalf_even

          default half_up

        • statusstring · enum

          Required

          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.

        • versionVersion

          RequiredRead only

          Optimistic concurrency version, raised by the server on every write. A version is never lower than one already returned for the resource, though it may rise by more than one.

          Returned in the ETag response header as well as in the body. Send it back in If-Match on an update and the write is rejected with 412 if anyone else has written since — which is what stops two tills silently overwriting each other's edit to the same product when both come back online at once.

          ≥ 1

      • next_cursorstring

        Nullable

        Opaque cursor for the next page, or null on the last page. Pass it back verbatim as the cursor query parameter; its contents are not part of this contract.

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  37. POST/v1/tax-rates/batch-upsert

    Create or replace tax rates

    No. batchUpsertTaxRatesAuth catalog:write

    Creates tax rates at client-minted ids, or replaces existing ones at the version they were read at. A batch of one is how a single tax rate is created.

    Each item carries its own idempotency_key and stands or falls on its own: an invalid item, or one written from a stale version, fails in its result and every other item is still applied. Only a malformed envelope refuses the request as a whole. Every change reaches devices through the change feed.

    Request body application/json · TaxRateBatchUpsertRequest

    • itemsarray of TaxRateBatchUpsertItem

      Required

      ≥ 1 items · ≤ 500 items

      • idempotency_keyItemIdempotencyKey

        Required

        A key for one item in a batch, minted by the client when it queues the item and reused on every attempt to send it. The item, not the request, is the unit of work: a retried batch returns the stored result for every item already applied under its key. A fresh key per attempt defeats the whole mechanism.

        Scoped per merchant, so keys need only be unique within a merchant's own traffic.

        16–255 chars

      • tax_rateTaxRate

        Required

        A tax rate that can be applied to products.

        • idstring · uuid

          Required

        • kindstring · enum

          What the rate is: percentage of the price, or per_unit — a fixed amount per unit sold, for a levy that is not a percentage: a bag levy, a bottle deposit, a per-drink US tax (markets item 16). A per-unit rate carries per_unit and a percentage of zero; it is charged amount × quantity, exact, on top of the price, never compounding, and only with inclusive: false. Nothing else changes: it is a rate like any other, named by a rule per class and fulfilment type.

          percentageper_unit

          default percentage

        • namestring

          Required

          1–128 chars

        • effective_fromstring · date

          The date the rate takes effect, as a calendar date. A rate change is scheduled by creating a rate (and a rule naming it) with an effective_from in the future: it reaches every till on the change feed ahead of time, and a sale is taxed at the rate in force on its business date, not the current one (docs/decisions/0014).

        • inclusiveboolean

          Required

          Whether the tax is already contained in the product's price (true, the norm in the UK and EU) or added at the till (false, the norm in the US). This changes how a total is computed, so it is required rather than defaulted.

        • per_unitMoney

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

        • percentagePercentage

          Required

          A share of a whole, from "0" to "100", as a percentage in a decimal string with at most four decimal places, as Square's percentage and Stripe Tax's percentage_decimal are: UK VAT at 20% is "20", and an 8.875% US sales tax is "8.875".

          A string because a JSON number is a binary float to most parsers, and a float cannot hold most decimal fractions exactly (RFC 7493 §2.2). Tenvelo computes with a percentage as an integer in parts per million ("8.875" is 88750), which every four-place percentage is exactly. A client that computes with one should read the digits the same way, never through a float.

          pattern ^(100(\.0{1,4})?|(0|[1-9][0-9]?)(\.[0-9]{1,4})?)$

        • roundingstring · enum

          How this rate's pennies round. A rate computes on its own base and rounds before the set a rule names is summed — each tax a line carries is a separate calculation with the mode its authority prescribes. half_up is the norm; half_even is what some US states ask for (banker's rounding).

          half_uphalf_even

          default half_up

        • statusstring · enum

          Required

          activeinactive

      • versioninteger · int64

        The version this write was made from. Required to replace a rate that exists, and refused as 412 when somebody else has written it since.

        ≥ 1

    Responses

    • 200

      The batch was processed. results says what happened to each item.

      BatchResponse · application/json
      • resultsarray of BatchResult

        Required

        • idstring · uuid

          The resource the item created or changed. Absent when it failed.

        • idempotency_keystring

          Required

          The item's key, echoed so a result can be matched by key as well as position.

        • problemProblem

          Nullable

          Why the item failed, as an RFC 9457 problem. A 422 means the item is malformed and will never succeed — drop it and show it to staff rather than retrying for ever. A 409 or a 5xx means try again later.

          • typestring · uri

            Required

            A stable URI identifying the problem kind. This is the field to branch on.

            default about:blank

          • api_versionstring

            Present on an api-version-retired or api-version-unknown refusal: the API-Version this server speaks (docs/decisions/0011).

          • detailstring

            A human-readable explanation specific to this occurrence. Safe to show to a developer; never contains internal state, SQL, or stack traces.

          • instancestring · uri-reference

            A URI reference identifying the specific occurrence.

          • invalid_paramsarray of InvalidParam

            Present on 422 responses. Lists every field that failed validation, rather than only the first, so a client can highlight an entire form in one pass.

            • codestring

              Stable machine-readable code for this failure, for clients that localise their own messages rather than showing reason.

            • pointerstring

              Required

              RFC 6901 JSON Pointer to the offending value within the request body. A pointer rather than a field name so that failures nested inside arrays — the third line item of an order — are addressable at all.

            • reasonstring

              Required

              Why this value was rejected, in human-readable form.

          • refusalsarray of Location refusal

            Present on 409 responses to a withdrawal refused by the state it depends on (deleting a location with tills still assigned, open orders, …). Lists every reason at once, each with how much stands in the way, so a client can say what to put right and link to it rather than walking a person through one refusal after another.

            • kindstring

              Required

              A stable refusal kind — devices, open_orders, drawer_sessions, bookings or stock — that a client links to the screen that puts it right. Branch on this field, never on the surrounding prose.

            • countinteger

              Required

              How many things stand in the way of the withdrawal, so a client can repeat the number rather than re-read prose.

              ≥ 1

          • statusinteger

            Required

            The HTTP status code, repeated here so the body is self-contained.

            100–599

          • titlestring

            Required

            A short, human-readable summary of the problem kind.

          • trace_idstring

            The OpenTelemetry trace id for this request. Quote it in a support ticket and the whole request can be pulled up.

        • statusstring · enum

          Required

          created — the item made a new resource. updated — it changed an existing one.

          unchanged — this key was already applied and the original outcome stands. A success, not an error: it is what a client retrying a batch it was unsure landed should expect, and it clears the item from its queue exactly as created does.

          failed — the item was refused, and nothing else in the batch was affected. See problem.

          createdupdatedunchangedfailed

        • versioninteger · int64

          The resource's version after the item. Absent when it failed.

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 415

      The request Content-Type is not supported. This API accepts application/json on all mutations.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  38. GET/v1/tax-rates/{id}

    Get a tax

    No. getTaxRateAuth catalog:read

    Returns a single tax.

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

      TaxRate · application/json
      • idstring · uuid

        Required

      • kindstring · enum

        What the rate is: percentage of the price, or per_unit — a fixed amount per unit sold, for a levy that is not a percentage: a bag levy, a bottle deposit, a per-drink US tax (markets item 16). A per-unit rate carries per_unit and a percentage of zero; it is charged amount × quantity, exact, on top of the price, never compounding, and only with inclusive: false. Nothing else changes: it is a rate like any other, named by a rule per class and fulfilment type.

        percentageper_unit

        default percentage

      • namestring

        Required

        1–128 chars

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

      • effective_fromstring · date

        The date the rate takes effect, as a calendar date. A rate change is scheduled by creating a rate (and a rule naming it) with an effective_from in the future: it reaches every till on the change feed ahead of time, and a sale is taxed at the rate in force on its business date, not the current one (docs/decisions/0014).

      • inclusiveboolean

        Required

        Whether the tax is already contained in the product's price (true, the norm in the UK and EU) or added at the till (false, the norm in the US). This changes how a total is computed, so it is required rather than defaulted.

      • merchant_idstring · uuid

        RequiredRead only

      • per_unitMoney

        A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

        The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

        Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

        • amountinteger · int64

          Required

          The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

        • currencyCurrency

          Required

          ISO 4217 currency code.

          Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

          One of 182 values; see the schema.

      • percentagePercentage

        Required

        A share of a whole, from "0" to "100", as a percentage in a decimal string with at most four decimal places, as Square's percentage and Stripe Tax's percentage_decimal are: UK VAT at 20% is "20", and an 8.875% US sales tax is "8.875".

        A string because a JSON number is a binary float to most parsers, and a float cannot hold most decimal fractions exactly (RFC 7493 §2.2). Tenvelo computes with a percentage as an integer in parts per million ("8.875" is 88750), which every four-place percentage is exactly. A client that computes with one should read the digits the same way, never through a float.

        pattern ^(100(\.0{1,4})?|(0|[1-9][0-9]?)(\.[0-9]{1,4})?)$

      • roundingstring · enum

        How this rate's pennies round. A rate computes on its own base and rounds before the set a rule names is summed — each tax a line carries is a separate calculation with the mode its authority prescribes. half_up is the norm; half_even is what some US states ask for (banker's rounding).

        half_uphalf_even

        default half_up

      • statusstring · enum

        Required

        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.

      • versionVersion

        RequiredRead only

        Optimistic concurrency version, raised by the server on every write. A version is never lower than one already returned for the resource, though it may rise by more than one.

        Returned in the ETag response header as well as in the body. Send it back in If-Match on an update and the write is rejected with 412 if anyone else has written since — which is what stops two tills silently overwriting each other's edit to the same product when both come back online at once.

        ≥ 1

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 404

      No such resource, or none visible to this caller.

      Also returned instead of 403 where acknowledging existence would itself leak information — so a 404 does not prove the id is unused globally, only that it is not yours.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  39. PATCH/v1/tax-rates/{id}

    Change a tax rate

    No. updateTaxRateAuth catalog:write

    Applies a JSON Merge Patch (RFC 7396) to a tax rate, guarded by If-Match.

    Send only the fields that change. The patch is refused with 412 if the rate has changed since the version you read. A change reaches every till through the change feed; sales already made keep the rate they were sold at, which is snapshotted onto each line.

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

    • kindstring · enum

      What the rate is: percentage of the price, or a fixed amount per unit sold (markets item 16). Changing one to the other also sets that kind's fields.

      percentageper_unit

    • namestring

      1–128 chars

    • effective_fromstring · date

      The date the rate takes effect, as a calendar date. Sending it schedules the rate (and any rule naming it) for that date; a sale is taxed at the rate in force on its business date (docs/decisions/0014).

    • inclusiveboolean
    • per_unitMoney

      A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

      The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

      Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

      • amountinteger · int64

        Required

        The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

      • currencyCurrency

        Required

        ISO 4217 currency code.

        Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

        One of 182 values; see the schema.

    • percentagePercentage

      A share of a whole, from "0" to "100", as a percentage in a decimal string with at most four decimal places, as Square's percentage and Stripe Tax's percentage_decimal are: UK VAT at 20% is "20", and an 8.875% US sales tax is "8.875".

      A string because a JSON number is a binary float to most parsers, and a float cannot hold most decimal fractions exactly (RFC 7493 §2.2). Tenvelo computes with a percentage as an integer in parts per million ("8.875" is 88750), which every four-place percentage is exactly. A client that computes with one should read the digits the same way, never through a float.

      pattern ^(100(\.0{1,4})?|(0|[1-9][0-9]?)(\.[0-9]{1,4})?)$

    • statusstring · enum

      activeinactive

    Responses

    • 200

      The changed tax rate.

      TaxRate · application/json
      • idstring · uuid

        Required

      • kindstring · enum

        What the rate is: percentage of the price, or per_unit — a fixed amount per unit sold, for a levy that is not a percentage: a bag levy, a bottle deposit, a per-drink US tax (markets item 16). A per-unit rate carries per_unit and a percentage of zero; it is charged amount × quantity, exact, on top of the price, never compounding, and only with inclusive: false. Nothing else changes: it is a rate like any other, named by a rule per class and fulfilment type.

        percentageper_unit

        default percentage

      • namestring

        Required

        1–128 chars

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

      • effective_fromstring · date

        The date the rate takes effect, as a calendar date. A rate change is scheduled by creating a rate (and a rule naming it) with an effective_from in the future: it reaches every till on the change feed ahead of time, and a sale is taxed at the rate in force on its business date, not the current one (docs/decisions/0014).

      • inclusiveboolean

        Required

        Whether the tax is already contained in the product's price (true, the norm in the UK and EU) or added at the till (false, the norm in the US). This changes how a total is computed, so it is required rather than defaulted.

      • merchant_idstring · uuid

        RequiredRead only

      • per_unitMoney

        A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

        The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

        Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

        • amountinteger · int64

          Required

          The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

        • currencyCurrency

          Required

          ISO 4217 currency code.

          Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

          One of 182 values; see the schema.

      • percentagePercentage

        Required

        A share of a whole, from "0" to "100", as a percentage in a decimal string with at most four decimal places, as Square's percentage and Stripe Tax's percentage_decimal are: UK VAT at 20% is "20", and an 8.875% US sales tax is "8.875".

        A string because a JSON number is a binary float to most parsers, and a float cannot hold most decimal fractions exactly (RFC 7493 §2.2). Tenvelo computes with a percentage as an integer in parts per million ("8.875" is 88750), which every four-place percentage is exactly. A client that computes with one should read the digits the same way, never through a float.

        pattern ^(100(\.0{1,4})?|(0|[1-9][0-9]?)(\.[0-9]{1,4})?)$

      • roundingstring · enum

        How this rate's pennies round. A rate computes on its own base and rounds before the set a rule names is summed — each tax a line carries is a separate calculation with the mode its authority prescribes. half_up is the norm; half_even is what some US states ask for (banker's rounding).

        half_uphalf_even

        default half_up

      • statusstring · enum

        Required

        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.

      • versionVersion

        RequiredRead only

        Optimistic concurrency version, raised by the server on every write. A version is never lower than one already returned for the resource, though it may rise by more than one.

        Returned in the ETag response header as well as in the body. Send it back in If-Match on an update and the write is rejected with 412 if anyone else has written since — which is what stops two tills silently overwriting each other's edit to the same product when both come back online at once.

        ≥ 1

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 404

      No such resource, or none visible to this caller.

      Also returned instead of 403 where acknowledging existence would itself leak information — so a 404 does not prove the id is unused globally, only that it is not yours.

    • 409

      The request conflicts with current state — a uniqueness constraint, or an attempt to mutate an order that is already completed or voided.

      Unlike 412, re-reading and retrying unchanged will not resolve it; the request itself has to change.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 412

      The If-Match version did not match the resource's current version: someone else wrote since you read.

      This is the expected, healthy outcome of two clients editing the same record — not an error condition to suppress. Re-read the resource, re-apply your change on top of the new state, and retry with the fresh ETag.

    • 415

      The request Content-Type is not supported. This API accepts application/json on all mutations.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  40. DELETE/v1/tax-rates/{id}

    Delete a tax

    No. deleteTaxRateAuth catalog:write

    Soft-deletes a tax rate. Products referencing it become untaxed, so reassign them first.

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

  41. GET/v1/tax-rules

    List tax rules

    No. listTaxRulesAuth catalog:read

    Returns the merchant's tax rules — the rate each class carries for each fulfilment type, versioned by effective date — ordered by class name then fulfilment then id.

    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 tax rules.

      TaxRulePage · application/json
      • has_moreboolean

        Required

        Whether another page follows.

      • itemsarray of TaxRule

        Required

        • idstring · uuid

          Required

        • compoundingboolean

          Whether each rate in the set applies to the net plus the taxes before it (tax on tax) rather than all rates on the price. Only an exclusive rule may compound — an inclusive tax is inside the price, so there is nothing earlier to build on — and a rule mixing the two is refused. Defaults to false: state + county + city are all on the price, and a rule that compounds when the authority does not is a silent overcharge.

          default false

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

        • effective_fromstring · date

          The date this rule takes effect, as a calendar date. A dated schedule for a class and fulfilment is a set of rules with different dates; the rule in force on a sale's business date is the one with the greatest effective_from at or before it (docs/decisions/0014).

        • exempt_rate_idsarray of string · uuid

          The rates in rate_ids this rule does not charge — the class is exempt from them: the US shape is groceries taxed by the state but not by the city. An exempt rate exists at its percentage for classes that do not have it exempt, contributes nothing to a line, and is what separates an exempt class from a zero-rated one in reporting.

          default []

        • fulfillment_typestring · enum

          Required

          How the sale leaves the building, as the glossary names it: on_premises is what the law calls eat-in, pickup is takeaway and delivery is delivery. This is the distinction that makes a cold sandwich zero-rated as takeaway and standard-rated eat-in.

          on_premisespickupdelivery

        • merchant_idstring · uuid

          RequiredRead only

        • price_fromMoney

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

        • price_toMoney

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

        • rate_idsarray of string · uuid

          Required

          The ordered set of rates the class carries for this fulfilment. Zero-rated food still names a rate at 0%: "no rule" and "zero rate" are different things, and only the former is a configuration error. All rates share the same inclusivity (inclusive or exclusive), because a line is taxed inside its price or on top of it; a rule mixing the two is refused.

          ≥ 1 items

        • statusstring · enum

          Required

          activeinactive

        • tax_class_idstring · uuid

          Required

          The class this rule resolves a rate for.

        • updated_atTimestamp

          RequiredRead only

          Server-assigned RFC 3339 timestamp, always UTC.

          Server-assigned because device clocks drift and cannot be trusted for ordering — a till that has been powered off for a week may come back with a clock hours out. Where the device's own view of time matters it is carried separately in client_created_at.

        • versionVersion

          RequiredRead only

          Optimistic concurrency version, raised by the server on every write. A version is never lower than one already returned for the resource, though it may rise by more than one.

          Returned in the ETag response header as well as in the body. Send it back in If-Match on an update and the write is rejected with 412 if anyone else has written since — which is what stops two tills silently overwriting each other's edit to the same product when both come back online at once.

          ≥ 1

      • next_cursorstring

        Nullable

        Opaque cursor for the next page, or null on the last page. Pass it back verbatim as the cursor query parameter; its contents are not part of this contract.

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.

  42. POST/v1/tax-rules/batch-upsert

    Create or replace tax rules

    No. batchUpsertTaxRulesAuth catalog:write

    Creates tax rules at client-minted ids, or replaces existing ones at the version they were read at. A batch of one is how a single tax rule is created.

    Each item carries its own idempotency_key and stands or falls on its own: an invalid item, or one written from a stale version, fails in its result and every other item is still applied. Only a malformed envelope refuses the request as a whole. Every change reaches devices through the change feed.

    A change of rate from a date is a new rule with that effective_from, never an edit of the rule in force: the sales already rung under the old one keep the rate they were taxed at.

    Request body application/json · TaxRuleBatchUpsertRequest

    • itemsarray of TaxRuleBatchUpsertItem

      Required

      ≥ 1 items · ≤ 500 items

      • idempotency_keyItemIdempotencyKey

        Required

        A key for one item in a batch, minted by the client when it queues the item and reused on every attempt to send it. The item, not the request, is the unit of work: a retried batch returns the stored result for every item already applied under its key. A fresh key per attempt defeats the whole mechanism.

        Scoped per merchant, so keys need only be unique within a merchant's own traffic.

        16–255 chars

      • tax_ruleTaxRule

        Required

        Which rates a tax class carries for a fulfilment type — the eat-in/takeaway case is a rule, not a second product (docs/decisions/0014). A pair with no rule is a configuration error the back office shows; it never silently means "no tax". One rule names one or several rates: the several-taxes shape is the ordered set, applied in order with the combined rate as the sum, and every rate in a rule shares the same inclusivity so a line's "inside or on top of the price" stays one answer.

        • idstring · uuid

          Required

        • compoundingboolean

          Whether each rate in the set applies to the net plus the taxes before it (tax on tax) rather than all rates on the price. Only an exclusive rule may compound — an inclusive tax is inside the price, so there is nothing earlier to build on — and a rule mixing the two is refused. Defaults to false: state + county + city are all on the price, and a rule that compounds when the authority does not is a silent overcharge.

          default false

        • effective_fromstring · date

          The date this rule takes effect, as a calendar date. A dated schedule for a class and fulfilment is a set of rules with different dates; the rule in force on a sale's business date is the one with the greatest effective_from at or before it (docs/decisions/0014).

        • exempt_rate_idsarray of string · uuid

          The rates in rate_ids this rule does not charge — the class is exempt from them: the US shape is groceries taxed by the state but not by the city. An exempt rate exists at its percentage for classes that do not have it exempt, contributes nothing to a line, and is what separates an exempt class from a zero-rated one in reporting.

          default []

        • fulfillment_typestring · enum

          Required

          How the sale leaves the building, as the glossary names it: on_premises is what the law calls eat-in, pickup is takeaway and delivery is delivery. This is the distinction that makes a cold sandwich zero-rated as takeaway and standard-rated eat-in.

          on_premisespickupdelivery

        • price_fromMoney

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

        • price_toMoney

          A monetary amount in a specific currency: Square's Money, and Stripe's amount beside its currency.

          The amount is always an integer count of the currency's minor unit — pence for GBP, cents for USD. Zero-decimal currencies such as JPY have a minor unit equal to their major unit, so {"amount": 500, "currency": "JPY"} is ¥500 while {"amount": 500, "currency": "GBP"} is £5.00.

          Floating point is never used for money anywhere in this API. Binary floats cannot represent most decimal fractions exactly, so accumulating them across the line items of an order produces totals that disagree with the printed receipt by a penny. Integer minor units make every arithmetic operation exact.

          • amountinteger · int64

            Required

            The amount, as a whole number of the currency's minor unit. May be negative to express a refund, discount or credit.

          • currencyCurrency

            Required

            ISO 4217 currency code.

            Determines the minor unit that Money.amount is expressed in. Zero-decimal currencies such as JPY have a minor unit equal to the major unit.

            One of 182 values; see the schema.

        • rate_idsarray of string · uuid

          Required

          The ordered set of rates the class carries for this fulfilment. Zero-rated food still names a rate at 0%: "no rule" and "zero rate" are different things, and only the former is a configuration error. All rates share the same inclusivity (inclusive or exclusive), because a line is taxed inside its price or on top of it; a rule mixing the two is refused.

          ≥ 1 items

        • statusstring · enum

          Required

          activeinactive

        • tax_class_idstring · uuid

          Required

          The class this rule resolves a rate for.

      • versioninteger · int64

        The version this write was made from. Required to replace a rule that exists, and refused as 412 when somebody else has written it since.

        ≥ 1

    Responses

    • 200

      The batch was processed. results says what happened to each item.

      BatchResponse · application/json
      • resultsarray of BatchResult

        Required

        • idstring · uuid

          The resource the item created or changed. Absent when it failed.

        • idempotency_keystring

          Required

          The item's key, echoed so a result can be matched by key as well as position.

        • problemProblem

          Nullable

          Why the item failed, as an RFC 9457 problem. A 422 means the item is malformed and will never succeed — drop it and show it to staff rather than retrying for ever. A 409 or a 5xx means try again later.

          • typestring · uri

            Required

            A stable URI identifying the problem kind. This is the field to branch on.

            default about:blank

          • api_versionstring

            Present on an api-version-retired or api-version-unknown refusal: the API-Version this server speaks (docs/decisions/0011).

          • detailstring

            A human-readable explanation specific to this occurrence. Safe to show to a developer; never contains internal state, SQL, or stack traces.

          • instancestring · uri-reference

            A URI reference identifying the specific occurrence.

          • invalid_paramsarray of InvalidParam

            Present on 422 responses. Lists every field that failed validation, rather than only the first, so a client can highlight an entire form in one pass.

            • codestring

              Stable machine-readable code for this failure, for clients that localise their own messages rather than showing reason.

            • pointerstring

              Required

              RFC 6901 JSON Pointer to the offending value within the request body. A pointer rather than a field name so that failures nested inside arrays — the third line item of an order — are addressable at all.

            • reasonstring

              Required

              Why this value was rejected, in human-readable form.

          • refusalsarray of Location refusal

            Present on 409 responses to a withdrawal refused by the state it depends on (deleting a location with tills still assigned, open orders, …). Lists every reason at once, each with how much stands in the way, so a client can say what to put right and link to it rather than walking a person through one refusal after another.

            • kindstring

              Required

              A stable refusal kind — devices, open_orders, drawer_sessions, bookings or stock — that a client links to the screen that puts it right. Branch on this field, never on the surrounding prose.

            • countinteger

              Required

              How many things stand in the way of the withdrawal, so a client can repeat the number rather than re-read prose.

              ≥ 1

          • statusinteger

            Required

            The HTTP status code, repeated here so the body is self-contained.

            100–599

          • titlestring

            Required

            A short, human-readable summary of the problem kind.

          • trace_idstring

            The OpenTelemetry trace id for this request. Quote it in a support ticket and the whole request can be pulled up.

        • statusstring · enum

          Required

          created — the item made a new resource. updated — it changed an existing one.

          unchanged — this key was already applied and the original outcome stands. A success, not an error: it is what a client retrying a batch it was unsure landed should expect, and it clears the item from its queue exactly as created does.

          failed — the item was refused, and nothing else in the batch was affected. See problem.

          createdupdatedunchangedfailed

        • versioninteger · int64

          The resource's version after the item. Absent when it failed.

    • 400

      The request could not be parsed. Malformed JSON, a bad query parameter, or a header that failed to decode, among them an API-Version later than the server speaks or not a date (api-version-unknown, with the server's own in api_version).

      Distinct from 422: a 400 never reached validation, so invalid_params is absent. Retrying without changing the request will fail again, except that a request refused for its API-Version goes once the server is updated.

    • 401

      No credentials, or credentials that could not be verified — missing bearer token, malformed JWT, expired token, or a signature that failed against the issuer's JWKS.

      Refresh the token and retry once. Repeated 401 after a refresh means the device needs to re-enrol.

    • 403

      Authenticated, but not permitted. The token is valid and its merchant was resolved, but the caller's role does not allow this operation, or the target belongs to a different merchant.

      Cross-tenant access is reported as 403 on a resource the caller may not see, and as 404 where even confirming existence would leak information. Retrying will not help.

    • 410

      The request names an API-Version this server no longer serves (docs/decisions/0011). Nothing in it was read. api_version names the version the server speaks: the client is behind, and sends the request again once it is updated.

    • 415

      The request Content-Type is not supported. This API accepts application/json on all mutations.

    • 422

      The request parsed but failed validation.

      invalid_params lists every failing field with a JSON Pointer, not just the first, so a client can mark up a whole form in one pass rather than making the user fix errors one round trip at a time.

      Also returned when an Idempotency-Key is replayed with a different body, and when device-computed order totals disagree with the server's recomputation.

    • 429

      Rate limit exceeded. Honour Retry-After rather than applying your own backoff.

      A fleet of tills coming back online together after an outage is the realistic cause, which is exactly when uncoordinated client-side retry turns a brief outage into a sustained one. The limit is per process, and per site for what records trade there — a request spends its site's budget, and a till's own smaller one inside it — and per (client, merchant) for everything else; no RateLimit field is sent, so this response and its Retry-After are the whole of the contract.

    • 500

      Something failed on our side. trace_id identifies the request in our tracing — quote it in a support ticket.

      The body never carries internal detail: no SQL, no stack traces, no upstream error strings. Safe to retry with the same Idempotency-Key.

    • 503

      Temporarily unable to serve the request — a dependency is down or the service is shedding load.

      Retry with the same Idempotency-Key after Retry-After. This is always safe: a request that did take effect before the failure will be recognised as a replay.