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

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

Merchant · Reference

Merchant

The calling merchant's own record, and the milestones that say how far they have got set up.

Operations
6
Base
https://api.tenvelo.com
  1. GET/v1/markets

    List the markets a business can open in

    No. getMarketsAuth no token

    The countries a person may open a business in themselves, with the currency it trades in, in the order to offer them. Public: the sign-up page asks before anybody has a business.

    Responses

    • 200

      The markets.

      MarketList · application/json
      • has_moreboolean

        Required

        Whether another page follows. Always false: the list is finite.

      • itemsarray of Market

        Required

        • currencystring

          Required

          The currency a business there trades in, ISO 4217.

        • territorystring

          Required

          The country, ISO 3166-1 alpha-2.

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

  2. POST/v1/merchants

    Open a business

    No. createMerchantAuth bearer

    Opens a business with the signed-in person as its owner (0026). It takes a token for the person and no business (0107); once opened, the app asks the issuer for a token for it. A sandbox business takes cards through the simulated processor and is a tenant of its own, so nothing it does reaches a live business. Rate limited per client address.

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

    • business_namestring

      Required

      1–200 chars

    • business_typestring · enum

      Required

      The legal form of the business.

      companyindividualnon_profit

    • sandboxboolean

      A business to try things in, whose cards go to the simulated processor.

    • territorystring

      The market it opens in, as one GET /v1/markets lists. Left out, the first market listed.

      pattern ^[A-Z]{2}$

    • trade_profilestring · enum

      What it trades as, which decides the screens it starts with. Left out, it starts with every screen.

      hospitalityretail

    Responses

    • 201

      The business is open.

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

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

  3. GET/v1/merchants/me

    Get the current merchant

    No. getCurrentMerchantAuth bearer

    Returns the merchant the caller's token belongs to.

    There is deliberately no GET /v1/merchants returning a list, and no GET /v1/merchants/{id}. The merchant is not a parameter — it is derived from the bearer token and cannot be chosen by the caller. Exposing an id-addressed endpoint would create a surface on which a missing authorization check becomes cross-tenant data access; /me has no such surface to get wrong.

    Responses

    • 200

      The merchant.

      Merchant · application/json
      • idstring · uuid

        RequiredRead only

        UUIDv7 identifying the merchant. Server-assigned for merchants, since a merchant is never created offline.

      • addressAddress

        A postal address.

        The address_line_* fields carry the most specific part of the address — street number, street name, building name. Less specific detail belongs in locality, administrative_district_level_* and country.

        The administrative_district_level_* fields are deliberately generic because their meaning is country-dependent: in the US level 1 is the state and level 2 the county; in the UK level 1 is the county.

        • address_line_1string

          Required

          First line of the address — street number, street name, building name.

          ≤ 255 chars

        • address_line_2string

          Second line of the address, if any.

          ≤ 255 chars

        • address_line_3string

          Third line of the address, if any.

          ≤ 255 chars

        • administrative_district_level_1string

          A civil entity within the country. The state in the US; the county in the UK.

          ≤ 128 chars

        • administrative_district_level_2string

          A civil entity within administrative_district_level_1.

          ≤ 128 chars

        • countryCountry

          Required

          ISO 3166-1 alpha-2 country code.

          ZZ is the unknown/unspecified placeholder.

          One of 250 values; see the schema.

        • localitystring

          The city or town.

          ≤ 128 chars

        • postal_codestring

          The postal or ZIP code.

          ≤ 32 chars

        • sublocalitystring

          A civil region within the locality, if any.

          ≤ 128 chars

      • business_namestring

        Required

        The registered legal name of the business.

        1–255 chars

      • business_typestring · enum

        Required

        The legal form of the business. Determines which onboarding evidence is required and, for payfac purposes, which KYB checks apply.

        companyindividualnon_profit

      • comp_requires_managerboolean

        When true, a staff member may not comp an item (give it away at no charge) without a manager or owner authorising it. Defaults to false — a comp is a distinct give-away rather than a reduction.

      • company_numberstring

        Registered company number, where the jurisdiction issues one.

        ≤ 64 chars

      • countryCountry

        Required

        ISO 3166-1 alpha-2 country code.

        ZZ is the unknown/unspecified placeholder.

        One of 250 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.

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

      • discount_approval_thresholdMoney

        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.

      • discount_approval_threshold_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})?)$

      • gift_card_expiry_daysinteger · int32

        The number of days a newly issued gift card is redeemable, or zero for a card that does not expire (gift cards item 18). Snapshotted onto the card at issue, so changing it never shortens a card already sold. Carried so the till can say the term on a credit-note slip it prints at the counter (retail item 9).

        ≥ 0

      • gift_card_offline_capMoney

        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.

      • language_codeLanguageCode

        IETF BCP 47 language tag.

        Used for receipts, dashboard copy and buyer-facing text.

        One of 30 values; see the schema.

      • livemodeboolean

        RequiredRead only

        False for a test business: one a developer or a partner trades against without real money, kept apart from live trading. Stripe's livemode. Chosen at sign-up and never changed.

      • main_location_idstring · uuid

        Read only

        The business's first location, the one a link made for the whole business opens on. Square's main_location_id. Absent until the business has a location.

      • manager_discount_approval_thresholdMoney

        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.

      • manager_discount_approval_threshold_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})?)$

      • manager_refund_approval_thresholdMoney

        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.

      • over_short_thresholdMoney

        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.

      • parent_merchant_idstring · uuid

        Read only

        The brand this merchant is a franchisee of, when it is one. Read-only: a franchisee is made by its brand, not itself, so the write is a brand's job and this is exposed so an integration or a franchisee can see who they belong to.

      • price_spike_threshold_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})?)$

      • refund_approval_thresholdMoney

        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.

      • return_window_daysinteger · int32

        The number of days within which a shop accepts a return, or zero for no window (retail item 10). The till warns rather than refuses a refund outside it, and a sale of a gift card or a perishable is exempt. Prints on the receipt from the same setting.

        ≥ 0

      • statusstring · enum

        Required

        Whether the merchant can transact. An inactive merchant's devices are refused at the point of sale.

        activeinactive

      • tax_registration_numberstring

        The merchant's tax registration number (a VAT number in the UK), which a legally sufficient receipt must show. Distinct from company_number, which is a corporate-registry id and not a tax number.

        ≤ 64 chars

      • trading_namestring

        The name the business trades under, when it differs from the legal name. This is what appears on receipts.

        ≤ 255 chars

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

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

  4. PATCH/v1/merchants/me

    Change the current merchant

    No. updateCurrentMerchantAuth merchant:write

    Applies a JSON Merge Patch (RFC 7396) to the caller's own merchant, guarded by If-Match on the version read. A field set to null is cleared.

    Who may change a field depends on what it is. The business's names, numbers, address and language, and the drawer's over/short threshold, are the owner's. The limits the tills work to are a manager's as well: what needs a manager's approval, the returns window, the gift card terms and the price alert. A patch that touches anything its sender may not change is refused whole.

    Parameters

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

    • business_namestring

      1–255 chars

    • comp_requires_managerboolean
    • company_numberstring

      ≤ 64 chars

    • discount_approval_thresholdMoney

      Nullable

      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.

    • discount_approval_threshold_percentagePercentage

      Nullable

      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})?)$

    • gift_card_expiry_daysinteger · int32

      ≥ 0

    • gift_card_offline_capMoney

      Nullable

      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.

    • language_codeLanguageCode

      IETF BCP 47 language tag.

      Used for receipts, dashboard copy and buyer-facing text.

      One of 30 values; see the schema.

    • manager_discount_approval_thresholdMoney

      Nullable

      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.

    • manager_discount_approval_threshold_percentagePercentage

      Nullable

      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})?)$

    • manager_refund_approval_thresholdMoney

      Nullable

      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.

    • over_short_thresholdMoney

      Nullable

      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_spike_threshold_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})?)$

    • refund_approval_thresholdMoney

      Nullable

      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.

    • return_window_daysinteger · int32

      ≥ 0

    • tax_registration_numberstring

      ≤ 64 chars

    • trading_namestring

      ≤ 255 chars

    Responses

    • 200

      The merchant as changed.

      Merchant · application/json
      • idstring · uuid

        RequiredRead only

        UUIDv7 identifying the merchant. Server-assigned for merchants, since a merchant is never created offline.

      • addressAddress

        A postal address.

        The address_line_* fields carry the most specific part of the address — street number, street name, building name. Less specific detail belongs in locality, administrative_district_level_* and country.

        The administrative_district_level_* fields are deliberately generic because their meaning is country-dependent: in the US level 1 is the state and level 2 the county; in the UK level 1 is the county.

        • address_line_1string

          Required

          First line of the address — street number, street name, building name.

          ≤ 255 chars

        • address_line_2string

          Second line of the address, if any.

          ≤ 255 chars

        • address_line_3string

          Third line of the address, if any.

          ≤ 255 chars

        • administrative_district_level_1string

          A civil entity within the country. The state in the US; the county in the UK.

          ≤ 128 chars

        • administrative_district_level_2string

          A civil entity within administrative_district_level_1.

          ≤ 128 chars

        • countryCountry

          Required

          ISO 3166-1 alpha-2 country code.

          ZZ is the unknown/unspecified placeholder.

          One of 250 values; see the schema.

        • localitystring

          The city or town.

          ≤ 128 chars

        • postal_codestring

          The postal or ZIP code.

          ≤ 32 chars

        • sublocalitystring

          A civil region within the locality, if any.

          ≤ 128 chars

      • business_namestring

        Required

        The registered legal name of the business.

        1–255 chars

      • business_typestring · enum

        Required

        The legal form of the business. Determines which onboarding evidence is required and, for payfac purposes, which KYB checks apply.

        companyindividualnon_profit

      • comp_requires_managerboolean

        When true, a staff member may not comp an item (give it away at no charge) without a manager or owner authorising it. Defaults to false — a comp is a distinct give-away rather than a reduction.

      • company_numberstring

        Registered company number, where the jurisdiction issues one.

        ≤ 64 chars

      • countryCountry

        Required

        ISO 3166-1 alpha-2 country code.

        ZZ is the unknown/unspecified placeholder.

        One of 250 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.

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

      • discount_approval_thresholdMoney

        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.

      • discount_approval_threshold_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})?)$

      • gift_card_expiry_daysinteger · int32

        The number of days a newly issued gift card is redeemable, or zero for a card that does not expire (gift cards item 18). Snapshotted onto the card at issue, so changing it never shortens a card already sold. Carried so the till can say the term on a credit-note slip it prints at the counter (retail item 9).

        ≥ 0

      • gift_card_offline_capMoney

        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.

      • language_codeLanguageCode

        IETF BCP 47 language tag.

        Used for receipts, dashboard copy and buyer-facing text.

        One of 30 values; see the schema.

      • livemodeboolean

        RequiredRead only

        False for a test business: one a developer or a partner trades against without real money, kept apart from live trading. Stripe's livemode. Chosen at sign-up and never changed.

      • main_location_idstring · uuid

        Read only

        The business's first location, the one a link made for the whole business opens on. Square's main_location_id. Absent until the business has a location.

      • manager_discount_approval_thresholdMoney

        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.

      • manager_discount_approval_threshold_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})?)$

      • manager_refund_approval_thresholdMoney

        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.

      • over_short_thresholdMoney

        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.

      • parent_merchant_idstring · uuid

        Read only

        The brand this merchant is a franchisee of, when it is one. Read-only: a franchisee is made by its brand, not itself, so the write is a brand's job and this is exposed so an integration or a franchisee can see who they belong to.

      • price_spike_threshold_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})?)$

      • refund_approval_thresholdMoney

        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.

      • return_window_daysinteger · int32

        The number of days within which a shop accepts a return, or zero for no window (retail item 10). The till warns rather than refuses a refund outside it, and a sale of a gift card or a perishable is exempt. Prints on the receipt from the same setting.

        ≥ 0

      • statusstring · enum

        Required

        Whether the merchant can transact. An inactive merchant's devices are refused at the point of sale.

        activeinactive

      • tax_registration_numberstring

        The merchant's tax registration number (a VAT number in the UK), which a legally sufficient receipt must show. Distinct from company_number, which is a corporate-registry id and not a tax number.

        ≤ 64 chars

      • trading_namestring

        The name the business trades under, when it differs from the legal name. This is what appears on receipts.

        ≤ 255 chars

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

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

  5. GET/v1/milestones

    List the merchant's milestones

    No. getMilestonesAuth milestones:read

    Returns every milestone the merchant's tier lets them see, answered from their own data at read time — never recorded when a feature is used, so a merchant who did the thing before this existed is already met, and deleting the thing a milestone is about brings it back.

    A milestone whose tier the merchant has not bought is not returned: the same check the back office rail and gateSuite make, so the list never argues with the gate. Milestones are the merchant's own account, so the operation is bearer-only, like the current merchant.

    Responses

    • 200

      The merchant's milestones.

      MilestoneList · application/json
      • has_moreboolean

        Required

        Whether another page follows. Always false: the list is finite.

      • itemsarray of Milestone

        Required

        • dismissedboolean

          Whether THIS caller waved it away, permanently — the one thing adoption stores. Per user; a manager's dismissal never hides it from the owner.

        • evidencestring

          Required

          The case in the merchant's own numbers — what the question asked and what it found, so the reason is theirs rather than ours.

        • keystring

          Required

          The catalogue key, e.g. menu or drawer-counted. Adding a milestone is a row in the catalogue, so this is stable.

        • met_atstring · date-time

          Nullable

          When the thing first became true, where the question can know it; omitted when it can not (there is no moment a merchant "had a tax rate").

        • screenstring

          Required

          The back office destination that does the thing — a milestone points at a rail row, so it is a path, not a copy of an action.

        • setup_track_positioninteger · int32

          Read only

          Where it stands in the setup track a new business works through first, 1 being the first; omitted for a milestone outside the track. The order is the server's, so every screen shows the track the same way.

          ≥ 1

        • sites_metinteger · int32

          A shop-scoped milestone's count: how many of those locations have done the thing. Omitted with sites_total, and both are 1 of 1 for a single-location merchant — the boolean state stays the whole answer there.

          ≥ 0

        • sites_totalinteger · int32

          A shop-scoped milestone's total: how many locations the merchant has. Omitted on a merchant-scoped milestone (a tax rate is the business's, wherever it filed).

          ≥ 1

        • statestring · enum

          Required

          The derived answer.

          metunmet

        • suggestedboolean

          RequiredRead only

          Whether this is the one next step suggested now, chosen from the merchant's own data (adoption, item 5). At most one is, and none once there is nothing worth saying; the choice is the server's, so no screen works it out for itself.

        • tierBillingTier

          The packaging a merchant is on. Free is the till and back office with cash-up and Z reports; Plus adds tables, kitchen, bookings, customer records, rotas and several sites; Pro adds online and QR ordering, loyalty, gift cards, stock, and payroll and accounts exports.

        • titlestring

          Required

          What the thing is, in the words the rail's destination uses.

        • versioninteger · int64

          Required

          The dismissal's version, 0 while never dismissed — the value to send back in If-Match on the patch. It is the stored resource's version, not the derived state's; derived state has no version because it has no history.

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

  6. PATCH/v1/milestones/{key}

    Dismiss or restore a milestone

    No. updateMilestoneAuth milestones:write

    Applies a JSON Merge Patch (RFC 7396) to a milestone, guarded by If-Match — the one thing adoption stores.

    Only dismissed is patchable: achievement is derived from the merchant's own data at read time, so nothing else is written. A dismissal is per caller and permanent; a manager waving a suggestion away never hides it from the owner who would have acted on it. The version is the dismissal's (0 while never dismissed): re-read the list, send If-Match with the version you saw, and a 412 means someone else wrote since — re-read and re-apply.

    Parameters

    • keypath · string

      Required

      The catalogue key, e.g. menu.

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

    • dismissedboolean

      Required

      Wave this milestone away for the caller, permanently; false takes the wave back. Per user, never per merchant.

    Responses

    • 200

      The changed milestone, answered afresh.

      Milestone · application/json
      • dismissedboolean

        Whether THIS caller waved it away, permanently — the one thing adoption stores. Per user; a manager's dismissal never hides it from the owner.

      • evidencestring

        Required

        The case in the merchant's own numbers — what the question asked and what it found, so the reason is theirs rather than ours.

      • keystring

        Required

        The catalogue key, e.g. menu or drawer-counted. Adding a milestone is a row in the catalogue, so this is stable.

      • met_atstring · date-time

        Nullable

        When the thing first became true, where the question can know it; omitted when it can not (there is no moment a merchant "had a tax rate").

      • screenstring

        Required

        The back office destination that does the thing — a milestone points at a rail row, so it is a path, not a copy of an action.

      • setup_track_positioninteger · int32

        Read only

        Where it stands in the setup track a new business works through first, 1 being the first; omitted for a milestone outside the track. The order is the server's, so every screen shows the track the same way.

        ≥ 1

      • sites_metinteger · int32

        A shop-scoped milestone's count: how many of those locations have done the thing. Omitted with sites_total, and both are 1 of 1 for a single-location merchant — the boolean state stays the whole answer there.

        ≥ 0

      • sites_totalinteger · int32

        A shop-scoped milestone's total: how many locations the merchant has. Omitted on a merchant-scoped milestone (a tax rate is the business's, wherever it filed).

        ≥ 1

      • statestring · enum

        Required

        The derived answer.

        metunmet

      • suggestedboolean

        RequiredRead only

        Whether this is the one next step suggested now, chosen from the merchant's own data (adoption, item 5). At most one is, and none once there is nothing worth saying; the choice is the server's, so no screen works it out for itself.

      • tierBillingTier

        The packaging a merchant is on. Free is the till and back office with cash-up and Z reports; Plus adds tables, kitchen, bookings, customer records, rotas and several sites; Pro adds online and QR ordering, loyalty, gift cards, stock, and payroll and accounts exports.

      • titlestring

        Required

        What the thing is, in the words the rail's destination uses.

      • versioninteger · int64

        Required

        The dismissal's version, 0 while never dismissed — the value to send back in If-Match on the patch. It is the stored resource's version, not the derived state's; derived state has no version because it has no history.

    • 400

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

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

    • 401

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

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

    • 403

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

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

    • 404

      No such resource, or none visible to this caller.

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

    • 410

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

    • 412

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

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

    • 429

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

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

    • 500

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

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

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