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.
Path Parameters
Transaction identifier (txn_ prefix).
^txn_[A-Za-z0-9]+$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.
Opaque ledger identifier, txn_ prefix. Stable forever.
^txn_[A-Za-z0-9]+$"txn_01HW9R5Q3AB7N8MTX1RVTXNDEF"
Always "transaction".
"transaction"Server-set. normal = a regular ledger entry. reversal = a compensating row created by POST .../reverse, carrying reverses: {id, type}. Tombstones exist internally but never surface in the API.
normal, reversal "normal"
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"
The card template this transaction targets. One transaction touches one card.
^crd_[A-Za-z0-9]+$"crd_01HW9R4P2ZK7N8MTX1RVCARD01"
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 customer this entry credits or debits.
^cus_[A-Za-z0-9]+$"cus_01HW9R4P2ZK7N8MTX1RVABCDEF"
award adds stamps or points, redeem uses them up. On a reversal (kind: "reversal"), action describes what is being undone (the original action), not a new customer action; detect reversals via kind/type, never by parsing action.
award, redeem 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
ISO 4217. Defaults to the organization's default currency.
3"EUR"
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"
True once a reversal is posted against this entry.
false
Unix seconds, server-assigned.
1750691210
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
Location attribution. Optional.
^loc_[A-Za-z0-9]+$"loc_01HW9R4P2ZK7N8MTX1RVLOCS01"
The team member (scanner staff) who recorded this entry. Populated only on source: "manual" transactions - the cashier scanner runs under a signed-in team member. 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.
^tm_[A-Za-z0-9]+$null
The bill total in minor units (cents) that drove the action. Required for award on spend-driven cards (reward-by-spend, cashback, discount); optional otherwise.
1250
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
Server-authoritative balance snapshot. Shape varies by card_type.
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 refund math uses the ORIGINAL tier, not the customer's current one.
"silver"
For tiered reward cards: the earn multiplier in effect. Round-trips verbatim to the reversal row.
1.5
Your correlation reference (order id, receipt number). Indexed for list filtering. (Public name for the platform's internal receipt reference.)
255"pos-receipt-abc-123"
On reversal rows only: {id, type} of the original - lets receivers branch on the reversed type without a second GET. Null on originals.
null
The txn_ of the reversal that voids this row. Null while un-reversed.
null
Unix seconds the reversal was posted.
null
Optional line items. Stored verbatim, emitted on webhooks, never interpreted by the loyalty engine. Use sku for product correlation.
50Your 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.