Changelog
Every partner-visible change to the Loyalty API, newest first. Changes within
/v1 are additive: we do not remove fields, rename error codes, change types,
or make optional fields required. If we ever need to break something it will be
/v2, running alongside /v1 — never a deadline handed to you.
2026-10-21
Pull a day's transactions for your accounting system.
GET /transactions?from=&to= lists every transaction in your store whose
business time falls in the window — not only the ones your system sent, but
also adjustments your staff make in the Cardy dashboard, which your own records
cannot contain. Cursor-paginated, newest first, up to 100 per page; from is
inclusive and to exclusive, so consecutive days tile without overlap. The
receipt lookup on the same path is unchanged and still scoped to your own
references.
If you already poll GET /transactions?external_ref=, nothing changes except
that the response now carries next_cursor (always null for a receipt
lookup) beside data. A client that only reads data keeps working.
Every transaction now says where it came from, and what it reversed.
channel is pos, ecommerce, cardy (an action in the Cardy dashboard or
scanner app by your staff), manual (an adjustment by Cardy support on your
behalf) or internal (a correction written by the nightly reconciliation).
reverses_transaction_id is set on a reverse and names the transaction it
undid, so a refund pairs with its sale without a second lookup; it is null
everywhere else.
No action needed. Both fields are additive and present on every read.
2026-08-29
Scanning a card now identifies the customer holding it.
loyalty_id — the serial your scanner reads off a card — has always been part
of identify, and in the sandbox it resolved to nobody: every scan answered
CUSTOMER_NOT_FOUND while the same customer's phone number worked. It resolves
now, for any card, including ones issued before you integrated.
If you built against the phone lookup because the scan did not work, switch. The scan is what your cashiers use, and it is the identifier we trust over the others when two of them disagree.
You now get a test card, not just a test phone number. Your test card shows a real sandbox card's serial as a QR you can scan off the screen or print for the drawer. The quickstart starts there, and certification now checks that a scan and a phone lookup return the same customer.
No action needed, but the new Level 2 check runs on your next certification run. If your integration only ever identified by phone, it will pass — the check is on our resolution, not on your input.
allowed_operations now means "this will work".
Four card types used to advertise redeem_points and then refuse it. If you
were defensively handling a refusal on an operation we had listed, you no
longer need to: every operation in the list is one the card can perform. Cards
that hold money advertise redeem_amount instead, and cards that hold no
spendable balance advertise no redemption at all.
No action needed. If you were suppressing buttons that we listed but that failed in practice, you can stop.
Back-dated sales beyond the offline window are now refused.
occurred_at older than the store's offline window (24 hours by default) now
answers VALIDATION_ERROR with details: ["occurred_at"]. It was previously
accepted silently, which let a mis-configured till post sales into periods a
store had already reconciled.
Action needed only if you back-date by more than a day. Normal offline batching is unaffected.
Subscription and multipass cards now report their points balance.
balances.points was null on these cards and is now a number, so redemption
against them works. Nothing changed shape — a field that was always present
stopped being empty.
Entries begin here. Earlier changes predate the published API and are not listed; if you integrated before this date, nothing in the list above requires work beyond what each entry states.