> ## Documentation Index
> Fetch the complete documentation index at: https://vexioncards.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# List transactions

> Lists ledger entries newest first (`created_at desc, id desc` tiebreak). Cursor pagination: default 25, max 100; filters compose with the cursor.

## Common patterns

- **Customer history:** `?customer_id=cus_...`
- **Cashback only:** `?card_type=reward&type[]=cashback.earn&type[]=cashback.redeem`
- **Incremental sync from a known anchor:** hold your last-ingested id and walk forward with `ending_before` until an empty page
- **Un-reversed redemptions:** `?action=redeem&reversed=false`
- **Reconcile one receipt:** `?external_id=pos-receipt-abc-123`

See [Pagination](/api-reference/pagination).



## OpenAPI

````yaml /openapi.json get /v1/transactions
openapi: 3.1.0
info:
  title: VEXiON cards Public API
  description: >-
    ## Rate limiting


    Two independent layers, checked in this order:


    1. **Per-IP throttle (pre-authentication).** Applied to every `/v1` request
    BEFORE the API key is checked, keyed on the caller's IP address:
       - **600 requests/minute** per IP - a flood ceiling, `429 rate_limit_error` / `rate_limited`.
       - **20 failed authentications per 15 minutes** per IP - a key-guessing ceiling. Only responses that are `401` (missing/invalid/revoked key) count against this ceiling; a `403` from a valid key that simply lacks a scope does not. `429 rate_limit_error` / `too_many_failed_attempts`.

       Both ceilings send `Retry-After` (seconds).
    2. **Per-key, per-bucket limiter (post-authentication).** Documented per
    endpoint (see each operation's `429` response and the `rate_limit` field on
    the API Key resource) - e.g. 60/min default, 120/min for transactions, plus
    the notification-broadcast and lookup-filter buckets described on their own
    endpoints.


    ### Rate-limit response headers


    Every rate-limited response carries the legacy `X-RateLimit-Limit` /
    `X-RateLimit-Remaining` / `X-RateLimit-Reset` family (the last is an **epoch
    timestamp**) AND the IETF standards-track headers, side by side:


    - `RateLimit-Policy: "<bucket>";q=<quota>;w=<window seconds>` - the static
    policy for the bucket that was hit.

    - `RateLimit: "<bucket>";r=<remaining>;t=<seconds until reset>` - live
    state. **`t` is a DURATION (seconds remaining), not the epoch timestamp
    `X-RateLimit-Reset` carries - the two are never the same value.**
  version: 1.0.0
servers:
  - url: https://api.vexioncards.one
    description: Production
  - url: https://migration.api.vexioncards.one
    description: Sandbox
security:
  - apiKey: []
tags:
  - name: Customers
  - name: Cards
  - name: Transactions
  - name: Notifications
  - name: Locations
  - name: Products
  - name: API keys
  - name: Organization
  - name: Webhook endpoints
  - name: Events
  - name: Custom fields
  - name: Team members
  - name: Icons
paths:
  /v1/transactions:
    get:
      tags:
        - Transactions
      summary: List transactions
      description: >-
        Lists ledger entries newest first (`created_at desc, id desc` tiebreak).
        Cursor pagination: default 25, max 100; filters compose with the cursor.


        ## Common patterns


        - **Customer history:** `?customer_id=cus_...`

        - **Cashback only:**
        `?card_type=reward&type[]=cashback.earn&type[]=cashback.redeem`

        - **Incremental sync from a known anchor:** hold your last-ingested id
        and walk forward with `ending_before` until an empty page

        - **Un-reversed redemptions:** `?action=redeem&reversed=false`

        - **Reconcile one receipt:** `?external_id=pos-receipt-abc-123`


        See [Pagination](/api-reference/pagination).
      operationId: listTransactions
      parameters:
        - name: customer_id
          in: query
          required: false
          description: Filter to one customer.
          schema:
            type: string
          example: cus_01HW9R4P2ZK7N8MTX1RVABCDEF
        - name: card_id
          in: query
          required: false
          description: Filter to one card.
          schema:
            type: string
          example: crd_01HW9R4P2ZK7N8MTX1RVCARD01
        - name: card_type
          in: query
          required: false
          description: >-
            Filter by card family. For cashback, combine `card_type=reward` with
            `type[]=cashback.earn&type[]=cashback.redeem`.
          schema:
            type: string
            enum:
              - stamp
              - reward
              - discount
              - coupon
              - membership
              - gift_card
              - custom
          example: reward
        - name: location_id
          in: query
          required: false
          description: Filter to one location.
          schema:
            type: string
          example: loc_01HW9R4P2ZK7N8MTX1RVLOCS01
        - name: team_member_id
          in: query
          required: false
          description: >-
            Filter to entries recorded by one team member (`tm_` id). Only
            matches `source: "manual"` scanner entries - the only origin that
            carries a person. Combine with `created[gte|lte]` for per-staff
            shift reports.
          schema:
            type: string
            pattern: ^tm_[A-Za-z0-9]+$
          example: tm_01HW9R9Z4EF7N8MTX1RVLTMABC
        - name: action
          in: query
          required: false
          description: Filter by verb.
          schema:
            type: string
            enum:
              - award
              - redeem
          example: award
        - name: kind
          in: query
          required: false
          description: >-
            `normal` or `reversal`. Passing `tombstone` returns `422
            parameter_invalid` - tombstones never surface.
          schema:
            type: string
            enum:
              - normal
              - reversal
          example: normal
        - name: type
          in: query
          required: false
          description: >-
            Filter by transaction type. Accepts a single value
            (`?type=points.earn`) OR an array of up to 10 values
            (`?type[]=points.earn&type[]=points.redeem`) - same toggle pattern
            as localized fields on the Card resource.
          explode: true
          schema:
            anyOf:
              - type: string
                title: Single type
                enum:
                  - 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
              - type: array
                title: Multiple types (max 10)
                items:
                  type: string
                  enum:
                    - 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
                maxItems: 10
          example: points.earn
        - name: source
          in: query
          required: false
          description: >-
            Filter by origin. Known values: `api`, `pos`, `manual`, `online`,
            `system` - treat as an open set (new origins may be added). Unknown
            values return an empty list, not an error.
          schema:
            type: string
          example: pos
        - name: external_id
          in: query
          required: false
          description: Exact match on your correlation reference.
          schema:
            type: string
          example: pos-receipt-abc-123
        - name: created[gte]
          in: query
          required: false
          description: Unix seconds lower bound (inclusive).
          schema:
            anyOf:
              - type: integer
                title: Unix seconds
              - type: string
                format: date-time
                title: ISO-8601
          example: 1750600000
        - name: created[lte]
          in: query
          required: false
          description: Unix seconds upper bound (inclusive).
          schema:
            anyOf:
              - type: integer
                title: Unix seconds
              - type: string
                format: date-time
                title: ISO-8601
          example: 1750700000
        - name: reversed
          in: query
          required: false
          description: >-
            `true` = only reversed originals; `false` = only un-reversed; omit
            for both.
          schema:
            type: boolean
          example: false
        - name: limit
          in: query
          required: false
          description: Page size 1-100, default 25.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
          example: 25
        - name: starting_after
          in: query
          required: false
          description: >-
            Cursor - strictly OLDER than this `txn_` id (forward/next page).
            Numeric offset or page parameters are not supported; cursors are the
            only supported paging mechanism.
          schema:
            type: string
          example: txn_01HW9R5Q3AB7N8MTX1RVTXNDEF
        - name: ending_before
          in: query
          required: false
          description: >-
            Cursor - strictly NEWER than this `txn_` id (walk forward in time
            from an anchor).
          schema:
            type: string
          example: txn_01HW9R5Q3AB7N8MTX1RVTXNDEF
      responses:
        '200':
          description: Paginated transaction list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransactionList'
              examples:
                Mixed ledger page:
                  summary: api + pos + manual origins, per-type balance_after
                  value:
                    object: list
                    data:
                      - id: txn_01HW9R5Q3AB7N8MTX1RVTXNDEF
                        object: transaction
                        kind: normal
                        type: points.earn
                        unit: points
                        card_id: crd_01HW9R4P2ZK7N8MTX1RVCARD01
                        card_type: reward
                        customer_id: cus_01HW9R4P2ZK7N8MTX1RVABCDEF
                        location_id: loc_01HW9R4P2ZK7N8MTX1RVLOCS01
                        team_member_id: null
                        action: award
                        value: 18
                        amount_minor: 1250
                        eligible_spend_minor: null
                        saving_minor: null
                        currency: EUR
                        balance_after:
                          points: 124
                        tier_at_earning: silver
                        applied_multiplier: 1.5
                        source: api
                        external_id: demo-receipt-abc-123
                        reversed: false
                        reverses: null
                        reversed_by: null
                        reversed_at: null
                        products:
                          - name: Flat White
                            sku: FW-12
                            quantity: 1
                            amount_minor: 350
                            currency: EUR
                            category: drinks
                          - name: Cinnamon Roll
                            sku: CR-04
                            quantity: 1
                            amount_minor: 900
                            currency: EUR
                            category: pastry
                        metadata:
                          till_id: till-3
                        created_at: 1750691210
                        occurred_at: 1750691210
                      - id: txn_01HXR9JNEW00000000000000A2
                        object: transaction
                        kind: normal
                        type: stamp.add
                        unit: stamps
                        card_id: crd_01HW9R4P2ZK7N8MTX1RVCARD02
                        card_type: stamp
                        customer_id: cus_01HW9R4P2ZK7N8MTX1RVABCDEF
                        location_id: loc_01HW9R4P2ZK7N8MTX1RVLOCS01
                        team_member_id: null
                        action: award
                        value: 1
                        amount_minor: 350
                        eligible_spend_minor: null
                        saving_minor: null
                        currency: EUR
                        balance_after:
                          stamps_collected: 7
                        tier_at_earning: null
                        applied_multiplier: null
                        source: pos
                        external_id: null
                        reversed: false
                        reverses: null
                        reversed_by: null
                        reversed_at: null
                        products: []
                        metadata: {}
                        created_at: 1750691100
                        occurred_at: 1750691100
                      - id: txn_01HXR9JNEW00000000000000A3
                        object: transaction
                        kind: normal
                        type: gift_card.spend
                        unit: gift_card_minor
                        card_id: crd_01HW9R4P2ZK7N8MTX1RVCARD03
                        card_type: gift_card
                        customer_id: cus_01HW9R4P2ZK7N8MTX1RVABCDEF
                        location_id: loc_01HW9R4P2ZK7N8MTX1RVLOCS01
                        team_member_id: null
                        action: redeem
                        value: 450
                        amount_minor: 450
                        eligible_spend_minor: null
                        saving_minor: null
                        currency: EUR
                        balance_after:
                          remaining_amount_minor: 4550
                        tier_at_earning: null
                        applied_multiplier: null
                        source: manual
                        external_id: null
                        reversed: false
                        reverses: null
                        reversed_by: null
                        reversed_at: null
                        products: []
                        metadata: {}
                        created_at: 1750690980
                        occurred_at: 1750690980
                    has_more: true
                    url: /v1/transactions
                Empty result:
                  summary: No matches
                  value:
                    object: list
                    data: []
                    has_more: false
                    url: /v1/transactions
          headers: {}
        '400':
          description: Invalid query parameter value or format.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers: {}
        '401':
          description: Missing, invalid, or revoked API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers: {}
        '403':
          description: API key lacks the required scope 'transactions:read'.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers: {}
        '429':
          description: Per-minute or per-day rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers: {}
        '500':
          description: Server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers: {}
      deprecated: false
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl -G https://api.vexioncards.one/v1/transactions \
              -H "Authorization: Bearer $VXN_API_KEY" \
              -d customer_id=cus_01HW9R4P2ZK7N8MTX1RVABCDEF \
              -d "type[]=points.earn" -d "type[]=points.redeem" \
              -d limit=50
        - lang: javascript
          label: Node.js
          source: |-
            // Incremental sync: walk forward from the last id you ingested
            let anchor = 'txn_01HW9R5Q3AB7N8MTX1RVTXNDEF';
            for (;;) {
              const url = new URL('https://api.vexioncards.one/v1/transactions');
              url.searchParams.set('ending_before', anchor);
              url.searchParams.set('limit', '100');
              const page = await (await fetch(url, {
                headers: { Authorization: `Bearer ${process.env.VXN_API_KEY}` },
              })).json();
              if (page.data.length === 0) break;
              for (const txn of page.data.reverse()) ingest(txn);   // oldest-first
              anchor = page.data[0].id;                              // newest of this page
            }
        - lang: python
          label: Python
          source: >-
            import os, requests


            params = {"customer_id": "cus_01HW9R4P2ZK7N8MTX1RVABCDEF", "limit":
            100}

            headers = {"Authorization": f"Bearer {os.environ['VXN_API_KEY']}"}

            while True:
                page = requests.get("https://api.vexioncards.one/v1/transactions",
                                    headers=headers, params=params).json()
                for txn in page["data"]:
                    print(txn["id"], txn["type"], txn["value"], txn["unit"])
                if not page["has_more"]:
                    break
                params["starting_after"] = page["data"][-1]["id"]
components:
  schemas:
    TransactionList:
      type: object
      description: >-
        Cursor-paginated transaction list (newest first, `created_at desc, id
        desc`).
      required:
        - object
        - data
        - has_more
      properties:
        object:
          type: string
          const: list
          description: Always `"list"`.
        data:
          type: array
          items:
            $ref: '#/components/schemas/Transaction'
          description: Transaction rows.
        has_more:
          type: boolean
          description: True when more pages exist - pass the last id as `starting_after`.
        next_cursor:
          type:
            - string
            - 'null'
          description: >-
            Cursor for the next page **in the direction you are already
            paging**, or `null` when this page ends the collection. `has_more:
            true` always comes with a non-null `next_cursor`.


            Feed it back to the SAME parameter you used: `starting_after` when
            paging forward (the default), or `ending_before` when paging
            backward. Treat it as opaque and do not parse it.


            You no longer need to take the last object's `id` yourself. That
            still works and is unchanged, but deriving the cursor from the page
            put the burden of knowing the sort direction on you, and it broke
            silently on any endpoint whose cursor is not the object id.
          example: cus_iofv5j4hn6bphinmmyat27tc5y
        url:
          type: string
          description: Stable collection URL.
          example: /v1/transactions
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - type
            - code
            - message
          properties:
            type:
              type: string
              enum:
                - invalid_request_error
                - authentication_error
                - permission_error
                - not_found_error
                - rate_limit_error
                - idempotency_error
                - api_error
            code:
              type: string
            message:
              type: string
            retry_after:
              type: integer
              description: >-
                Seconds to wait before retrying. Present on rate-limit (429) and
                idempotency-key-in-use (409) errors; mirrors the `Retry-After`
                HTTP header. Absent on error types that carry no retry guidance.
              example: 5
            param:
              type:
                - string
                - 'null'
            request_id:
              type: string
            doc_url:
              type: string
              format: uri
            field_errors:
              type: array
              description: >-
                Per-field errors on a 422. At most 100 entries: a request with
                more errors gets the first 100.
              items:
                type: object
                properties:
                  param:
                    type: string
                  code:
                    type: string
                  message:
                    type: string
                  expected_currency:
                    type: string
                    description: >-
                      On `currency_mismatch`: the business currency the request
                      must use. Absent on every other field error.
          description: >-
            The error payload: type (category), code (machine-readable), message
            (human-readable), retry_after (seconds, on
            rate-limit/idempotency-in-use errors only), param (offending field),
            request_id, doc_url, field_errors[].
      description: >-
        Standard error envelope. Branch on `error.code`; include
        `error.request_id` in support requests. See the Errors guide.
    Transaction:
      type: object
      description: >-
        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`.
      required:
        - id
        - object
        - kind
        - type
        - unit
        - card_id
        - card_type
        - customer_id
        - action
        - value
        - currency
        - source
        - reversed
        - created_at
        - occurred_at
      properties:
        id:
          type: string
          pattern: ^txn_[A-Za-z0-9]+$
          description: Opaque ledger identifier, `txn_` prefix. Stable forever.
          example: txn_01HW9R5Q3AB7N8MTX1RVTXNDEF
        object:
          type: string
          const: transaction
          description: Always `"transaction"`.
        kind:
          $ref: '#/components/schemas/TransactionKind'
        type:
          $ref: '#/components/schemas/TransactionType'
        unit:
          $ref: '#/components/schemas/TransactionUnit'
        card_id:
          type: string
          pattern: ^crd_[A-Za-z0-9]+$
          description: >-
            The card template this transaction targets. One transaction touches
            one card.
          example: crd_01HW9R4P2ZK7N8MTX1RVCARD01
        card_type:
          type:
            - string
            - 'null'
          enum:
            - stamp
            - reward
            - discount
            - coupon
            - membership
            - gift_card
            - custom
            - null
          description: >-
            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.
          example: reward
        customer_id:
          type: string
          pattern: ^cus_[A-Za-z0-9]+$
          description: The customer this entry credits or debits.
          example: cus_01HW9R4P2ZK7N8MTX1RVABCDEF
        location_id:
          type:
            - string
            - 'null'
          pattern: ^loc_[A-Za-z0-9]+$
          description: Location attribution. Optional.
          example: loc_01HW9R4P2ZK7N8MTX1RVLOCS01
        team_member_id:
          type:
            - string
            - 'null'
          pattern: ^tm_[A-Za-z0-9]+$
          description: >-
            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.
          example: null
        action:
          $ref: '#/components/schemas/TransactionAction'
        value:
          type: number
          description: >-
            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
        amount_minor:
          type:
            - integer
            - 'null'
          description: >-
            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:
          type:
            - integer
            - 'null'
          description: >-
            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:
          type:
            - integer
            - 'null'
          description: >-
            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
        currency:
          type: string
          minLength: 3
          maxLength: 3
          description: ISO 4217. Defaults to the organization's default currency.
          example: EUR
        balance_after:
          $ref: '#/components/schemas/BalanceAfter'
        tier_at_earning:
          type:
            - string
            - 'null'
          description: >-
            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:
          type:
            - number
            - 'null'
          description: >-
            For tiered reward cards: the earn multiplier in effect. Round-trips
            verbatim to the reversal row.
          example: 1.5
        source:
          $ref: '#/components/schemas/TransactionSource'
        external_id:
          type:
            - string
            - 'null'
          maxLength: 255
          description: >-
            Your correlation reference (order id, receipt number). Indexed for
            list filtering. (Public name for the platform's internal receipt
            reference.)
          example: pos-receipt-abc-123
        reversed:
          type: boolean
          description: True once a reversal is posted against this entry.
          example: false
        reverses:
          type:
            - object
            - 'null'
          description: >-
            On reversal rows only: `{id, type}` of the original - lets receivers
            branch on the reversed type without a second GET. Null on originals.
          properties:
            id:
              type: string
              description: The original `txn_` id.
            type:
              $ref: '#/components/schemas/TransactionType'
          example: null
        reversed_by:
          type:
            - string
            - 'null'
          description: >-
            The `txn_` of the reversal that voids this row. Null while
            un-reversed.
          example: null
        reversed_at:
          type:
            - integer
            - 'null'
          description: Unix seconds the reversal was posted.
          example: null
        products:
          type: array
          maxItems: 50
          items:
            $ref: '#/components/schemas/TransactionProduct'
          description: >-
            Optional line items. Stored verbatim, emitted on webhooks, never
            interpreted by the loyalty engine. Use `sku` for product
            correlation.
          example:
            - name: Espresso doppio
              sku: ESP-D
              quantity: 1
              amount_minor: 1250
              currency: EUR
              category: drinks
        metadata:
          type: object
          additionalProperties:
            type: string
            minLength: 1
            maxLength: 500
          description: >-
            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:
            till_id: till-3
          maxProperties: 50
          propertyNames:
            pattern: ^[a-zA-Z0-9_]{1,40}$
        created_at:
          type: integer
          description: Unix seconds, server-assigned.
          example: 1750691210
        occurred_at:
          type: integer
          description: >-
            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
    TransactionKind:
      type: string
      enum:
        - normal
        - reversal
      description: >-
        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.
      example: normal
    TransactionType:
      type: string
      enum:
        - 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
      description: >-
        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).


        | Family | Types |

        |---|---|

        | Generic | `purchase` (bill without loyalty action), `manual_adjust`,
        `reversal` (original type under `reverses.type`) |

        | Stamp | `stamp.add`, `stamp.redeem`, `stamp.bonus` |

        | Points | `points.earn`, `points.redeem`, `points.manual_adjust`* |

        | Cashback | `cashback.earn`, `cashback.redeem` |

        | Discount | `discount.apply`, `discount.tier_change`*,
        `employee_discount.apply` (carries `eligible_spend_minor`) |

        | Coupon | `coupon.issue`, `coupon.redeem` |

        | Gift card | `gift_card.create`, `gift_card.topup`, `gift_card.spend`,
        `gift_card.refund` |

        | Membership | `membership.activate`, `membership.use`,
        `membership.renew`, `membership.expire`* |

        | Lifecycle* | `tier.change`, `card.issue`, `card.link`, `card.unlink`,
        `import` |


        \* = **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.
      example: points.earn
    TransactionUnit:
      type: string
      enum:
        - stamps
        - points
        - gift_card_minor
        - membership_uses
        - none
      description: >-
        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).
      example: points
    TransactionAction:
      type: string
      enum:
        - award
        - redeem
      description: >-
        `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`.
    BalanceAfter:
      type:
        - object
        - 'null'
      description: Server-authoritative balance snapshot. Shape varies by card_type.
      properties:
        stamps_collected:
          type: integer
          description: STAMP cards only - current stamp count after this entry.
        points:
          type: number
          description: >-
            REWARD cards only - point balance after this entry. Can carry up to
            4 decimals after `manual` or `pos` entries, as for
            Transaction.value. Treat it as a decimal.
        remaining_amount_minor:
          type: integer
          description: GIFT cards only - remaining balance in minor units.
        cashback_minor:
          type: integer
          description: >-
            CASHBACK-mode reward cards only - cashback balance in minor units.
            Computed with the card's current point value, so older rows change
            if the value changes.
      example:
        points: 124
    TransactionSource:
      type: string
      description: >-
        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
    TransactionProduct:
      type: object
      description: >-
        A line item on the bill. Stored verbatim and emitted in the
        `transaction.created` webhook - the loyalty engine does NOT interpret
        line items (rules act on the aggregate `amount_minor`); they exist for
        per-product reporting and basket analytics.


        **Product correlation, in resolution order:** (1) an explicit
        `product_id` you supply, validated against the transaction's location
        catalog; (2) `sku` matched against the catalog; (3) POS `pos_ref`
        matching on POS-originated entries. Unresolved lines stay free-form
        (`product_id: null`) and back-link when the catalog entry appears.


        Up to 50 line items per transaction. Line totals are NOT required to sum
        to the transaction's `amount_minor` (taxes, service fees, rounding are
        accepted silently).
      required:
        - name
      properties:
        product_id:
          type:
            - string
            - 'null'
          pattern: ^prd_[A-Za-z0-9]+$
          description: >-
            Reference into the location's product catalog. Optional on input
            (must belong to the transaction's location - `422 product_not_found`
            otherwise); on reads it is resolver-stamped from `sku` / POS mapping
            when you omit it. Null when unresolved.
          example: prd_01HW9R7T2CD8N8MTX1RVPRDFW1
        name:
          type: string
          minLength: 1
          maxLength: 200
          description: Display name of the line item. The only required field.
          example: Flat White
        sku:
          type: string
          minLength: 1
          maxLength: 64
          description: >-
            Your stable product identifier - the correlation key for mapping
            this line to your product catalog. Strongly recommended on every
            line.
          example: FW-12
        quantity:
          type: integer
          minimum: 1
          default: 1
          description: >-
            Units purchased. Capped at 100,000 units per line item to prevent
            overflow abuse.
          example: 1
          maximum: 100000
        amount_minor:
          type: integer
          description: Line total in minor units (cents). Maximum 100,000,000,000.
          example: 350
          minimum: 0
          maximum: 100000000000
        currency:
          type: string
          minLength: 3
          maxLength: 3
          description: ISO 4217. Defaults to the transaction's currency.
          example: EUR
        category:
          type: string
          minLength: 1
          maxLength: 100
          description: Free-form category for basket analytics.
          example: drinks
        metadata:
          type: object
          additionalProperties:
            type: string
            minLength: 1
            maxLength: 500
          description: >-
            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.
          example:
            pos_line_id: e9dbedb5
          maxProperties: 50
          propertyNames:
            pattern: ^[a-zA-Z0-9_]{1,40}$
      example:
        name: Flat White
        sku: FW-12
        quantity: 1
        amount_minor: 350
        currency: EUR
        category: drinks
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: >-
        Your secret API key as a Bearer token: `Authorization: Bearer
        vxn_sk_...`. Keys are created and scoped in the dashboard - see
        [Authentication](/api-reference/introduction#authentication).

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.