Appearance
API changelog
Every entry is dated. Newest first.
The compatibility promise
Inside /v1 we only add. That means:
- new endpoints, new optional request fields and new response fields can appear at any time;
- an existing field will not be removed, renamed, or change type or meaning;
- an existing endpoint will not change its status codes or its error
codevocabulary for the same condition; - a webhook event's existing
datafields will not change shape — new fields may be added.
So: ignore fields you do not recognise, and do not validate responses with a strict schema that rejects unknown keys. An integration that treats a new optional field as an error breaks on our next additive release, and that is the one avoidable failure mode in this list.
Anything that cannot be done additively goes to /api/public/v2 and both versions run side by side. There is no plan for a v2.
Webhook payloads are versioned separately and per webhook: an existing webhook keeps the payload version it was created with, byte for byte, until you change it deliberately. See Webhooks → Payload versions.
Entries
2026-10-08 — card.expired now fires
card.expired has been in the catalogue since payload v2, but nothing ever sent it. It now does: a sweep running every 15 minutes sends it once per expiry for every card whose expires_at has passed (the end of the local day it was set to expire on). If the expiry is extended and then lapses again, the event is sent again. Cards that had already expired before today are not announced retroactively.
Its shape is final as of this first delivery, and it differs from the shape the catalogue described while nothing sent it:
datais{ card_instance_id, expired_at }.expired_atis the card'sexpires_at. There is notransaction_id: expiry writes nothing to the ledger, so the envelope'stransactionisnullandactorissystem.- It is a shop-wide event (
location: null). A location-scoped webhook receives it whileinclude_shop_wide_eventsis on, which is the default. - It is no longer mutually exclusive with
transaction.created, because the firehose never carries an expiry. Subscribing to both is accepted.
No webhook could have received the old shape, so nothing that worked before has changed.
2026-10-08 — a "uses" figure for discount and access programs
GET /programs/{id}/stats gains two fields, and GET /programs/{id}/timeline one, all non-null only for discount and access programs — the two types whose activity is a recorded use transaction (a card presented and accepted):
total_uses— how many non-reversedusetransactions the program's cards have, all time;uses_by_day— the same, per day over the last 90 days (shop timezone), in the{ date, count }shape ofcards_by_day;- timeline
uses—usetransactions per bucket, index-aligned withbuckets.
For every other program type all three are null (a stamp card stamps, a gift card or points card moves a balance — there is no bare "use" to count), never 0. Additive: nothing existing changed.
2026-10-03 — gift-card top-ups: the prepaid_topup transaction type
A gift card (type prepaid) can now be topped up from the dashboard. Each top-up is a ledger row of the new type prepaid_topup with a positivevalue_delta (euro cents), and it fires the events a gift-card payment fires: value.changed with tx_type: "prepaid_topup" and a positive delta, and transaction.created. It is listed by GET /transactions (and filterable with ?type=prepaid_topup) like any other row. Additive: type and tx_type have always been open strings, so an integration that ignores a type it does not know keeps working.
One behaviour change: GET /cards/:id/stats no longer counts a top-up in last_used_at — loading money onto a card is not a visit.
2026-09-08 — first release
The Business API is live. It is sold as an add-on, its Stripe Price is provisioned, and the module's flag is on in production — GET /api/v1/pricing reports the api_only role as both available and provisioned, which is what the dashboard's purchase surface is gated on. Everything listed below is on the wire today. See Plan limits for what the add-on includes and what it costs.
/api/public/v1— REST surface,tsk_API keys, scopes, location restriction, cursor pagination,Idempotency-Keyon every write.- Webhooks widened to shop scope with a much larger event catalogue and payload v2. Existing program-scoped webhooks keep their behaviour and their payload bytes.
- The MCP connector with
tmk_keys. - Cards can be disabled and re-enabled:
POST /cards/{id}/disableandPOST /cards/{id}/enable, adisabledvalue in the cardstatusvocabulary and in the?status=filter, and thecard.disabled/card.enabledevents. Disable is the reversible one; archive stays terminal. See Cards. GET /cardsrefuses an unrecognised?status=with a400instead of answering with an empty page, and the spec now publishes the accepted values as an enum.GET /cards/{id}returnsbarcode—scan_url, the camera-openable form a printed badge should carry, andlegacy_payload, the originaltlp:string. Both are accepted wherever a barcode payload is accepted.- The MCP
search_cardstool acceptsdisabledin itsstatusfilter.