Skip to content

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 code vocabulary for the same condition;
  • a webhook event's existing data fields 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:

  • data is { card_instance_id, expired_at }. expired_at is the card's expires_at. There is no transaction_id: expiry writes nothing to the ledger, so the envelope's transaction is null and actor is system.
  • It is a shop-wide event (location: null). A location-scoped webhook receives it while include_shop_wide_events is 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-reversed use transactions 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 of cards_by_day;
  • timeline uses — use transactions per bucket, index-aligned with buckets.

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-Key on 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}/disable and POST /cards/{id}/enable, a disabled value in the card status vocabulary and in the ?status= filter, and the card.disabled / card.enabled events. Disable is the reversible one; archive stays terminal. See Cards.
  • GET /cards refuses an unrecognised ?status= with a 400 instead of answering with an empty page, and the spec now publishes the accepted values as an enum.
  • GET /cards/{id} returns barcode — scan_url, the camera-openable form a printed badge should carry, and legacy_payload, the original tlp: string. Both are accepted wherever a barcode payload is accepted.
  • The MCP search_cards tool accepts disabled in its status filter.

Requires the Business API add-on.