Retrieve a transaction
Returns a single ledger entry by id.
Reversal navigation: on a kind: "reversal" row, follow reverses.id to the original (its type is inlined as reverses.type so you can branch without the second GET). On an original with reversed: true, follow reversed_by.
Authorizations
Your secret API key as a Bearer token: Authorization: Bearer vxn_sk_.... Keys are created and scoped in the dashboard - see Authentication.
Response
Transaction record.
One immutable ledger entry. Transactions are the source of truth for every balance - the server computes balance_after, type, and unit; clients only ever describe the real-world action.
Reversal model: mistakes are never edited - post a reversal (POST /v1/transactions/{id}/reverse). The reversal is itself a transaction (kind: "reversal") whose effect exactly negates the original; both rows stay in the ledger forever. On a reversal, action describes what is being undone (the original action), not a new customer action - detect reversals via kind/type, never by parsing action.
Server-set stable public classification (29 values). Partners building dashboards or per-event handlers should switch on type, never parse action.
You never send type - the server derives it from action × the card's type (see the mapping table on POST /v1/transactions).
* = server-emitted only - can never be produced via POST. Where one of them appears at all, it is in list results and webhooks; the seven types named below never appear.
Namespace note: transaction types use verb stems (points.earn); webhook event names use past participles (transaction.created). Subscribe to the webhook name, branch on the inner type.
Declared but NOT emitted by v1 today. These seven are reserved in the enum for forward compatibility and no current code path produces them, so a switch branch on any of them is dead code until this note is removed: card.issue, card.link, card.unlink, coupon.issue, points.manual_adjust, stamp.bonus, tier.change.
type is DERIVED per row from the underlying event and its deltas, never stored, so the set of producible values is fixed by the classifier rather than by data. coupon.issue is structurally impossible rather than merely unimplemented: coupon issuance is not a ledger row, so every coupon transaction is a redemption.
card.issue, card.link, card.unlink, cashback.earn, cashback.redeem, coupon.issue, coupon.redeem, discount.apply, discount.tier_change, employee_discount.apply, gift_card.create, gift_card.refund, gift_card.spend, gift_card.topup, import, manual_adjust, membership.activate, membership.expire, membership.renew, membership.use, points.earn, points.manual_adjust, points.redeem, purchase, reversal, stamp.add, stamp.bonus, stamp.redeem, tier.change "points.earn"
Server-set. Names what the integer in value carries, so partners never guess: stamps, points (cashback rides the same points ledger as any other Reward card - see the Transaction value description; the money view lives on saving_minor and balance_after.cashback_minor, never on unit/value), gift_card_minor (cents), membership_uses, or none (for example purchase, discount.apply, employee_discount.apply, coupon.redeem, discount.tier_change and import rows).
stamps, points, gift_card_minor, membership_uses, none "points"
Server-set, denormalized from the card. Cashback is NOT a separate type - cashback transactions target a reward card in cashback redemption mode. POST /v1/transactions accepts stamp, reward, discount and coupon cards; every other card type returns 422 card_type_not_writable. NULL when the card this transaction belongs to has been HARD-deleted: card_type is denormalized from the Card document and nothing on the transaction row preserves it, so it is unrecoverable once the card is gone. card_id is still returned for audit, but GET /v1/cards/{id} for it will 404. Every other field on the row stays valid.
stamp, reward, discount, coupon, membership, gift_card, custom, null "reward"
The units the action carried, paired with unit: stamps added/redeemed, points granted/consumed, points on a cashback card too (cashback is a presentation mode over the same points ledger - the money view is on saving_minor and balance_after.cashback_minor, never here), or gift-card minor units to load/spend. Whole for source: api rows. Rows from the staff scanner (manual) and from a POS (pos) can carry up to 4 decimals on any Reward card, flat or tiered: for example 10 EUR at 0.5 points per EUR on a 1.5x level yields exactly 7.5 on a scanner row. Treat this as a decimal.
18
Where the ledger entry originated. Server-stamped - never accepted on input. Every transaction you create via POST /v1/transactions is stamped "api", no exceptions.
Values you will see on reads:
api- created through the Public API by you or your developerpos- pushed by a connected POS bridge after a real-world bill (the specific POS vendor is never surfaced)manual- recorded by staff through the VEXiON cashier scanner. A scanner undo appears as an opposite entry, not as a reversalonline- originated from a first-class online checkout integrationsystem- generated by VEXiON itself (tier demotion, expiry sweeps, referral payouts, transform reconciliation, import audit rows)
Treat this as an open set - new origin values may be added without a version bump. Code defensively: branch on the values you know, pass unknown ones through.
Entries with any source other than api are read-only through this API: you can list and retrieve them, but you cannot create or reverse them (corrections happen in their originating channel - POS cancellation flow, scanner undo, or support).
"api"
Unix seconds when the event happened at its origin (till time for POS entries, request time for API entries). Usually equals created_at; differs when a POS webhook or batch import arrives late. Range filters (created[gte|lte]) and sort use created_at - use occurred_at for business-day reconciliation.
1750691210
The team member who recorded this entry: the person signed in on the scanner or in the dashboard, not the account owner. Populated only on source: "manual" transactions. Null everywhere else: api entries are attributed to the API key, pos entries to the till (the POS does not identify the operating person), online/system have no human operator. Older manual entries, recorded before the person was captured, show the account owner. Null on a manual entry when the person can no longer be resolved, for example a removed staff member without a team member record.
^tm_[A-Za-z0-9]+$null
Only set on type: "employee_discount.apply" and on its reversal (reverses.type === "employee_discount.apply") - a reversal's own type is always "reversal", never the original type. Sum of loyalty-enabled line items (with VAT) so reversal math reconciles to the exact budget increment applied. Never accepted on input. It is null on source: api rows.
null
Server-computed monetary benefit the customer received on this bill, in minor units - the discount granted (discount.apply, employee_discount.apply), the credit used (cashback.redeem) or a referral bonus row. Null on earns and on every row with no money value, including points.redeem and coupon.redeem. Sum this per period for 'total value given' reporting. Never accepted on input.
null
For Reward cards with levels: the name of the level (its display name, for example Silver) active when this entry was recorded, not a slug. Null on discount rows and on rows from some POS channels, where applied_multiplier is still set. Round-trips verbatim onto the reversal row so reversal math uses the ORIGINAL tier, not the customer's current one.
"silver"
Your free-form key/value bag. NOT renderable in pass/notification templates (unlike customer.custom). Shape IS enforced (422 validation_failed): max 50 keys, key must match ^[a-zA-Z0-9_]{1,40}$, values must be strings of at most 500 chars. The platform never interprets the contents. The only server-set key is metadata.reversal_reason on reversal rows.