Skip to main content
POST
cURL

Authorizations

Authorization
string
header
required

Your secret API key as a Bearer token: Authorization: Bearer vxn_sk_.... Keys are created and scoped in the dashboard - see Authentication.

Headers

Idempotency-Key
string
required

Required. Stable opaque string, 8-255 chars. Same key + same body within 24 h replays the original response (Idempotent-Replayed: true); same key + different body → 409 idempotency_key_conflict. See Idempotency.

Required string length: 8 - 255
Pattern: ^[A-Za-z0-9_-]{8,255}$
Accept-Language
enum<string>

Preferred language for error messages (sk, cs, en, de). Defaults to organization language.

Available options:
sk,
cs,
en,
de

Body

application/json

Describe the real-world action; the server computes everything else (type, unit, balance_after, tier snapshots).

Server-authoritative balances: any body carrying type, kind, unit, card_type, balance_after, points_delta, stamps_delta, eligible_spend_minor, tier_at_earning, applied_multiplier, or any legacy Balance/Amount field returns 422 parameter_unknown. There is no trust-the-client path.

Idempotency-Key header is REQUIRED on this endpoint - a retried award without one would double-grant.

customer_id
string
required

The customer (cus_ id) this entry credits or debits. The server derives the card (and card_type) from the customer's enrollment.

Pattern: ^cus_[A-Za-z0-9]+$
Example:

"cus_01HW9R4P2ZK7N8MTX1RVABCDEF"

action
enum<string>
required

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.

Available options:
award,
redeem
card_id
string | null

Optional consistency check - when provided, must match the customer's enrolled card (422 validation_failed otherwise).

Pattern: ^crd_[A-Za-z0-9]+$
Example:

"crd_01HW9R4P2ZK7N8MTX1RVCARD01"

value
integer | null

Stamps or points to add or redeem. On a cashback card value is points too, also on redeem: the money view is on saving_minor and balance_after.cashback_minor. Omit it where the card earns from spend and send amount_minor instead: a Reward award then books round(amount_minor / 100 x earn_rate_per_unit x level multiplier), rounded once at the end to the nearest whole point (halves round up). The level multiplier applies only on a card with levels. A card with no earn_rate_per_unit set earns 1 point per currency unit. A rate of 0 earns 0 points: the request answers 201 with value: 0 and the balance does not change. When you send both value and amount_minor on a tiered Reward award, the points are your value times the customer's level multiplier, rounded to the nearest whole point (5 x 1.25 = 6.25 books 6), and the bill (amount_minor) still counts toward the customer's level spend, so the customer can level up. An explicit value always books value points, also on a card with rate 0. A redeem never adds spend. Stamp cards have no spend-driven mechanic: value is always required there. Maximum 100,000,000,000. An explicit value has a limit per call: a points card 100,000 points; a cashback card, points whose worth (value x cashback_point_value) stays within the largest bill of the currency; a stamp award one full card (stamps_required). Above the limit answers 422 value_too_large on value.

Required range: 0 <= x <= 100000000000
Example:

null

amount_minor
integer | null

Bill total in minor units. Required for award on spend-driven cards (reward by spend, cashback, discount); optional otherwise. Maximum 100,000,000,000. The largest single bill is limited per currency, in major units: EUR 10,000; CZK 250,000; HUF 4,000,000; PLN 45,000; RON 50,000; GBP 9,000; CHF 10,000; USD 11,000. Above it answers 422 amount_too_large on amount_minor. A maximum transaction amount that the business set in the dashboard applies too. Amounts are in minor units with 2 decimals for every currency, also HUF (1,990 Ft is 199000). A redeem carries no bill: amount_minor on a redeem answers 422 amount_not_valid_for_action. See Money and currency.

Required range: 0 <= x <= 100000000000
Example:

1250

currency
string | null

ISO 4217. Optional. A business has one currency (default_currency on GET /v1/organization). When you send it, it must be that currency, in any letter case. Another code, or a code we do not support, answers 422 currency_mismatch, and field_errors[0].expected_currency names the currency to send. Leave it out to book in the business currency. See Money and currency.

Required string length: 3
Example:

"EUR"

location_id
string | null

Attribution. Optional.

Pattern: ^loc_[A-Za-z0-9]+$
Example:

"loc_01HW9R4P2ZK7N8MTX1RVLOCS01"

external_id
string | null

Your correlation reference (order or receipt id). Indexed for list filtering. Reusing an external_id to retry the same transaction returns the original transaction with 201 and records nothing new. A retry may leave amount_minor and currency out. If it sends them, they must match what was booked. Reusing it for a different action, value, amount_minor or currency answers 409 external_id_conflict: use a fresh external_id for every new event.

Maximum string length: 255
Example:

"pos-receipt-abc-123"

occurred_at
integer | null

Unix seconds when the event actually happened at your till/checkout. Defaults to now. Supply it when recording late (offline POS catch-up, batch import) so business-day reconciliation stays honest. Bounds: not in the future, not older than 90 days (422 occurred_at_out_of_range) - matching the reversal window. Sorting and created[gte|lte] filters always use created_at (ingest time), never this field.

Example:

null

products
object[]

Optional line items - see TransactionProduct. Use sku as your product correlation key. Line items use the TransactionProduct shape: product_id, name, sku, quantity, amount_minor (per-line money, minor units), currency, category, metadata. The request schema previously declared NO properties at all, so any field sent was rejected as parameter_unknown with nothing in the contract to guide you.

Maximum array length: 50
metadata
object

Free-form key/value bag, not renderable in templates. 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.

Response

Transaction created. Server-computed balances are populated on the response.

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.

id
string
required

Opaque ledger identifier, txn_ prefix. Stable forever.

Pattern: ^txn_[A-Za-z0-9]+$
Example:

"txn_01HW9R5Q3AB7N8MTX1RVTXNDEF"

object
string
required

Always "transaction".

Allowed value: "transaction"
kind
enum<string>
required

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.

Available options:
normal,
reversal
Example:

"normal"

type
enum<string>
required

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.

Available options:
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
Example:

"points.earn"

unit
enum<string>
required

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).

Available options:
stamps,
points,
gift_card_minor,
membership_uses,
none
Example:

"points"

card_id
string
required

The card template this transaction targets. One transaction touches one card.

Pattern: ^crd_[A-Za-z0-9]+$
Example:

"crd_01HW9R4P2ZK7N8MTX1RVCARD01"

card_type
enum<string> | null
required

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.

Available options:
stamp,
reward,
discount,
coupon,
membership,
gift_card,
custom,
null
Example:

"reward"

customer_id
string
required

The customer this entry credits or debits.

Pattern: ^cus_[A-Za-z0-9]+$
Example:

"cus_01HW9R4P2ZK7N8MTX1RVABCDEF"

action
enum<string>
required

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.

Available options:
award,
redeem
value
number
required

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.

Example:

18

currency
string
required

ISO 4217. Defaults to the organization's default currency.

Required string length: 3
Example:

"EUR"

source
string
required

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 developer
  • pos - 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 reversal
  • online - originated from a first-class online checkout integration
  • system - 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).

Example:

"api"

reversed
boolean
required

True once a reversal is posted against this entry.

Example:

false

created_at
integer
required

Unix seconds, server-assigned.

Example:

1750691210

occurred_at
integer
required

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.

Example:

1750691210

location_id
string | null

Location attribution. Optional.

Pattern: ^loc_[A-Za-z0-9]+$
Example:

"loc_01HW9R4P2ZK7N8MTX1RVLOCS01"

team_member_id
string | null

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.

Pattern: ^tm_[A-Za-z0-9]+$
Example:

null

amount_minor
integer | 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.

Example:

1250

eligible_spend_minor
integer | 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.

Example:

null

saving_minor
integer | 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.

Example:

null

balance_after
object | null

Server-authoritative balance snapshot. Shape varies by card_type.

Example:
tier_at_earning
string | 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 refund math uses the ORIGINAL tier, not the customer's current one.

Example:

"silver"

applied_multiplier
number | null

For tiered reward cards: the earn multiplier in effect. Round-trips verbatim to the reversal row.

Example:

1.5

external_id
string | null

Your correlation reference (order id, receipt number). Indexed for list filtering. (Public name for the platform's internal receipt reference.)

Maximum string length: 255
Example:

"pos-receipt-abc-123"

reverses
object | null

On reversal rows only: {id, type} of the original - lets receivers branch on the reversed type without a second GET. Null on originals.

Example:

null

reversed_by
string | null

The txn_ of the reversal that voids this row. Null while un-reversed.

Example:

null

reversed_at
integer | null

Unix seconds the reversal was posted.

Example:

null

products
object[]

Optional line items. Stored verbatim, emitted on webhooks, never interpreted by the loyalty engine. Use sku for product correlation.

Maximum array length: 50
Example:
metadata
object

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.

Example: