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

# Upsert a customer

> Enrolls a new customer on a specific card template, or upserts an existing one.

## Dedupe & upsert

The unique key is scoped to the `card_id`. Resolution order:

1. `external_id` (if provided)
2. `email` (case-insensitive)
3. `phone` (E.164 normalized)

If any match is found, the request UPDATES the existing customer. Otherwise a NEW customer is created.

Parallel requests for the same person on the same card create ONE customer: the first one creates it, the others wait for it and answer `200` with that customer.

## Wallet delivery

Optional `deliver` object sends the wallet pass link via email or SMS on creation (e.g., `{ "channel": "email" }`).

Without `deliver`, the response carries `claim_url` and you deliver it yourself.

## Idempotency

Send an `Idempotency-Key` header to make retries safe. See [Idempotency](/api-reference/idempotency).

## Required fields from the card form

The card `form_builder.fields[]` decides what this endpoint demands. A field marked `required: true` must be present and non-empty, and a PATCH may not clear it to empty. This holds whether or not the public signup page is enabled: `form_builder.enabled` governs that page, never this endpoint.

Enforcement covers the five system fields this request carries: `email`, `first_name`, `last_name`, `phone`, `date_of_birth`. Custom fields are enforced through the `custom` object.

The other two system keys are the exception. `consent` and `preferred_language` are collected by the public signup page ONLY and are never required here: this request declares no `consent` property, and `language` always resolves to the card default_language, so neither can ever be absent. Marking either required on a card changes nothing about this endpoint, and an API-created customer records no marketing consent from a form consent checkbox. Send `marketing_opt_in` if you want to record consent deliberately.

## Common failures

- `422 invalid_format` on `phone` - supply E.164 (`+14155552671`)
- `404 resource_missing` (param `card_id`) - the card does not exist, is archived, or does not belong to your organization
- `422 unknown_custom_field` - a key in `custom` that the card's form does not define
- `409 external_id_conflict` - another customer on this card already has that `external_id`
- `409 create_in_progress` - a parallel request for the same person on this card is still creating that customer. Retry after `Retry-After` seconds: the retry finds the customer
- `403 plan_limit_reached` - the plan's customer limit is already in use. Only a new customer counts against it: matching an existing customer is never blocked
- `422 unsupported_card_type` - `card_id` points to a `gift_card` or `membership` card; those types are dashboard-managed and don't take enrollments through this endpoint



## OpenAPI

````yaml /openapi.json post /v1/customers
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/customers:
    post:
      tags:
        - Customers
      summary: Upsert a customer
      description: >-
        Enrolls a new customer on a specific card template, or upserts an
        existing one.


        ## Dedupe & upsert


        The unique key is scoped to the `card_id`. Resolution order:


        1. `external_id` (if provided)

        2. `email` (case-insensitive)

        3. `phone` (E.164 normalized)


        If any match is found, the request UPDATES the existing customer.
        Otherwise a NEW customer is created.


        Parallel requests for the same person on the same card create ONE
        customer: the first one creates it, the others wait for it and answer
        `200` with that customer.


        ## Wallet delivery


        Optional `deliver` object sends the wallet pass link via email or SMS on
        creation (e.g., `{ "channel": "email" }`).


        Without `deliver`, the response carries `claim_url` and you deliver it
        yourself.


        ## Idempotency


        Send an `Idempotency-Key` header to make retries safe. See
        [Idempotency](/api-reference/idempotency).


        ## Required fields from the card form


        The card `form_builder.fields[]` decides what this endpoint demands. A
        field marked `required: true` must be present and non-empty, and a PATCH
        may not clear it to empty. This holds whether or not the public signup
        page is enabled: `form_builder.enabled` governs that page, never this
        endpoint.


        Enforcement covers the five system fields this request carries: `email`,
        `first_name`, `last_name`, `phone`, `date_of_birth`. Custom fields are
        enforced through the `custom` object.


        The other two system keys are the exception. `consent` and
        `preferred_language` are collected by the public signup page ONLY and
        are never required here: this request declares no `consent` property,
        and `language` always resolves to the card default_language, so neither
        can ever be absent. Marking either required on a card changes nothing
        about this endpoint, and an API-created customer records no marketing
        consent from a form consent checkbox. Send `marketing_opt_in` if you
        want to record consent deliberately.


        ## Common failures


        - `422 invalid_format` on `phone` - supply E.164 (`+14155552671`)

        - `404 resource_missing` (param `card_id`) - the card does not exist, is
        archived, or does not belong to your organization

        - `422 unknown_custom_field` - a key in `custom` that the card's form
        does not define

        - `409 external_id_conflict` - another customer on this card already has
        that `external_id`

        - `409 create_in_progress` - a parallel request for the same person on
        this card is still creating that customer. Retry after `Retry-After`
        seconds: the retry finds the customer

        - `403 plan_limit_reached` - the plan's customer limit is already in
        use. Only a new customer counts against it: matching an existing
        customer is never blocked

        - `422 unsupported_card_type` - `card_id` points to a `gift_card` or
        `membership` card; those types are dashboard-managed and don't take
        enrollments through this endpoint
      operationId: createCustomer
      parameters:
        - name: Idempotency-Key
          in: header
          description: >-
            Replay-safe key for this POST. Caching window is 24h: an identical
            body with the same key returns the original response without side
            effects; a different body with the same key returns 409
            idempotency_key_in_use. UUID v4 is recommended; any 8-255 char
            string from the allowed alphabet works.
          required: true
          example: 9b2f4a7e-2c1c-4d3e-9f8b-1234567890ab
          schema:
            type: string
            maxLength: 255
            format: uuid
            minLength: 8
            pattern: ^[A-Za-z0-9_-]{8,255}$
        - name: Accept-Language
          in: header
          description: One of en, sk, cs, de. Defaults to en.
          required: false
          example: sk
          schema:
            type: string
            enum:
              - en
              - sk
              - cs
              - de
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerCreateRequest'
            examples:
              Minimal:
                summary: Minimal
                value:
                  card_id: crd_sophcrprtvpmmnwxah2lomnkee
                  email: anna.smith@example.com
              Skeleton POS sync:
                summary: Skeleton POS sync
                value:
                  card_id: crd_sophcrprtvpmmnwxah2lomnkee
                  external_id: crm-contact-8f3c2a91
              Full profile:
                summary: Full profile
                value:
                  card_id: crd_sophcrprtvpmmnwxah2lomnkee
                  external_id: crm-customer-7421
                  first_name: Tomáš
                  last_name: Varga
                  email: thomas.wright@example.com
                  phone: '+421905123456'
                  date_of_birth: '1991-04-12'
                  language: sk
                  marketing_opt_in: true
                  location_id: loc_ibjeubrxvvruf6aq5jfnhrz6fa
              VIP onboarding tiered reward:
                summary: Vip onboarding tiered reward
                value:
                  card_id: crd_2hh2fekk5zazfyyvian4uhepyq
                  first_name: Lucia
                  last_name: Mokrá
                  email: lucy.morgan@example.com
                  language: sk
                  initial_state:
                    points: 500
              Discount card migration:
                summary: Discount card migration
                value:
                  card_id: crd_pxq7wlqupnagpeyaby256rhzme
                  external_id: old-loyalty-id-9981
                  first_name: Eva
                  last_name: Horáková
                  email: eve.harrison@example.com
                  language: sk
                  initial_state:
                    cumulative_spend: 125000
              Custom card with fields:
                summary: Custom card with fields
                value:
                  card_id: crd_g2yvof5i3kdhukbjhwiojpkwo4
                  first_name: Anna
                  email: andrea@example.com
                  custom:
                    pet_name: Rex
                    pet_breed: Labrador
                    pet_birthday: '2022-06-27'
                  metadata:
                    vet_practice_id: VET-001
                    lead_source: facebook_q2_2026
              Create and send immediately:
                summary: Create and send immediately
                value:
                  card_id: crd_sophcrprtvpmmnwxah2lomnkee
                  first_name: Mary
                  phone: '+421907555111'
                  language: sk
                  deliver:
                    channel: sms
        required: true
      responses:
        '200':
          description: Existing customer matched on dedupe key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
              example:
                id: cus_a4f72c91e0bd4185b6c2
                object: customer
                external_id: crm-customer-7421
                email: thomas.wright@example.com
                phone: null
                first_name: Tomáš
                last_name: Varga
                date_of_birth: null
                language: sk
                marketing_opt_in: true
                gdpr_redacted_at: null
                card_id: crd_2hh2fekk5zazfyyvian4uhepyq
                card_type: reward
                card:
                  card_id: crd_2hh2fekk5zazfyyvian4uhepyq
                  card_type: reward
                  status: installed
                  total_visits: 12
                  first_transaction_at: 1779580800
                  last_transaction_at: 1782345600
                  created_at: 1779494400
                  updated_at: 1782345600
                  points: 480
                  points_pending: 0
                  reward_tier:
                    tier_name: Silver
                    earn_multiplier: 1.5
                    tier_effective_date: 1781481600
                    previous_tier_name: Bronze
                signup_location_id: loc_ibjeubrxvvruf6aq5jfnhrz6fa
                status: installed
                device: apple_wallet
                installed_at: 1779494400
                apple_wallet_url: >-
                  https://api.vexioncards.one/api/passkit/download/afbd9cbc8b9ab72f43bf6ffc?t=dl_9f2c4e7a1b
                google_wallet_url: null
                claim_url: https://vexioncards.one/card/add/afbd9cbc8b9ab72f43bf6ffc
                serial_number: DEMO-RW-0000412
                serial_number_locked: true
                custom: {}
                metadata: {}
                last_delivery: null
                referral_stats: null
                created_at: 1779494400
                updated_at: 1782345600
                last_visited_at: 1782345600
                last_visited_location_id: loc_ibjeubrxvvruf6aq5jfnhrz6fa
                deleted_at: null
              examples:
                Matched by external ID:
                  summary: Matched by external ID
                  value:
                    id: cus_yovo5wk634htfojgkzdpmzvu5a
                    object: customer
                    external_id: crm-customer-7421
                    email: thomas.wright@example.com
                    phone: null
                    first_name: Tomáš
                    last_name: Varga
                    date_of_birth: null
                    language: sk
                    marketing_opt_in: true
                    gdpr_redacted_at: null
                    card_id: crd_2hh2fekk5zazfyyvian4uhepyq
                    card_type: reward
                    card:
                      card_id: crd_2hh2fekk5zazfyyvian4uhepyq
                      card_type: reward
                      status: installed
                      total_visits: 12
                      first_transaction_at: 1779580800
                      last_transaction_at: 1782345600
                      created_at: 1779494400
                      updated_at: 1782345600
                      points: 480
                      points_pending: 0
                      reward_tier:
                        tier_name: Silver
                        earn_multiplier: 1.5
                        tier_effective_date: 1781481600
                        previous_tier_name: Bronze
                    signup_location_id: loc_ibjeubrxvvruf6aq5jfnhrz6fa
                    status: installed
                    device: apple_wallet
                    installed_at: 1779494400
                    apple_wallet_url: >-
                      https://api.vexioncards.one/api/passkit/download/afbd9cbc8b9ab72f43bf6ffc?t=dl_9f2c4e7a1b
                    google_wallet_url: null
                    claim_url: https://vexioncards.one/card/add/afbd9cbc8b9ab72f43bf6ffc
                    serial_number: DEMO-RW-0000412
                    serial_number_locked: true
                    custom: {}
                    metadata: {}
                    last_delivery: null
                    referral_stats: null
                    created_at: 1779494400
                    updated_at: 1782345600
                    last_visited_at: 1782345600
                    last_visited_location_id: loc_ibjeubrxvvruf6aq5jfnhrz6fa
                    deleted_at: null
          headers: {}
        '201':
          description: Customer created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
              examples:
                Created stamp:
                  summary: Created stamp
                  value:
                    id: cus_b8c2e0a4f59d4a73a1e9
                    object: customer
                    external_id: null
                    email: anna.smith@example.com
                    phone: null
                    first_name: null
                    last_name: null
                    date_of_birth: null
                    language: sk
                    marketing_opt_in: false
                    gdpr_redacted_at: null
                    card_id: crd_sophcrprtvpmmnwxah2lomnkee
                    card_type: stamp
                    card:
                      card_id: crd_sophcrprtvpmmnwxah2lomnkee
                      card_type: stamp
                      status: installed
                      total_visits: 0
                      first_transaction_at: null
                      last_transaction_at: null
                      created_at: 1782604800
                      updated_at: 1782604800
                      remaining_stamps: 10
                      total_stamps: 10
                      stamps_pending: 0
                    signup_location_id: null
                    status: not_installed
                    device: null
                    installed_at: null
                    apple_wallet_url: >-
                      https://api.vexioncards.one/api/passkit/download/2ea6adef9d5ba7a4d358ad46?t=dl_9f2c4e7a1b
                    google_wallet_url: https://pay.google.com/gp/v/save/eyJhbGciOi...
                    claim_url: https://vexioncards.one/card/add/2ea6adef9d5ba7a4d358ad46
                    serial_number: 6ac53be9fe06508914775125
                    serial_number_locked: false
                    custom: {}
                    metadata: {}
                    last_delivery: null
                    referral_stats: null
                    created_at: 1782604800
                    updated_at: 1782604800
                    last_visited_at: null
                    last_visited_location_id: null
                    deleted_at: null
                Created custom with delivery:
                  summary: Created custom with delivery
                  value:
                    id: cus_d31a90e6b8c47fb251f0
                    object: customer
                    external_id: null
                    email: lucy.morgan@example.com
                    phone: '+421948555200'
                    first_name: Lucia
                    last_name: null
                    date_of_birth: null
                    language: sk
                    marketing_opt_in: false
                    gdpr_redacted_at: null
                    card_id: crd_zqpuluiwik4cs2aulxk3e54haa
                    card_type: custom
                    card:
                      card_id: crd_zqpuluiwik4cs2aulxk3e54haa
                      card_type: custom
                      status: installed
                      total_visits: 0
                      first_transaction_at: null
                      last_transaction_at: null
                      created_at: 1782604900
                      updated_at: 1782604900
                    signup_location_id: null
                    status: not_installed
                    device: null
                    installed_at: null
                    apple_wallet_url: >-
                      https://api.vexioncards.one/api/passkit/download/84ad785b092fd348211c8dcc?t=dl_9f2c4e7a1b
                    google_wallet_url: https://pay.google.com/gp/v/save/eyJhbGciOi...
                    claim_url: https://vexioncards.one/card/add/84ad785b092fd348211c8dcc
                    serial_number: PET-0000247
                    serial_number_locked: false
                    custom:
                      pet_name: Rex
                      pet_birthday: '2022-06-27'
                      pet_species: dog
                      favourite_treat: matcha biscuit
                    metadata: {}
                    last_delivery:
                      channel: email
                      status: sent
                      to: lucy.morgan@example.com
                      sent_at: 1782604901
                    referral_stats: null
                    created_at: 1782604900
                    updated_at: 1782604901
                    last_visited_at: null
                    last_visited_location_id: null
                    deleted_at: null
          headers: {}
        '400':
          description: Malformed body or missing required field.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                Missing card ID:
                  summary: Missing card ID
                  value:
                    error:
                      type: invalid_request_error
                      code: parameter_missing
                      message: 'Missing required parameter: card_id.'
                      param: card_id
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#parameter_missing
                      request_id: req_01jBhYqKsfXr8M2WVCdXyL9pZk
          headers: {}
        '401':
          description: Missing, invalid, revoked, or paused API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  type: authentication_error
                  code: missing_api_key
                  message: >-
                    Authorization header is required. Send 'Authorization:
                    Bearer vxn_sk_...'.
                  doc_url: >-
                    https://vexioncards.mintlify.site/api-reference/errors#missing_api_key
                  request_id: req_01jBhYrJyHb2pX7VHV4ZAcM6JL
              examples:
                No API key:
                  summary: No API key
                  value:
                    error:
                      type: authentication_error
                      code: missing_api_key
                      message: >-
                        Authorization header is required. Send 'Authorization:
                        Bearer vxn_sk_...'.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#missing_api_key
                      request_id: req_01jBhYrJyHb2pX7VHV4ZAcM6JL
                API key revoked:
                  summary: API key revoked
                  value:
                    error:
                      type: authentication_error
                      code: api_key_revoked
                      message: >-
                        This API key has been revoked. Mint a new key from the
                        dashboard at
                        https://app.vexioncards.one/settings/developers.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#api_key_revoked
                      request_id: req_vVbJZj2n73jYX2WxDxD7CzsycC
                API key paused:
                  summary: API key paused
                  value:
                    error:
                      type: authentication_error
                      code: api_key_paused
                      message: This API key is paused. Resume it from the dashboard.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#api_key_paused
                      request_id: req_VMN5a69XcfPA9b0aqaNn2NaApr
          headers: {}
        '403':
          description: >-
            API key lacks the required scope, or `plan_limit_reached`: the
            plan's maximum number of customers is already in use.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                Insufficient scope:
                  summary: Insufficient scope
                  value:
                    error:
                      type: permission_error
                      code: insufficient_scope
                      message: >-
                        This API key does not have the customers:write scope.
                        Required: customers:write.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#insufficient_scope
                      request_id: req_01jBhYrTr5J2K8L7M9N0P1Q2R3
                Organization paused:
                  summary: Organization paused
                  value:
                    error:
                      type: permission_error
                      code: organization_paused
                      message: >-
                        Organization is paused. Writes are blocked until billing
                        is reactivated in the dashboard.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#organization_paused
                      request_id: req_01jBhYsAk7L9M2P3Q4R5S6T7U8
          headers: {}
        '404':
          description: Resource not found in your organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  type: not_found_error
                  code: resource_missing
                  message: >-
                    Card 'crd_does_not_exist' was not found. It may be archived
                    or belong to another organization.
                  param: card_id
                  doc_url: >-
                    https://vexioncards.mintlify.site/api-reference/errors#resource_missing
                  request_id: req_01jBhYsRm8N3P4Q5R6S7T8U9V0
              examples:
                Card not found:
                  summary: Card not found
                  value:
                    error:
                      type: not_found_error
                      code: resource_missing
                      message: >-
                        Card 'crd_does_not_exist' was not found. It may be
                        archived or belong to another organization.
                      param: card_id
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#resource_missing
                      request_id: req_01jBhYsRm8N3P4Q5R6S7T8U9V0
          headers: {}
        '409':
          description: >-
            Idempotency-key reuse, `external_id_conflict` (another customer on
            this card already has that `external_id`), or `create_in_progress`
            (a parallel request for the same person on this card is still
            creating that customer: retry after `Retry-After` seconds). A
            `customer_erased` conflict is terminal: once GDPR erasure has run on
            a customer the record is a tombstone that keeps its transaction
            ledger but never accepts fresh PII again, and there is no un-erase
            flow.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                Idempotency key in use:
                  summary: Idempotency key in use
                  value:
                    error:
                      type: idempotency_error
                      code: idempotency_key_in_use
                      message: >-
                        This Idempotency-Key was used for a different request
                        body within the last 24h. Generate a new
                        Idempotency-Key.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#idempotency_key_in_use
                      request_id: req_01jBhYt7n9P4Q5R6S7T8U9V0W1
                Customer already erased (GDPR):
                  summary: Customer already erased (GDPR)
                  value:
                    error:
                      type: invalid_request_error
                      code: customer_erased
                      message: >-
                        This customer has been erased (GDPR
                        right-to-be-forgotten) and can no longer be modified.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#customer_erased
                      request_id: req_7d2e9a1c4b8f6
          headers: {}
        '413':
          description: >-
            Request body exceeds the 1 MB cap. The body was never parsed, so the
            request had no effect.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  type: invalid_request_error
                  code: request_too_large
                  message: The request body exceeds the maximum allowed size.
                  doc_url: >-
                    https://vexioncards.mintlify.site/api-reference/errors#request_too_large
                  request_id: req_5f8b2c4a9e1d3
        '422':
          description: >-
            Body parsed but semantically invalid. Most codes here carry
            `field_errors[]`; `contact_missing_for_channel` carries a single
            `param` instead.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                Custom field required missing:
                  summary: Custom field required missing
                  value:
                    error:
                      type: invalid_request_error
                      code: validation_failed
                      message: One or more custom fields failed validation.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#validation_failed
                      request_id: req_01jBhYtTq1R6S7T8U9V0W1X2Y3
                      field_errors:
                        - param: custom.pet_name
                          code: parameter_missing
                          message: Pet name is required
                Unsupported card type:
                  summary: card_id points to a gift_card or membership template
                  value:
                    error:
                      type: invalid_request_error
                      code: unsupported_card_type
                      message: >-
                        Creating a gift_card customer via the API isn't
                        supported yet. Supported: stamp, reward, discount,
                        custom, coupon.
                      param: card_id
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#unsupported_card_type
                      request_id: req_01jBhYu8r2S7T8U9V0W1X2Y3Z4
                No dedupe field:
                  summary: None of external_id, email, or phone was sent
                  value:
                    error:
                      type: invalid_request_error
                      code: validation_failed
                      message: >-
                        Provide at least one of external_id, email, or phone so
                        the server can run dedupe on subsequent POSTs against
                        the same card_id.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#validation_failed
                      request_id: req_01jBhYr5q2A4B7HVeXf3PpdN8M
                      field_errors:
                        - param: email
                          code: parameter_missing
                          message: email is required.
                        - param: phone
                          code: parameter_missing
                          message: phone is required.
                        - param: external_id
                          code: parameter_missing
                          message: external_id is required.
                Deliver with no contactable address:
                  summary: 'deliver: true with no contactable address'
                  value:
                    error:
                      type: invalid_request_error
                      code: contact_missing_for_channel
                      message: >-
                        deliver: true needs an email or a phone on the customer
                        to pick a channel. Supply one, or use the object form to
                        name a channel explicitly.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#contact_missing_for_channel
                      request_id: req_01jBhYtTq1R6S7T8U9V0W1X2Y3
                      param: deliver
                Starting points above the limit:
                  summary: initial_state.points above the starting bonus limit
                  value:
                    error:
                      type: invalid_request_error
                      code: validation_failed
                      message: One or more fields failed validation.
                      field_errors:
                        - param: initial_state.points
                          code: out_of_range
                          message: >-
                            initial_state.points is above the limit for this
                            card.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#validation_failed
                      request_id: req_01HW9Q3K2YJZ7M5RX1P8VTBNC4
          headers: {}
        '429':
          description: Per-minute or per-day rate limit exceeded. Respect Retry-After.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  type: rate_limit_error
                  code: rate_limited
                  message: Too many requests. Retry after 5 seconds.
                  doc_url: >-
                    https://vexioncards.mintlify.site/api-reference/errors#rate_limited
                  request_id: req_01jBhYuJt3T8U9V0W1X2Y3Z4A5
              examples:
                Rate limited:
                  summary: Rate limited
                  value:
                    error:
                      type: rate_limit_error
                      code: rate_limited
                      message: Too many requests. Retry after 5 seconds.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#rate_limited
                      request_id: req_01jBhYuJt3T8U9V0W1X2Y3Z4A5
          headers: {}
        '500':
          description: Server error. Retry-safe on idempotent calls.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  type: api_error
                  code: internal_error
                  message: >-
                    An internal error occurred. The request may be retried with
                    the same Idempotency-Key.
                  doc_url: >-
                    https://vexioncards.mintlify.site/api-reference/errors#internal_error
                  request_id: req_5f8b2c4a9e1d3
              examples:
                Http 500 default:
                  summary: Http 500 default
                  value:
                    error:
                      type: api_error
                      code: internal_error
                      message: >-
                        An internal error occurred. The request may be retried
                        with the same Idempotency-Key.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#internal_error
                      request_id: req_5f8b2c4a9e1d3
          headers: {}
      deprecated: false
components:
  schemas:
    CustomerCreateRequest:
      type: object
      required:
        - card_id
      description: >-
        POST /v1/customers REQUIRES card_id. There is no /v1/cards/{id}/issue
        verb. Must send at least one of external_id, email, phone.


        Validation: card_id is required AND at least one of (email, phone,
        external_id) must be present for customer deduplication.
      properties:
        card_id:
          type: string
          description: >-
            REQUIRED. The card template (crd_) the customer is enrolled in.
            Immutable after creation. Returns 404 if the card doesn't exist or
            is archived.


            **`gift_card` and `membership` cards do not take new customers
            here.** They are managed in the dashboard: enrolling on either
            returns `422 unsupported_card_type`. Stamp, reward, discount, coupon
            and custom cards are fully supported; reading is unaffected for
            every card type.
        external_id:
          type: string
          description: >-
            Your stable reference for this customer on this card (CRM ID,
            internal account number, etc.). Used for dedupe on re-POST. Highest
            priority dedupe field.
        email:
          type: string
          format: email
          description: >-
            Email address. At least one of email, phone or external_id is
            required. Used for wallet pass email delivery and dedupe.
          maxLength: 255
          example: customer@example.com
        phone:
          type: string
          description: >-
            Contact phone. Accepted in any common written form -
            `+421908123456`, `+421 908 123 456`, `+421-908-123-456`,
            `00421908123456`, or a bare Slovak local number like `0908123456`.
            The platform canonicalises it to E.164 on write, and every read
            returns strict E.164 (`^\\+[1-9]\\d{1,14}$`) or null. A value that
            cannot be resolved without guessing a country code is rejected with
            `422 invalid_format` rather than stored and silently reported as
            null.
          pattern: ^\+?[0-9][0-9\s().\-]{6,24}$
          minLength: 8
          maxLength: 16
          example: '+14155552671'
        first_name:
          type: string
          description: >-
            Customer's given name. Strongly recommended - the wallet pass
            renders this in the primary slot. When omitted, the pass shows blank
            where the name should be. You can update it later.
          example: Tomáš
          minLength: 1
          maxLength: 50
        last_name:
          type: string
          description: >-
            Customer's family name. Shown on the wallet pass alongside
            first_name when configured.
          maxLength: 50
        date_of_birth:
          type: string
          format: date
          description: >-
            Date of birth in ISO 8601 format (YYYY-MM-DD). Used for birthday
            campaigns and age-gated rewards.
        language:
          type: string
          enum:
            - sk
            - cs
            - en
            - de
          description: >-
            Preferred display language for the wallet pass and notifications.
            One of sk, cs, en, de. Defaults to the card's default_language.
        location_id:
          type: string
          description: >-
            The location where the customer signed up (QR scan location,
            in-store signup, and so on). Set on creation and cannot be changed
            afterwards. Required only if your card uses location-scoped
            settings.
        marketing_opt_in:
          type: boolean
          default: false
          description: >-
            A record of the customer's marketing consent, stored and returned
            for your own use and audits. Defaults to false when omitted. VEXiON
            does not apply it when sending notifications: a notification sent
            through the API is not filtered by this flag or by the customer's
            marketing switch in the dashboard. To respect opt-outs on API sends,
            send only to customers with marketing_opt_in true, using explicit
            `customer_ids`.
        custom:
          type: object
          additionalProperties: true
          description: >-
            Schema-validated custom field values. Keys must match the card's
            form_builder fields (type='custom' fields). Values validate against
            the referenced custom_field's type. Returns 422 if a required custom
            field is missing or a value fails validation.
          properties: {}
        metadata:
          type: object
          description: >-
            Free-form key-value map for partner-internal IDs and integration
            markers. NOT rendered in pass_fields 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.
          additionalProperties:
            type: string
            maxLength: 500
            description: Value (1-500 chars, string only).
          maxProperties: 50
          propertyNames:
            pattern: ^[a-zA-Z0-9_]{1,40}$
            description: Key (1-40 chars, alphanumeric + underscore).
          example:
            crm_secondary_id: marketing_456
            lead_source: facebook_ad_2026_q2
          properties: {}
        initial_state:
          type: object
          description: >-
            OPTIONAL per-customer override of the card's default initial state
            (welcome bonus stamps, starting points, etc.). When omitted, the
            customer is enrolled with the card's configured defaults. Server
            validates each field matches the card's type and returns 422 with
            param='initial_state.<field>' if mismatched.


            Honoured on creation only. When the call matches an existing
            customer (200), `initial_state` is ignored and no balance changes. A
            starting balance is not a transaction: it creates no ledger entry,
            sends no `transaction.created` event and does not count toward level
            spend. `field_errors[].code` is `invalid_for_card_type` (the field
            does not fit the card), `out_of_range` (above the card size, or
            `initial_state.points` above the starting bonus limit of the card:
            points 100,000, cashback about EUR 50 of credit in your currency,
            never above 1,000,000,000) or `prepaid_starts_empty`.
          additionalProperties: false
          properties:
            stamps_collected:
              type: integer
              minimum: 0
              description: >-
                STAMP cards only. Override starting stamp count. Default is the
                card's numberOfStampsWhenIssuingCard (welcome bonus, typically 0
                or 1). Must be <= the card's stamps_required total.


                **Prepaid stamp cards must start empty.** On a card with
                `mechanics.is_prepaid` true the only accepted value is `0` (or
                omitting the field): a prepaid card is a balance the customer
                buys, so it is issued empty, filled by a transaction, and then
                deducted from. A non-zero value returns `422` with
                `field_errors[].code = "prepaid_starts_empty"`. Ordinary
                collect-then-reward stamp cards are unaffected and may still be
                issued part-filled, which is how an existing paper card is
                migrated. Capped at 1,000 to prevent overflow abuse (stamp cards
                realistically require far fewer).
              example: 5
              maximum: 1000
            points:
              type: integer
              minimum: 0
              description: >-
                REWARD cards only (flat or tiered). Override starting point
                balance. Default is the card's numberOfPointsWhenIssuingCard
                (welcome bonus, typically 0). Use for migration from existing
                loyalty programs. The schema accepts up to 100,000,000,000, but
                the effective maximum is 1,000,000,000: a larger value is
                replaced by the card's starting points. On a cashback card
                (`redemption_mode: "cashback"`) the value is still points; the
                dashboard shows and edits it as credit in the business currency:
                credit = points x `cashback_point_value` (200 points at 0.01 =
                EUR 2.00). Limit: the same as the starting bonus of the card.
                Above it answers 422 `validation_failed` with
                `field_errors[].code` `out_of_range` on `initial_state.points`.
              example: 250
              maximum: 100000000000
            cumulative_spend:
              type: integer
              minimum: 0
              description: >-
                Discount cards. Override starting cumulative spend value; the
                starting level follows from this spend. On a reward card it
                answers 422 `invalid_for_card_type`. Useful when migrating
                customers with existing spend history. In minor units (cents).
                Maximum 100,000,000,000.
              example: 125000
              maximum: 100000000000
            coupon_status:
              type: string
              enum:
                - active
                - redeemed
              description: >-
                Issuance-time coupon state. `active` (default) or `redeemed`
                (pre-consumed, e.g. migration).
              example: active
          example:
            stamps_collected: 5
        deliver:
          type:
            - boolean
            - object
          properties:
            channel:
              type: string
              enum:
                - email
                - sms
                - both
            locale:
              type: string
          description: >-
            Triggers immediate email/SMS pass delivery on successful creation.


            Accepts either form:


            - `true` - deliver over the best channel the customer's own contact
            details allow: email when an `email` was supplied, otherwise SMS
            when a `phone` was supplied. Email wins when both are present,
            because an SMS spends money you did not explicitly ask to spend.

            - an object - explicit control: `channel` picks the transport,
            `locale` overrides the message language.


            When omitted or `false`, the customer is created but the pass is NOT
            auto-delivered (call `POST /v1/customers/{id}/deliver` separately,
            or share the `claim_url` manually).


            `deliver: true` on a customer with neither an `email` nor a `phone`
            is rejected with `422 contact_missing_for_channel` and the customer
            is NOT created, because there is no channel to pick. That case is
            reachable: `external_id` alone is a valid identifying field. The
            object form is not subject to that guard.


            **Honoured on creation only.** When this call matches an existing
            customer and returns `200` instead of `201`, `deliver` is ignored:
            use `POST /v1/customers/{id}/deliver` to (re)send to an existing
            customer.


            Delivery is best-effort: the customer is still created (201) if the
            send itself fails, and the outcome is recorded on `last_delivery`.
      anyOf:
        - required:
            - email
        - required:
            - phone
        - required:
            - external_id
    Customer:
      type: object
      required:
        - id
        - object
        - card_id
      properties:
        id:
          type: string
          description: >-
            Customer identifier - an opaque, server-issued token prefixed cus_.
            Encodes the internal record through a keyed cipher (AES-128 + an
            integrity tag, base32-encoded) - not decodable or guessable
            client-side; the prefix lets clients route on resource type without
            parsing the ID.
          pattern: ^cus_[A-Za-z0-9]+$
          example: cus_g2y5z2ylfkrybsthe2qnweualy
        object:
          type: string
          const: customer
          description: >-
            Always 'customer'. Type discriminator for consumers that handle
            multiple resource families.
        external_id:
          description: >-
            Your stable reference for this customer (CRM ID, internal account
            number, etc.). Highest-priority dedupe field on POST.
          type:
            - string
            - 'null'
        email:
          format: email
          description: >-
            Customer's email address. Used for wallet pass delivery, account
            recovery, and dedupe.
          maxLength: 255
          example: customer@example.com
          type:
            - string
            - 'null'
        phone:
          description: >-
            Customer's phone number in E.164 format. Used for SMS pass delivery
            and dedupe.
          pattern: ^\+[1-9]\d{1,14}$
          minLength: 8
          maxLength: 16
          example: '+14155552671'
          type:
            - string
            - 'null'
        first_name:
          description: Given name. Shown on the wallet pass when configured.
          type:
            - string
            - 'null'
        last_name:
          description: Family name. Shown on the wallet pass when configured.
          type:
            - string
            - 'null'
        date_of_birth:
          format: date
          description: >-
            Customer's date of birth in ISO 8601 format (YYYY-MM-DD). Used for
            birthday campaigns and age-gated rewards.
          type:
            - string
            - 'null'
        language:
          type: string
          enum:
            - sk
            - cs
            - en
            - de
            - hu
            - hr
            - ro
          description: >-
            Preferred display language for the wallet pass and notifications.
            Writable values are sk, cs, en, de; reads may additionally return
            hu, hr, or ro for legacy customers enrolled before the language set
            was narrowed.
        marketing_opt_in:
          type: boolean
          description: >-
            A record of the customer's marketing consent, stored and returned
            for your own use and audits. VEXiON does not apply it when sending
            notifications: a notification sent through the API is not filtered
            by this flag or by the customer's marketing switch in the dashboard.
            To respect opt-outs on API sends, send only to customers with
            marketing_opt_in true, using explicit `customer_ids`.
        gdpr_redacted_at:
          description: >-
            Unix timestamp when the customer requested GDPR
            right-to-be-forgotten. When set, all PII fields are anonymized but
            the skeleton record is preserved for analytics integrity. Distinct
            from deleted_at (soft-delete from admin or DELETE
            /v1/customers/{id}).
          example: null
          type:
            - integer
            - 'null'
        card_id:
          type: string
          description: Card template (crd_) this customer is enrolled in. Immutable.
        card_type:
          type: string
          enum:
            - stamp
            - reward
            - discount
            - coupon
            - membership
            - gift_card
            - custom
          description: >-
            Echo of the parent card's type for routing convenience. Mirrors
            card.type.
        card:
          $ref: '#/components/schemas/CustomerCardSlot'
          description: >-
            Type-specific mechanics data (stamps collected, points balance,
            tier, gift-card balance, etc.). Shape varies per card_type.
        signup_location_id:
          description: >-
            Location whose signup QR/link enrolled this customer (`?loc=` on the
            signup URL). Null for customers created via API or dashboard - only
            public signup-form enrollments carry it.
          type:
            - string
            - 'null'
        status:
          enum:
            - installed
            - not_installed
            - deleted
            - disputed
            - null
          description: >-
            Wallet pass install state on the customer's device. installed = pass
            on at least one device, not_installed = signed up but pass not yet
            added, deleted = customer removed the pass, disputed = wallet
            flagged the pass. A customer with no stored status reads
            `not_installed`; the value is never null.
          type:
            - string
            - 'null'
        device:
          enum:
            - apple_wallet
            - google_wallet
            - null
          description: >-
            Wallet platform holding the pass: `apple_wallet` or `google_wallet`.
            Null when no wallet pass is installed. The platform tracks Apple and
            Google Wallet only - any other install channel reads as null.
          type:
            - string
            - 'null'
        installed_at:
          description: >-
            Unix seconds when the pass was FIRST added to a wallet (Apple/Google
            install callback). Null until installed. Distinct from `created_at`
            (when the customer record was created at signup) - the gap between
            the two is your activation funnel.
          example: 1779494400
          type:
            - integer
            - 'null'
        apple_wallet_url:
          description: >-
            Direct download URL for the .pkpass file. Sent to iPhone customers
            via SMS or email.
          type:
            - string
            - 'null'
        google_wallet_url:
          description: >-
            Save-to-Google-Wallet URL for this customer's card, or `null` if not
            yet generated. Generated lazily: `null` on a freshly created
            customer, and populated on the first `GET /v1/customers/{id}` for
            that customer (not on create, and not on the list endpoint) -
            persisted after the first successful generation, so later reads
            return the same URL without regenerating it. A transient generation
            failure leaves this `null`; retry by reading the customer again.
          type:
            - string
            - 'null'
        claim_url:
          type: string
          description: >-
            Public claim URL the customer visits to add the pass to their
            wallet. Auto-detects device and serves the right format. Treat the
            URL as opaque - never construct or parse it; its internal format may
            change.
        serial_number:
          type: string
          description: >-
            The value encoded in the customer's wallet pass QR code or barcode,
            and the number printed on physical cards. A merchant-assigned custom
            number when `serial_number_locked` is true; otherwise a
            platform-assigned 24-character hex value, or a referral link ending
            in it when the card has referrals enabled. NOT the customer `id`: to
            find the customer behind a scanned code, call `GET
            /v1/customers?serial_number=<scanned value>`.
        serial_number_locked:
          type: boolean
          description: >-
            True when the serial number was custom-set by staff (do not
            regenerate). False when auto-generated.
        custom:
          type: object
          additionalProperties: true
          description: >-
            Custom field VALUES for this customer, keyed by the definition's
            `key`. Validated against the card's form and the `cf_` definitions -
            unknown keys are rejected (`422 unknown_custom_field`).


            Value shapes by field type: `text`/`textarea` → string; `number` →
            number (integer when the definition says so); `currency` → integer
            minor units; `date` → `YYYY` / `YYYY-MM` / `YYYY-MM-DD` per
            granularity; `boolean` → true/false; `select` → one option value;
            `multiselect` → array of option values.


            Renderable everywhere via `{{custom.<key>}}`: pass slots,
            notification title/body - typed values format in the customer's
            locale.
          properties: {}
        metadata:
          type: object
          description: >-
            Partner-internal `{key: value}` map (max 50 keys, key
            `^[a-zA-Z0-9_]{1,40}$`, string values ≤500 chars). NEVER rendered to
            the customer - for CRM ids, import batches, integration markers.
            Customer-facing typed data belongs in `custom` (validated against
            custom field definitions, renderable on passes/notifications).


            **Update semantics (JSON Merge Patch):** PATCH merges per key - send
            only the keys you change; `"key": null` deletes that key;
            `"metadata": null` clears the whole map; omitting `metadata` leaves
            it untouched.
          additionalProperties:
            type: string
            maxLength: 500
          maxProperties: 50
          propertyNames:
            pattern: ^[a-zA-Z0-9_]{1,40}$
          properties: {}
        last_delivery:
          properties:
            channel:
              type: string
              enum:
                - email
                - sms
                - both
            status:
              type: string
              enum:
                - sent
                - failed
              description: >-
                `sent` = the provider accepted the message (not proof of
                receipt); `failed` = provider rejection (bad number, bounced
                address).
            to:
              type: string
            sent_at:
              type: integer
            error:
              type:
                - string
                - 'null'
              description: >-
                Provider failure reason when `status: "failed"` (bounced
                address, invalid number). Null on success.
          description: >-
            Most recent email/SMS delivery attempt for this customer's wallet
            pass. Null if never delivered via API. Contains channel, destination
            (redacted email/phone), status, sent_at timestamp, and error message
            if delivery failed.
          type:
            - object
            - 'null'
        referral_stats:
          description: >-
            Referral program statistics for this customer. Null when the card
            template has no referral program configured. All counts are
            non-negative integers.
          properties:
            total_referrals:
              type: integer
              minimum: 0
              description: >-
                Total referrals sent by this customer (whether they converted or
                not).
              example: 3
            successful_referrals:
              type: integer
              minimum: 0
              description: Referrals that converted into active customers.
              example: 2
            pending_referrals:
              type: integer
              minimum: 0
              description: >-
                Referrals sent but not yet converted. A referral waits 90 days
                after the friend signs up for the first purchase; after that it
                expires and is not counted here.
              example: 1
            total_bonuses_earned:
              type: integer
              minimum: 0
              description: >-
                Total bonus stamps or points awarded for successful referrals -
                the same unit as the card's
                `referral.referrer_bonus`/`referral.referee_bonus`, and as
                `unit` on this object names directly (no need to cross-reference
                the enrolled card's `type` separately anymore).
              example: 200
            unit:
              type: string
              enum:
                - stamps
                - points
              description: >-
                What `total_bonuses_earned` counts, resolved from the enrolled
                card's type: `stamp` cards award `stamps`; every other card type
                (`reward`, including cashback redemption mode, which rides the
                same points ledger as any other Reward card) awards `points`.
                Mirrors the `unit` convention on Transaction.
              example: points
            last_referral_at:
              description: >-
                Unix timestamp of most recent referral sent. Null if never
                referred.
              example: 1782000000
              type:
                - integer
                - 'null'
          required:
            - total_referrals
            - successful_referrals
            - pending_referrals
            - total_bonuses_earned
            - unit
          type:
            - object
            - 'null'
        created_at:
          type: integer
          description: Unix timestamp when the customer record was created (signup time).
        updated_at:
          type: integer
          description: Unix timestamp when any field on the customer was last modified.
        last_visited_at:
          description: >-
            Unix seconds of the customer's most recent confirmed transaction -
            ANY channel (POS, scanner, API, online). Stored denormalized for
            fast filtering; equals the newest ledger entry (see GET
            /v1/transactions), except that gift card lock, employee refill and
            merchant issue rows do not move it.
          example: 1782604800
          type:
            - integer
            - 'null'
        level_valid_until:
          description: >-
            Unix seconds until which the customer keeps the current discount or
            reward tier (level retention, `mechanics.tier_demotion`). It is the
            end of the Bratislava calendar day: a qualifying transaction on or
            before that day keeps the level, and the first daily run after it
            moves the customer down one tier. Null when level retention is off
            on the card, the customer is on the lowest tier, the card cannot use
            level retention (employee discount, Booqme or Bookio card, flat
            reward, fewer than 2 tiers), or the card entry is deleted or
            disputed. Reminders go out 14 and 3 days before this date.
          example: 1807833599
          type:
            - integer
            - 'null'
        last_visited_location_id:
          description: >-
            Location of the most recent confirmed transaction - ANY channel.
            Stored denormalized for fast filtering; equals the newest ledger
            entry (see GET /v1/transactions), except that gift card lock,
            employee refill and merchant issue rows do not move it.
          example: loc_g2f2cbt7cbuz3hambazorvws2q
          type:
            - string
            - 'null'
        deleted_at:
          description: >-
            Unix seconds when the customer was soft-deleted (DELETE
            /v1/customers/{id} or dashboard). Null while active. Soft-deleted
            customers keep their FULL transaction ledger - history is never
            destroyed. Distinct from `gdpr_redacted_at` (right-to-be-forgotten
            anonymization).
          type:
            - integer
            - 'null'
    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)
                errors and on the retryable 409 conflicts
                (`idempotency_key_in_use`, `create_in_progress`,
                `concurrent_update`); 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 errors and
            retryable 409 conflicts 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.
    CustomerCardSlot:
      type: object
      description: >-
        Per-customer single card slot, trimmed by the parent card_type. Common
        fields (visit + transaction aggregates, slot timestamps) appear on every
        type; the type-specific block is selected by card_type. Points and
        cashback balance never coexist on one reward slot. Lifetime aggregates
        live in /v1/transactions, not here. See the Customers API reference for
        the per-type field tables.
      required:
        - card_id
        - card_type
      additionalProperties: false
      properties:
        card_id:
          type: string
          description: Public id of the parent card template (crd_).
        card_type:
          type: string
          enum:
            - stamp
            - reward
            - discount
            - coupon
            - membership
            - gift_card
            - custom
          description: >-
            Branch on this to read the variant fields. 'custom' carries no
            type-specific block - only the common fields above.
        status:
          type:
            - string
            - 'null'
          enum:
            - installed
            - not_installed
            - deleted
            - disputed
            - null
          description: Pass-install status for this slot.
        total_visits:
          type: integer
          description: Lifetime visit count on this slot.
        first_transaction_at:
          type:
            - integer
            - 'null'
          description: Unix seconds of the first transaction on this slot.
        last_transaction_at:
          type:
            - integer
            - 'null'
          description: Unix seconds of the most recent transaction on this slot.
        created_at:
          type: integer
          description: Unix seconds the slot was created.
        updated_at:
          type: integer
          description: Unix seconds the slot was last updated.
        remaining_stamps:
          type: integer
          description: 'Stamp only: stamps left until the next reward.'
        total_stamps:
          type: integer
          description: 'Stamp only: stamps required to complete one row.'
        stamps_pending:
          type: integer
          description: 'Stamp only: stamps awaiting confirmation.'
        points:
          type: number
          description: >-
            Reward (points modes) only: current points balance. Can carry up to
            4 decimals (see Transaction `value`).
        points_pending:
          type: number
          description: 'Reward (points modes) only: points awaiting confirmation.'
        reward_tier:
          type:
            - object
            - 'null'
          description: >-
            Reward cards with levels in use (tiered earn mode and at least one
            tier) only, points and cashback alike: the customer's current tier.
            The name and the multiplier are resolved live from the stored spend
            against the card's current tiers, so a tier edit shows at once.
            Absent when the card's levels are off. `tier_effective_date` and
            `previous_tier_name` are as stored and can lag `tier_name` after a
            tier edit.
          additionalProperties: false
          properties:
            tier_name:
              type:
                - string
                - 'null'
            earn_multiplier:
              type: number
            tier_effective_date:
              type:
                - integer
                - 'null'
            previous_tier_name:
              type:
                - string
                - 'null'
        cashback_balance_minor:
          type: integer
          description: >-
            Reward (cashback redemption mode) only: cashback balance in minor
            units. This is the credit the customer sees on the wallet pass and
            in push messages (points x `cashback_point_value`). On a card
            connected to a till that sets its own point value (Dotykacka,
            Storyous, Papaya, Oberon), the rate of the till is used, as on the
            wallet pass.
        cashback_pending_minor:
          type:
            - integer
            - 'null'
          description: >-
            Reward (cashback redemption mode) only. Always `null` in the current
            implementation - cashback redemptions finalize immediately at the
            till, so there is no pending/reservation state to report. Reserved
            for a possible future async confirmation flow.
        discount:
          type: object
          description: 'Discount only: current discount state.'
          additionalProperties: false
          properties:
            discount_level:
              type:
                - integer
                - 'null'
              description: Current tier percent off.
            period_spend_minor:
              type:
                - integer
                - 'null'
              description: Spend accumulated in the current window, in minor units.
            period_start_date:
              type:
                - string
                - 'null'
              description: YYYY-MM-DD the current window began.
            next_reset_date:
              type:
                - string
                - 'null'
              description: YYYY-MM-DD the window resets.
        coupon_status:
          type:
            - string
            - 'null'
          description: >-
            Coupon only: `active`, `used`, `redeemed` or `expired`. Always
            lowercase on the wire. The platform stores this value mixed case
            internally for historical reasons; the API normalises it, so branch
            on the lowercase form. `used` and `expired` are reserved: no current
            code path produces them, so a branch on either is dead code today.
          enum:
            - active
            - used
            - redeemed
            - expired
            - null
        membership:
          type: object
          description: 'Membership only: current membership state.'
          additionalProperties: false
          properties:
            is_active:
              type: boolean
            activated_at:
              type:
                - integer
                - 'null'
            expires_at:
              type:
                - integer
                - 'null'
            last_used_at:
              type:
                - integer
                - 'null'
            uses_today:
              type: integer
            total_uses:
              type: integer
        balance_minor:
          type: integer
          description: 'Gift card only: current balance in minor units.'
        expires_at:
          type:
            - integer
            - 'null'
          description: 'Gift card only: unix seconds the balance expires.'
        currency:
          $ref: '#/components/schemas/Currency'
          description: >-
            The business currency of the card. It is the currency of
            `cashback_balance_minor` and `balance_minor`. Also sent on the
            customer webhooks.
    Currency:
      type: string
      enum:
        - EUR
        - CZK
        - GBP
        - HUF
        - PLN
        - RON
        - CHF
        - USD
      description: >-
        Three-letter ISO 4217 code. The 8 currencies a business can use: EUR,
        CZK, GBP, HUF, PLN, RON, CHF, USD. Amounts in this API are minor units
        with 2 decimals for every currency, also HUF. See [Money and
        currency](/api-reference/money-and-currency).
  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.