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

# Retrieve a card template

> Retrieve a single card template by id. Returns the full Card object including design, pass_fields, mechanics, form_builder, referral configuration, issuer details, terms, registration URLs (with per-location variants), and live issued_count. Returns 404 if the card does not exist or is archived in another organization. Useful for verifying a card's configuration before enrolling customers via POST /v1/customers.



## OpenAPI

````yaml /openapi.json get /v1/cards/{id}
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/cards/{id}:
    get:
      tags:
        - Cards
      summary: Retrieve a card template
      description: >-
        Retrieve a single card template by id. Returns the full Card object
        including design, pass_fields, mechanics, form_builder, referral
        configuration, issuer details, terms, registration URLs (with
        per-location variants), and live issued_count. Returns 404 if the card
        does not exist or is archived in another organization. Useful for
        verifying a card's configuration before enrolling customers via POST
        /v1/customers.
      operationId: getCard
      parameters:
        - name: id
          in: path
          description: >-
            Card template identifier. Opaque, server-issued token, e.g.
            crd_c4zdlocdwajf6wtmnvvkonv4oa.
          required: true
          example: crd_sophcrprtvpmmnwxah2lomnkee
          schema:
            type: string
            pattern: ^crd_[A-Za-z0-9]+$
        - name: Accept-Language
          in: header
          description: >-
            Preferred language for error messages and localized fields. One of
            sk, cs, en, de. Defaults to card's default_language.
          required: false
          example: sk
          schema:
            type: string
            enum:
              - sk
              - cs
              - en
              - de
            default: sk
      responses:
        '200':
          description: Card template.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Card'
              examples:
                Stamp card active:
                  summary: Stamp card active
                  value:
                    type: stamp
                    name:
                      sk: Coffee Card
                      en: Coffee card
                    description:
                      sk: Collect 10 stamps, get the 11th coffee free.
                      en: Collect 10 stamps, get the 11th coffee free.
                    languages_supported:
                      - sk
                      - en
                    default_language: sk
                    barcode_format: QR
                    external_id: loyalty-coffee-stamp-2026
                    design:
                      text_color: '#FFFFFF'
                      bg_color: '#1F4E79'
                      logo_url: >-
                        https://stamp-icons.s3.eu-central-1.amazonaws.com/samples/logo-480x150.png?v=2026-09-17
                      icon_url: >-
                        https://stamp-icons.s3.eu-central-1.amazonaws.com/coffee.png
                      custom_background_image_url: null
                      pattern_type: null
                      show_logo_at_issuing_form: true
                      show_background_color_at_issuing_form: true
                      design_mode: standard
                      pattern_icon_urls: []
                      strip_color: '#36C6FA'
                    pass_fields:
                      header: null
                      front_left:
                        label:
                          sk: Card
                          en: Card
                        value_template: '{{serial_number}}'
                        change_message: null
                      front_right: null
                      back:
                        - key: terms
                          label:
                            sk: Terms
                            en: Terms
                          value_template: Full terms at www.example.com
                          change_message: null
                    mechanics:
                      type: stamp
                      stamps_required: 10
                      is_prepaid: false
                      starting_stamps: 1
                      reward_at_completion: 1x Free coffee (any size)
                      stamp_count_images: null
                    form_builder:
                      enabled: true
                      default_language: sk
                      fields:
                        - id: email
                          type: system
                          system_key: email
                          required: true
                          order: 0
                        - id: first_name
                          type: system
                          system_key: first_name
                          required: true
                          order: 1
                        - id: phone
                          type: system
                          system_key: phone
                          required: false
                          order: 2
                    referral:
                      enabled: true
                      referrer_bonus: 1
                      referee_bonus: 1
                    issuer:
                      company_name: Demo Bakery s.r.o.
                      email: info@demobakery.sk
                      phone: '+421901234567'
                    terms_of_use:
                      enabled: true
                      text: >-
                        Card je neprenosna a viazana na vasu identifikaciu.
                        Sluzi na zber peciatok pri nakupe v prevadzkach Demo
                        Bakery. Card je platna pocas trvania programu.
                      full_url: https://www.demobakery.sk/terms
                    issue_limit: null
                    metadata: {}
                    id: crd_sophcrprtvpmmnwxah2lomnkee
                    object: card
                    issued_count: 42
                    registration_url: >-
                      https://vexioncards.one/signup/?id=crd_sophcrprtvpmmnwxah2lomnkee
                    status: active
                    created_at: 1782604800
                    updated_at: 1782604800
                    archived_at: null
                Reward tiered with demotion:
                  summary: Reward tiered with demotion
                  value:
                    type: reward
                    name:
                      sk: Vernostny program
                      en: Loyalty program
                    description:
                      sk: >-
                        Postup tier-mi a ziskaj nasoboky bodov. Vymen body za
                        produkty z menu.
                      en: >-
                        Progress through tiers to earn point multipliers. Redeem
                        points for menu items.
                    languages_supported:
                      - sk
                      - en
                    default_language: sk
                    barcode_format: QR
                    design:
                      text_color: '#FFFFFF'
                      bg_color: '#1F4E79'
                      logo_url: >-
                        https://stamp-icons.s3.eu-central-1.amazonaws.com/samples/logo-480x150.png?v=2026-09-17
                      icon_url: >-
                        https://stamp-icons.s3.eu-central-1.amazonaws.com/coffee.png
                      custom_background_image_url: null
                      pattern_type: null
                      show_logo_at_issuing_form: true
                      show_background_color_at_issuing_form: true
                      design_mode: standard
                      pattern_icon_urls: []
                      strip_color: '#36C6FA'
                    pass_fields:
                      header:
                        label:
                          sk: Úrovne
                          en: Tier
                        value_template: '{{card.reward_tier.tier_name}}'
                        change_message: null
                      front_left:
                        label:
                          sk: Body
                          en: Points
                        value_template: '{{card.points}}'
                        change_message: null
                      front_right:
                        label:
                          sk: Násobok
                          en: Multiplier
                        value_template: '{{card.reward_tier.earn_multiplier}}x'
                        change_message: null
                      back: []
                    mechanics:
                      type: reward
                      earn_mode: tiered
                      earn_rate_per_unit: 1
                      starting_points: 0
                      redemption_mode: product
                      cashback_point_value: null
                      rewards:
                        - id: rwd_uas4xm344kos2gac5ky77axquy
                          name: Káva
                          points_required: 100
                        - id: rwd_z6dnezeod6ee4ingbnd5mxai4m
                          name: Tortový kúsok
                          points_required: 250
                        - id: rwd_n3f6kq2yv5xw4ztb7hcj2lm4pa
                          name: Combo menu
                          points_required: 500
                      tiers:
                        - id: tir_swbxxw6ogmmj2muabpnadygkee
                          name: Bronze
                          earn_multiplier: 1
                          spend_to_achieve_minor: 0
                        - id: tir_r4se33bb5tvfzqmm6czx3zltq4
                          name: Silver
                          earn_multiplier: 1.5
                          spend_to_achieve_minor: 50000
                        - id: tir_bu23sde3h6xbyb2vjplaehu2va
                          name: Gold
                          earn_multiplier: 2
                          spend_to_achieve_minor: 200000
                      tier_demotion:
                        enabled: true
                        inactivity_days: 365
                    form_builder:
                      enabled: true
                      default_language: sk
                      fields:
                        - id: email
                          type: system
                          system_key: email
                          required: true
                          order: 0
                        - id: first_name
                          type: system
                          system_key: first_name
                          required: true
                          order: 1
                        - id: phone
                          type: system
                          system_key: phone
                          required: false
                          order: 2
                    referral:
                      enabled: true
                      referrer_bonus: 200
                      referee_bonus: 100
                    issuer:
                      company_name: Demo Coffee s.r.o.
                      email: info@demobakery.sk
                      phone: '+421901234567'
                    terms_of_use:
                      enabled: true
                      text: >-
                        Card je neprenosna a viazana na vasu identifikaciu.
                        Sluzi na zber peciatok pri nakupe v prevadzkach Demo
                        Bakery. Card je platna pocas trvania programu.
                      full_url: https://www.demobakery.sk/terms
                    metadata: {}
                    id: crd_2hh2fekk5zazfyyvian4uhepyq
                    object: card
                    issued_count: 42
                    issue_limit: null
                    registration_url: >-
                      https://vexioncards.one/signup/?id=crd_2hh2fekk5zazfyyvian4uhepyq
                    status: active
                    external_id: null
                    created_at: 1782604800
                    updated_at: 1782604800
                    archived_at: null
                Giftcard with online sales:
                  summary: Giftcard with online sales
                  value:
                    type: gift_card
                    name:
                      sk: Online darcekova karta
                      en: Online gift card
                    description:
                      sk: Kup online a posli ako darcek e-mailom.
                      en: Buy online and send as a gift via email.
                    languages_supported:
                      - sk
                      - en
                    default_language: sk
                    barcode_format: QR
                    design:
                      text_color: '#FFFFFF'
                      bg_color: '#1F4E79'
                      logo_url: >-
                        https://stamp-icons.s3.eu-central-1.amazonaws.com/samples/logo-480x150.png?v=2026-09-17
                      icon_url: >-
                        https://stamp-icons.s3.eu-central-1.amazonaws.com/coffee.png
                      custom_background_image_url: null
                      pattern_type: null
                      show_logo_at_issuing_form: true
                      show_background_color_at_issuing_form: true
                      design_mode: standard
                      pattern_icon_urls: []
                      strip_color: '#36C6FA'
                    pass_fields:
                      header: null
                      front_left:
                        label:
                          sk: Card
                          en: Card
                        value_template: '{{serial_number}}'
                        change_message: null
                      front_right: null
                      back:
                        - key: terms
                          label:
                            sk: Terms
                            en: Terms
                          value_template: Full terms at www.example.com
                          change_message: null
                    mechanics:
                      type: gift_card
                      preset_values:
                        - amount_minor: 2500
                          bonus_minor: 0
                          total_minor: 2500
                        - amount_minor: 5000
                          bonus_minor: 250
                          total_minor: 5250
                        - amount_minor: 10000
                          bonus_minor: 750
                          total_minor: 10750
                      bonus_enabled: true
                      expiration_months: 24
                      is_employee_card: false
                      allow_online_sales: true
                      online_sale_location_id: loc_g2f2cbt7cbuz3hambazorvws2q
                    form_builder:
                      enabled: true
                      default_language: sk
                      fields:
                        - id: email
                          type: system
                          system_key: email
                          required: true
                          order: 0
                        - id: first_name
                          type: system
                          system_key: first_name
                          required: true
                          order: 1
                        - id: phone
                          type: system
                          system_key: phone
                          required: false
                          order: 2
                    referral:
                      enabled: false
                      referrer_bonus: 0
                      referee_bonus: 0
                    issuer:
                      company_name: Demo Bakery s.r.o.
                      email: info@demobakery.sk
                      phone: '+421901234567'
                    terms_of_use:
                      enabled: true
                      text: >-
                        Card je neprenosna a viazana na vasu identifikaciu.
                        Sluzi na zber peciatok pri nakupe v prevadzkach Demo
                        Bakery. Card je platna pocas trvania programu.
                      full_url: https://www.demobakery.sk/terms
                    metadata:
                      stripe_product_id: prod_O7TestExample
                    id: crd_unf6tb3sqiiehwshnx2cujc4xe
                    object: card
                    issued_count: 42
                    issue_limit: null
                    registration_url: >-
                      https://vexioncards.one/signup/?id=crd_unf6tb3sqiiehwshnx2cujc4xe
                    status: active
                    external_id: null
                    created_at: 1782604800
                    updated_at: 1782604800
                    archived_at: null
                Custom pet clinic:
                  summary: Custom pet clinic
                  value:
                    type: custom
                    name:
                      sk: Pet karta
                      en: Pet card
                    description:
                      sk: Card pacienta veterinarnej kliniky.
                      en: Veterinary clinic patient card.
                    languages_supported:
                      - sk
                      - en
                    default_language: sk
                    barcode_format: Code128
                    design:
                      text_color: '#FFFFFF'
                      bg_color: '#1F4E79'
                      logo_url: >-
                        https://stamp-icons.s3.eu-central-1.amazonaws.com/samples/logo-480x150.png?v=2026-09-17
                      icon_url: >-
                        https://stamp-icons.s3.eu-central-1.amazonaws.com/coffee.png
                      custom_background_image_url: null
                      pattern_type: null
                      show_logo_at_issuing_form: true
                      show_background_color_at_issuing_form: true
                      design_mode: standard
                      pattern_icon_urls: []
                      strip_color: '#36C6FA'
                    pass_fields:
                      header: null
                      front_left:
                        label:
                          sk: Majitel
                          en: Owner
                        value_template: '{{first_name}} {{last_name}}'
                        change_message: null
                      front_right:
                        label:
                          sk: Plemeno
                          en: Breed
                        value_template: '{{custom.pet_breed}}'
                        change_message: null
                      back:
                        - key: vet
                          label:
                            sk: Veterinar
                            en: Vet
                          value_template: '{{custom.assigned_vet}}'
                          change_message: null
                        - key: allergies
                          label:
                            sk: Alergie
                            en: Allergies
                          value_template: '{{custom.allergies | default:''Ziadne''}}'
                          change_message: null
                    mechanics: null
                    custom_fields: null
                    form_builder:
                      enabled: true
                      default_language: sk
                      fields:
                        - id: email
                          type: system
                          system_key: email
                          required: true
                          order: 0
                        - id: first_name
                          type: system
                          system_key: first_name
                          required: true
                          order: 1
                        - id: phone
                          type: system
                          system_key: phone
                          required: false
                          order: 2
                        - id: pet_name
                          type: custom
                          custom_field_id: cf_ydd35cjwmxfnl7f76iznatiode
                          required: true
                          order: 3
                        - id: pet_breed
                          type: custom
                          custom_field_id: cf_4mwhzvnsuxz6v6awnp3dee46n4
                          required: true
                          order: 4
                        - id: assigned_vet
                          type: custom
                          custom_field_id: cf_mmr7jvqp4j4wys6it3wrgfj33y
                          required: true
                          order: 5
                    referral:
                      enabled: false
                      referrer_bonus: 0
                      referee_bonus: 0
                    issuer:
                      company_name: Demo Clinic Clinic s.r.o.
                      email: info@democlinic.sk
                      phone: '+421901234567'
                    terms_of_use:
                      enabled: true
                      text: >-
                        Card je neprenosna a viazana na vasu identifikaciu.
                        Sluzi na zber peciatok pri nakupe v prevadzkach Demo
                        Bakery. Card je platna pocas trvania programu.
                      full_url: https://www.demobakery.sk/terms
                    metadata: {}
                    id: crd_qitthvziwgn552o6evjqm425im
                    object: card
                    issued_count: 42
                    issue_limit: null
                    registration_url: >-
                      https://vexioncards.one/signup/?id=crd_qitthvziwgn552o6evjqm425im
                    status: active
                    external_id: null
                    created_at: 1782604800
                    updated_at: 1782604800
                    archived_at: null
          headers: {}
        '400':
          description: Malformed body or missing required field.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  type: invalid_request_error
                  code: invalid_query_parameter
                  message: Invalid value for a query parameter.
                  doc_url: >-
                    https://vexioncards.mintlify.site/api-reference/errors#invalid_query_parameter
                  request_id: req_4d8b1e2f9c3a5
              examples:
                Http 400 default:
                  summary: Http 400 default
                  value:
                    error:
                      type: invalid_request_error
                      code: invalid_query_parameter
                      message: Invalid value for a query parameter.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#invalid_query_parameter
                      request_id: req_4d8b1e2f9c3a5
          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_5f8b2c4a9e1d3
              examples:
                Missing API key:
                  summary: Missing 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_5f8b2c4a9e1d3
                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_NxVtX29T50QVS5rdBXfQTFdSwh
                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_K7N5bDSNvJWzdb8GwQAeQYxQp3
          headers: {}
        '403':
          description: API key lacks the required scope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  type: permission_error
                  code: insufficient_scope
                  message: This API key does not have the cards:read scope.
                  doc_url: >-
                    https://vexioncards.mintlify.site/api-reference/errors#insufficient_scope
                  request_id: req_5f8b2c4a9e1d3
              examples:
                Insufficient scope:
                  summary: Insufficient scope
                  value:
                    error:
                      type: permission_error
                      code: insufficient_scope
                      message: This API key does not have the cards:read scope.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#insufficient_scope
                      request_id: req_5f8b2c4a9e1d3
          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_8f3a2b1c000000' was not found.
                  doc_url: >-
                    https://vexioncards.mintlify.site/api-reference/errors#resource_missing
                  request_id: req_5f8b2c4a9e1d3
                  param: id
              examples:
                Card not found:
                  summary: Card not found
                  value:
                    error:
                      type: not_found_error
                      code: resource_missing
                      message: Card 'crd_does_not_exist_8f3a2b1c000000' was not found.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#resource_missing
                      request_id: req_5f8b2c4a9e1d3
                      param: id
          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. Try again in 5 seconds.
                  retry_after: 5
                  doc_url: >-
                    https://vexioncards.mintlify.site/api-reference/errors#rate_limited
                  request_id: req_2c8e4f6a9d1b3
              examples:
                Http 429 default:
                  summary: Http 429 default
                  value:
                    error:
                      type: rate_limit_error
                      code: rate_limited
                      message: Too many requests. Try again in 5 seconds.
                      retry_after: 5
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#rate_limited
                      request_id: req_2c8e4f6a9d1b3
          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.
                  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.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#internal_error
                      request_id: req_5f8b2c4a9e1d3
          headers: {}
      deprecated: false
components:
  schemas:
    Card:
      type: object
      description: >-
        A card template defines the loyalty program a merchant offers. The
        template controls visual design, mechanics
        (stamps/points/discount/etc.), the customer signup form, wallet pass
        content, and behavioral defaults. Customers are enrolled in exactly one
        card via POST /v1/customers. Cards are immutable on type after creation.
      required:
        - id
        - type
        - name
        - description
        - default_language
        - languages_supported
        - design
        - issuer
        - mechanics
      properties:
        id:
          type: string
          pattern: ^crd_[A-Za-z0-9]+$
          description: >-
            Card identifier - an opaque, server-issued token prefixed crd_.
            Immutable.
          example: crd_sophcrprtvpmmnwxah2lomnkee
        object:
          type: string
          const: card
          description: >-
            Resource type discriminator. Always 'card' for this resource. Useful
            for client libraries that handle polymorphic responses (e.g.,
            webhooks delivering mixed event types). Server always sets it.
          example: card
        type:
          type: string
          enum:
            - stamp
            - reward
            - discount
            - coupon
            - membership
            - gift_card
            - custom
          description: >-
            Card mechanics type. Immutable after creation. Drives which
            mechanics block applies, which initial_state overrides are valid on
            POST /v1/customers, and which wallet pass layout is used. 'custom'
            is partner-driven and has no built-in mechanics. Notifications reach
            every card type, including `custom`.
          example: stamp
        external_id:
          maxLength: 255
          description: >-
            Your stable reference for this card template (e.g., your internal
            product SKU). Useful for syncing with external systems. Persisted on
            the card template and returned on reads (null when unset). Not
            unique: two templates may share a reference, so `GET
            /v1/cards?external_id=` filters and can return more than one row.
            Send null to clear it.
          example: loyalty-coffee-stamp-2026
          type:
            - string
            - 'null'
        name:
          oneOf:
            - type: string
              minLength: 1
              maxLength: 100
            - type: object
              additionalProperties:
                type: string
                maxLength: 100
              description: Per-language map (sk, cs, en, de -> name).
              properties: {}
          description: >-
            Display name of the card template. Accepts a plain string (used in
            all languages) OR a per-language object {sk: '...', en: '...'} for
            localized names.
          example:
            sk: Káva karta
            en: Coffee card
        description:
          description: >-
            REQUIRED. Brief customer-facing description of the card/loyalty
            program. Required by Apple PKPass (description field, used for
            accessibility/voice-over) and Google LoyaltyClass (programName
            description for review approval). Can be a string OR per-language
            object for multi-language passes. Min 1, max 500 chars per language.
          oneOf:
            - type: string
              minLength: 1
              maxLength: 500
            - type: object
              additionalProperties:
                type: string
                minLength: 1
                maxLength: 500
              description: >-
                Per-language object, e.g., { 'sk': 'Zbieraj 10 pečiatok...',
                'en': 'Collect 10 stamps...' }
              example:
                sk: Zbieraj 10 peciatok, ziskaj 11. kavu zdarma.
                en: Collect 10 stamps, get the 11th coffee free.
              properties: {}
          example:
            sk: Zbieraj 10 peciatok, ziskaj 11. kavu zdarma.
            en: Collect 10 stamps, get the 11th coffee free.
        languages_supported:
          type: array
          items:
            type: string
            enum:
              - sk
              - cs
              - en
              - de
          minItems: 1
          description: >-
            Languages this card template supports. Customer.language must be one
            of these. Pass content and notification templates must provide
            values for each.
          example:
            - sk
            - en
        default_language:
          type: string
          enum:
            - sk
            - cs
            - en
            - de
          description: >-
            Default language used when the customer's preferred language is not
            in languages_supported.
          example: sk
        barcode_format:
          type: string
          enum:
            - QR
            - Aztec
            - PDF417
            - Code128
            - Code39
            - EAN8
            - EAN13
            - ITF14
            - UPCA
            - DataMatrix
            - Codabar
          default: QR
          description: >-
            Barcode format rendered on the wallet pass to identify customers at
            POS. QR/Aztec/PDF417/Code128 work on both Apple and Google Wallet;
            others are Google Wallet only.
          example: QR
        purchase_amount_required:
          type: boolean
          default: false
          description: >-
            Scanner UX flag. When true, staff MUST enter a purchase amount
            before they can record a transaction against this card. When false,
            the amount field is optional (staff can record visit-only /
            quick-redeem transactions). Affects scanner behavior across all card
            types.
          example: false
        design:
          type: object
          description: >-
            Visual design of the wallet pass. Required: design_mode, bg_color,
            text_color, icon_url (logo_url is optional; the pass falls back to
            text when omitted).


            VISUAL LAYOUT by mode + type:
              - design_mode=standard + stamp: mechanics.stamp_icon_url + (optional custom_background_image_url)
              - design_mode=individual + stamp: mechanics.stamp_count_images (N+1 per-position images; image_url accepts a plain URL or an OPTIONAL per-language map - every position must share the same language set; each customer sees the variant in their language, else sk, else en, else the first one)
              - design_mode=standard + non-stamp: pattern_type + pattern_icon_urls + (optional custom_background_image_url)
                Server AUTO-GENERATES the final pattern image per supported language: pass a per-language object as card description and each language gets its own image; single-language cards (or a plain-string description) get one image. Each customer is served the image matching the language they picked at signup (fallback sk, then en, then the first one). No image URLs are needed for the standard pattern.
              - design_mode=individual + non-stamp: individual_background_image_url REQUIRED (accepts a plain URL or an OPTIONAL per-language map; each customer sees the variant in their language, else sk, else en, else the first one)

            TIER OVERRIDES: tiered reward/discount cards use
            mechanics.tiers[i].background_image_urls (per-language) which
            overrides any card-level background for customers at that tier.
          required:
            - design_mode
            - bg_color
            - text_color
            - icon_url
          properties:
            design_mode:
              type: string
              enum:
                - standard
                - individual
              description: >-
                Design approach: 'standard' uses icons from our library (10,000+
                3D icons across categories). 'individual' uses fully custom
                uploaded images. For STAMP: standard = single repeating icon,
                individual = per-stamp position images. For NON-STAMP: standard
                = pattern + up to 3 library icons, individual = full custom
                background.
              example: standard
            bg_color:
              type: string
              pattern: ^#[0-9A-Fa-f]{6}$
              description: >-
                REQUIRED. Pass background color in #RRGGBB hex. Maps to Apple
                PKPass.backgroundColor + Google LoyaltyClass.hexBackgroundColor.
              example: '#1F4E79'
            text_color:
              type: string
              pattern: ^#[0-9A-Fa-f]{6}$
              description: >-
                REQUIRED. Foreground text color in #RRGGBB hex. Maps to Apple
                PKPass.foregroundColor. Must contrast bg_color for
                accessibility.
              example: '#FFFFFF'
            strip_color:
              type: string
              pattern: ^#[0-9A-Fa-f]{6}$
              description: >-
                Strip / accent color in #RRGGBB hex - the panel behind the
                stamps (stamp cards) or behind the auto-generated pattern
                (non-stamp cards). Required on write when design_mode is
                standard. Not used for individual designs, where the uploaded
                background image covers this area.
              example: '#36C6FA'
            logo_url:
              type:
                - string
                - 'null'
              format: uri
              description: >-
                Wide header logo, or null when the card has none (the pass then
                shows text: Apple logoText = card name, Google programName).
                Image rules: https only, PNG only (magic-byte verified), ≤ 3MB,
                3.2:1 aspect ratio (480x150, within 5% tolerance), minimum
                240x75px (no ceiling). VEXiON stores a copy of every card image.
                This is the VEXiON copy URL, not necessarily the URL you sent.
              example: >-
                https://stamp-icons.s3.eu-central-1.amazonaws.com/samples/logo-480x150.png?v=2026-09-17
            icon_url:
              type: string
              format: uri
              description: >-
                REQUIRED. Small square icon shown in Wallet push notifications
                and list view (Apple PKPass icon.png / Google smallIconUrl) -
                the ONLY strictly-required image in the Apple PKPass spec;
                without it notifications fail to display. Image rules: https
                only, PNG only (magic-byte verified, never Content-Type), ≤ 3MB
                real streamed bytes, square (width equal to height, within 2%
                tolerance) between 88x88 and 2000x2000px. A non-library URL
                failing any of these returns 422 (invalid_image_url /
                image_format_invalid / image_too_large / image_not_square /
                image_resolution_too_small / image_resolution_too_large). A GET
                /v1/icons library icon is trusted pre-vetted square and only
                checked for https reachability. VEXiON stores a copy of the
                image. Reads return the VEXiON copy URL, not the URL you sent. A
                URL that VEXiON returned earlier (for example one you got back
                from this API, or a library icon) is kept as is.
              example: >-
                https://stamp-icons.s3.eu-central-1.amazonaws.com/samples/icon-512x512.png?v=2026-09-17
            custom_background_image_url:
              format: uri
              description: >-
                STANDARD design_mode only (any card type). Optional photo
                BACKDROP rendered BEHIND the auto-generated content (stamp icons
                for stamp cards, pattern + icons for non-stamp cards). Adds
                branded photography to the standard layout. Ignored when
                design_mode='individual' (use individual_background_image_url or
                mechanics.stamp_count_images instead). Must match the pass strip
                aspect ratio (~2.61:1, i.e. 527:202) within 5% tolerance and be
                at least 527px wide; the server returns 422
                image_aspect_ratio_invalid otherwise. Image rules: https only,
                PNG only (magic-byte verified), ≤ 3MB, width between 527px and
                4000px - 422 image_format_invalid / image_too_large /
                image_resolution_too_small / image_resolution_too_large on
                violation, in addition to image_aspect_ratio_invalid above.
                VEXiON stores a copy of the image. Reads return the VEXiON copy
                URL, not the URL you sent. A URL that VEXiON returned earlier
                (for example one you got back from this API, or a library icon)
                is kept as is.
              example: null
              type:
                - string
                - 'null'
            individual_background_image_url:
              description: >-
                Image URL: either a single URL string (used for all languages)
                or an object mapping language codes (sk, cs, en, de) to URLs for
                per-language variants (use when text is baked into the image).
                OPTIONAL per-language: a single URL is valid and serves every
                customer via fallback. WRITE: a map stores every variant; on
                PATCH a plain string (or null) clears any previously stored map.
                Map keys must be a subset of the card's languages_supported
                (defaulting to [default_language]) or the server returns 422
                parameter_invalid naming the offending key and the allowed set.
                SERVE: each customer sees the variant in their language, else
                'sk', else 'en', else the first one. Must match the pass strip
                aspect ratio (~2.61:1, i.e. 527:202) within 5% tolerance and be
                at least 527px wide (every language variant is checked); the
                server returns 422 image_aspect_ratio_invalid otherwise. Image
                rules per variant: https only, PNG only (magic-byte verified), ≤
                3MB, width between 527px and 4000px - 422 image_format_invalid /
                image_too_large / image_resolution_too_small /
                image_resolution_too_large on violation. VEXiON stores a copy of
                the image. Reads return the VEXiON copy URL, not the URL you
                sent. A URL that VEXiON returned earlier (for example one you
                got back from this API, or a library icon) is kept as is. Each
                language variant is mirrored to its OWN object, independently.
              anyOf:
                - type: string
                  format: uri
                  title: Single URL (all languages)
                  example: >-
                    https://stamp-icons.s3.eu-central-1.amazonaws.com/samples/backdrop-527x202.png?v=2026-09-17
                - type: object
                  title: Per-language URLs
                  additionalProperties:
                    type: string
                    format: uri
                  minProperties: 1
                  example:
                    sk: >-
                      https://stamp-icons.s3.eu-central-1.amazonaws.com/samples/backdrop-527x202.png?v=2026-09-17
                    en: >-
                      https://stamp-icons.s3.eu-central-1.amazonaws.com/samples/backdrop-527x202.png?v=2026-09-17
                  properties: {}
            pattern_type:
              enum:
                - brick
                - radial
                - diagonal
                - scattered
                - null
              description: >-
                Background pattern layout for NON-STAMP cards in STANDARD
                design_mode (Reward / Discount / Coupon / Membership / GiftCard
                / Custom). Options: 'brick' (grid), 'radial' (circular),
                'diagonal' (stripes), 'scattered' (random). Combined with
                pattern_icon_urls to render the card background. Ignored on
                STAMP cards (which use stamp_icon_url from mechanics instead).
                REQUIRED (non-null) for a STANDARD, non-stamp card: the server
                has no other content source for the pass background and returns
                422 missing_required_field on design.pattern_type when it is
                absent for that combination.
              example: brick
              type:
                - string
                - 'null'
            pattern_icon_urls:
              type: array
              maxItems: 3
              description: >-
                NON-STAMP cards in STANDARD design_mode only. 1-3 icon URLs
                tiled in the chosen pattern_type. Pick from GET /v1/icons
                (library icons are trusted pre-vetted squares) or supply a
                custom HTTPS URL; a non-library URL MUST be a square image
                (width equal to height, within 2% tolerance) or the server
                returns 422 image_not_square. REQUIRED (at least 1 entry) for a
                STANDARD, non-stamp card: the server returns 422
                missing_required_field on design.pattern_icon_urls when
                empty/absent for that combination. Ignored on STAMP cards. Full
                asset contract for a non-library URL: https only, PNG only
                (magic-byte verified), ≤ 3MB, square (±2% tolerance) between
                88x88 and 2000x2000px - 422 image_format_invalid /
                image_too_large / image_resolution_too_small /
                image_resolution_too_large on violation, in addition to
                image_not_square above. VEXiON stores a copy of the image. Reads
                return the VEXiON copy URL, not the URL you sent. A URL that
                VEXiON returned earlier (for example one you got back from this
                API, or a library icon) is kept as is. Each entry is mirrored to
                its OWN object, independently.
              items:
                type: string
                format: uri
              example:
                - https://stamp-icons.s3.eu-central-1.amazonaws.com/coffee.png
            show_logo_at_issuing_form:
              type: boolean
              default: true
              description: >-
                When true, the logo is rendered at the top of the public signup
                form at /signup/{card_id}. 
            show_background_color_at_issuing_form:
              type: boolean
              default: true
              description: >-
                When true, bg_color is used as the signup form background. False
                = white form regardless of pass color. 
        pass_fields:
          $ref: '#/components/schemas/PassFields'
        form_builder:
          type: object
          description: >-
            Configuration for the public customer signup form at
            /signup/{card_id}. Lists which fields to collect, in what order, and
            which are required.
          properties:
            enabled:
              type: boolean
              default: true
              description: >-
                When false, the public signup URL is fully disabled - customers
                can only be enrolled via API or dashboard.
            default_language:
              type: string
              enum:
                - sk
                - cs
                - en
                - de
              description: Default language the form renders in (customer can switch).
            fields:
              type: array
              description: >-
                Ordered list of fields shown on the signup form. Mix of system
                fields (email, phone, etc.) and references to org-level
                custom_fields.
              items:
                type: object
                required:
                  - id
                  - type
                  - order
                properties:
                  id:
                    type: string
                    description: >-
                      Stable identifier (matches the key used in customer.custom
                      or top-level system field).
                  type:
                    type: string
                    enum:
                      - system
                      - custom
                    description: >-
                      Source of the field. 'system' = built-in customer
                      attribute (email, first_name, etc.). 'custom' = references
                      an org-level CustomField.
                  system_key:
                    enum:
                      - email
                      - first_name
                      - last_name
                      - phone
                      - date_of_birth
                      - consent
                      - preferred_language
                      - null
                    description: >-
                      SYSTEM TYPE ONLY. Which built-in customer attribute this
                      field collects.
                    type:
                      - string
                      - 'null'
                  custom_field_id:
                    pattern: ^cf_[A-Za-z0-9]+$
                    description: >-
                      CUSTOM TYPE ONLY. Reference to an org-level CustomField
                      definition.
                    type:
                      - string
                      - 'null'
                  required:
                    type: boolean
                    default: false
                    description: >-
                      Whether the customer must fill this field to submit the
                      form (and whether POST /v1/customers requires it). On the
                      API, enforcement covers the five system fields the
                      customer request carries: `email`, `first_name`,
                      `last_name`, `phone`, `date_of_birth`. The other two
                      system keys are collected by the public signup page ONLY
                      and are never required through the API: the customer
                      request declares no `consent` property, and `language`
                      always resolves to the card default_language, so neither
                      can ever be absent. Marking `consent` or
                      `preferred_language` required therefore changes nothing
                      about POST /v1/customers, and an API-created customer
                      records no marketing consent from a form consent checkbox.
                      Send `marketing_opt_in` to record consent deliberately.
                  order:
                    type: integer
                    minimum: 0
                    description: Display position on the form (0-indexed).
                  translations_override:
                    type: object
                    description: >-
                      Per-language label overrides for this field, keyed by
                      language code: `{ "sk": { "label": "Meno" }, "en": {
                      "label": "Name" } }`. Falls back to the built-in preset
                      for a system field.
                    additionalProperties:
                      type: object
                      properties:
                        label:
                          type: string
        mechanics:
          description: >-
            Type-specific mechanics block. REQUIRED but must match Card.type.
            Pick the matching tab. Custom = pick 'Custom (null)'.
          anyOf:
            - $ref: '#/components/schemas/CardMechanics_Stamp'
            - $ref: '#/components/schemas/CardMechanics_Reward'
            - $ref: '#/components/schemas/CardMechanics_Discount'
            - $ref: '#/components/schemas/CardMechanics_Coupon'
            - $ref: '#/components/schemas/CardMechanics_Membership'
            - $ref: '#/components/schemas/CardMechanics_GiftCard'
            - title: Custom (null - no built-in mechanics)
              type: 'null'
              description: >-
                For Card.type='custom'. Notifications reach custom cards like
                any other type.
        issue_limit:
          minimum: 0
          description: >-
            Maximum number of customers that can be enrolled in this card. Null
            = unlimited. When reached, POST /v1/customers returns 403
            card_limit_reached. 0 and null both mean unlimited; both read back
            as null.
          example: 1000
          type:
            - integer
            - 'null'
        issued_count:
          type: integer
          minimum: 0
          readOnly: true
          description: >-
            Current number of customers enrolled in this card (read-only).
            Updated on every successful POST /v1/customers.
          example: 234
        referral:
          description: >-
            Referral program configuration. When enabled, customers can refer
            friends via a unique referral URL; both parties earn a bonus when
            the referee enrolls and completes a qualifying action.
          properties:
            enabled:
              type: boolean
              default: false
              description: When true, referral URLs are generated for every customer.
            referrer_bonus:
              type: integer
              minimum: 0
              default: 0
              description: >-
                Bonus awarded to the referrer when a referee converts. 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). The real limit depends on the card:
                stamps up to 1,000; points up to 100,000; cashback about EUR 50
                of credit in your currency (EUR 50, CZK 1,250, HUF 20,000, PLN
                200, RON 250, GBP 50, CHF 50, USD 50), so the most points are
                that amount divided by `cashback_point_value`; discount about
                EUR 1,000 of spend (CZK 25,000, HUF 400,000, PLN 4,000, RON
                5,000, GBP 1,000, CHF 1,000, USD 1,000). Whole numbers only. On
                a discount card the bonus is spend in whole currency units (not
                minor units) that raises the level. Above the limit the request
                answers 422 `validation_failed` with `field_errors[].code`
                `out_of_range` on `referral.referrer_bonus` or
                `referral.referee_bonus`. On PATCH only a changed value is
                checked, plus the stored value again when the change makes a
                point worth more.
            referee_bonus:
              type: integer
              minimum: 0
              default: 0
              description: >-
                Bonus awarded to the referee on enrollment. 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). The real limit depends on the card: stamps up to
                1,000; points up to 100,000; cashback about EUR 50 of credit in
                your currency (EUR 50, CZK 1,250, HUF 20,000, PLN 200, RON 250,
                GBP 50, CHF 50, USD 50), so the most points are that amount
                divided by `cashback_point_value`; discount about EUR 1,000 of
                spend (CZK 25,000, HUF 400,000, PLN 4,000, RON 5,000, GBP 1,000,
                CHF 1,000, USD 1,000). Whole numbers only. On a discount card
                the bonus is spend in whole currency units (not minor units)
                that raises the level. Above the limit the request answers 422
                `validation_failed` with `field_errors[].code` `out_of_range` on
                `referral.referrer_bonus` or `referral.referee_bonus`. On PATCH
                only a changed value is checked, plus the stored value again
                when the change makes a point worth more.
          example:
            enabled: true
            referrer_bonus: 50
            referee_bonus: 25
          type:
            - object
            - 'null'
        issuer:
          type: object
          description: >-
            REQUIRED. Issuer details. company_name is REQUIRED because it maps
            to Apple PKPass.organizationName (shown on lock screen) and Google
            LoyaltyClass.issuerName (shown in pass list). Without it, Apple
            shows 'Unknown Issuer' and Google rejects the pass during review.
          required:
            - company_name
          properties:
            company_name:
              type: string
              minLength: 1
              maxLength: 100
              description: >-
                REQUIRED. Business legal name. Maps to Apple
                PKPass.organizationName (lock screen) and Google
                LoyaltyClass.issuerName (max 20 chars displayed on small
                screens). Use the short customer-facing name (e.g., 'Demo
                Bakery', not 'Pekáreň Vlna s.r.o.').
              example: Demo Bakery
            email:
              format: email
              description: >-
                Issuer support email shown on pass back side (Apple) and contact
                details (Google). Optional but strongly recommended for customer
                support.
              example: support@demobakery.sk
              maxLength: 255
              type:
                - string
                - 'null'
            phone:
              description: >-
                Issuer support phone shown on pass back side. E.164 format
                preferred.
              example: '+421905123456'
              pattern: ^\+[1-9]\d{1,14}$
              minLength: 8
              maxLength: 16
              type:
                - string
                - 'null'
        terms_of_use:
          description: >-
            Optional terms and conditions shown on the wallet pass back side.
            `text` supports per-language (string OR object {sk, cs, en, de}) -
            the pass renders the customer's own language variant when present,
            falling back to the card's default-language text otherwise.
            `full_url` is a single URL only (no per-language variant).
          properties:
            enabled:
              type: boolean
              default: false
              description: Toggle to show/hide T&C on the pass.
            text:
              description: >-
                Inline T&C text (max 2000 chars per language). Single string OR
                per-language object. The card's default_language variant is
                always what the dashboard editor shows and edits; other-language
                variants render on the pass only for a customer whose preferred
                language matches them, falling back to the default-language text
                otherwise.
              anyOf:
                - type: string
                  title: Single text
                  maxLength: 2000
                  example: Terms apply...
                - type: object
                  title: Per-language
                  additionalProperties:
                    type: string
                    maxLength: 2000
                  example:
                    sk: Podmienky platia...
                    en: Terms apply...
                  properties: {}
            full_url:
              type: string
              format: uri
              description: >-
                Your loyalty programme rules, as one https URL. The signup page
                links them next to VEXiON's card terms (they supplement them,
                never replace them); the dashboard lists this link as the
                programme rules. Present only while the signup page shows the
                link; absent otherwise. Single URL, no per-language variant.
                When the link is shown, the signup checkbox reads that the
                member accepts VEXiON's card terms, the privacy policy and your
                loyalty programme rules, and the link is stored with the
                member's consent record. If your rules conflict with VEXiON's
                card terms, the card terms prevail. You are responsible for the
                content, lawfulness and availability of your rules; VEXiON does
                not review them.
          type:
            - object
            - 'null'
        registration_url:
          format: uri
          description: >-
            Public signup URL where customers self-enroll. Pattern:
            https://vexioncards.one/signup/?id={card_id}. Share via QR code,
            flyer, or email. Append ?loc={location_id} for per-location
            attribution (signup_location_id on the resulting customer). For
            per-location URLs of every location with this card enabled, call GET
            /v1/locations and check `enabled_card_ids` for this card's id (or
            read each location's `cards[]` (GET /v1/locations) - every entry
            carries its location-scoped `signup_url`). We previously had
            registration_urls_per_location on this response, but it duplicated
            data that lives on Location and forced an extra refresh whenever a
            location toggled the card on/off.
          example: https://vexioncards.one/signup/?id=crd_sophcrprtvpmmnwxah2lomnkee
          type:
            - string
            - 'null'
        metadata:
          type: object
          description: >-
            Free-form key-value map for partner-internal IDs and integration
            markers. NOT rendered on the wallet pass. 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.


            Persisted on the card template and returned on reads. Omitted
            entirely when empty rather than sent as null. On PATCH the map is
            merged per key, so sending one key never clears the others.
            (Distinct from `Customer.metadata`, which is built and live.)
          additionalProperties:
            type: string
            maxLength: 500
          maxProperties: 50
          propertyNames:
            pattern: ^[a-zA-Z0-9_]{1,40}$
          example:
            internal_campaign_id: Q3_2026_coffee_loyalty
            owner_team: marketing
          properties: {}
        status:
          type: string
          enum:
            - draft
            - active
            - paused
            - archived
          description: >-
            Template lifecycle. `draft` (created but never launched - not
            issuable, its public signup page turns customers away), `active`
            (issuable), `paused` (temporarily closed to NEW signups; existing
            passes keep working and keep earning), `archived` (retired -
            existing customer passes keep working, no new signups). A card
            created through this API is born `active`. DELETE archives, never
            destroys, and historical references stay resolvable forever.
          example: active
          default: active
        image_generation:
          type: object
          description: >-
            Server-side pass-image generation state. Standard-design cards get
            their wallet pass images (the per-stamp-state strip images, or the
            single pattern strip) rendered automatically after create and after
            design edits. The card is returned immediately and images render in
            the background, typically within seconds. processing: rendering in
            progress - the pass already works and images appear as soon as they
            are ready. completed: all images are live. failed: rendering hit an
            error and is retried automatically; the card itself is unaffected.
            not_applicable: individual-design cards (you supply the images) and
            cards with no generated imagery.
          properties:
            status:
              type: string
              enum:
                - not_applicable
                - processing
                - completed
                - failed
              description: Generation state.
              example: completed
            updated_at:
              description: >-
                Unix timestamp of the last generation state change. Null when
                not_applicable.
              example: 1782345600
              type:
                - integer
                - 'null'
            error:
              type:
                - string
                - 'null'
              description: >-
                Failure reason when status is failed (for example an unreachable
                custom image URL); null on every other status. Use it to
                self-diagnose and re-trigger generation with a corrected design
                PATCH.
          required:
            - status
        created_at:
          type: integer
          description: Unix timestamp when the card was created.
          example: 1782345600
        updated_at:
          type: integer
          description: Unix timestamp when any field on the card was last modified.
          example: 1782604800
        activated_at:
          type:
            - integer
            - 'null'
          description: >-
            Unix seconds when the card FIRST went live. Stamped on the first
            transition to `active` and never overwritten, so it survives
            pause/reactivate and un-archive cycles. Null for a card that has
            never been activated (a `draft`).
        paused_at:
          type:
            - integer
            - 'null'
          description: >-
            Unix seconds of the most recent transition to `paused`. Restamped on
            every pause. Null for a card that has never been paused. Not cleared
            when the card reactivates, so it records the last pause, not the
            current state - read `status` for that.
        archived_at:
          description: >-
            Unix timestamp when the card was archived (status='archived'). Null
            if active.
          example: null
          type:
            - integer
            - 'null'
        links:
          type: array
          description: >-
            Card-attached links shown on the wallet pass back side - business
            links (website/social) and a review-prompt link. Max 10 items.
          maxItems: 10
          items:
            type: object
            required:
              - type
              - value
            properties:
              type:
                type: string
                enum:
                  - website
                  - instagram
                  - facebook
                  - google_review
                description: >-
                  Link type. 'website' = company website, 'instagram'/'facebook'
                  = social profiles, 'google_review' = leave-a-review prompt for
                  Google Business.
              value:
                type: string
                description: The link value - always a full URL.
                minLength: 1
                maxLength: 500
          example:
            - type: website
              value: https://democoffee.sk
            - type: instagram
              value: https://instagram.com/democoffee
            - type: google_review
              value: https://g.page/r/abc123/review
        currency:
          $ref: '#/components/schemas/Currency'
          description: >-
            The business currency of this card. A business has one currency, so
            it is the same on every card. Set from the country of the first
            location and locked after the first transaction, customer balance or
            gift card purchase. See [Money and
            currency](/api-reference/money-and-currency).
    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.
    PassFields:
      type: object
      description: >-
        The wallet pass layout - **three front slots + back rows**, identical
        model on Apple and Google (the same shape the platform's partner
        integrations have used in production):


        | slot | Apple | Google |

        |---|---|---|

        | `header` | `headerFields[0]` (top-right) | top text module |

        | `front_left` / `front_right` | `secondaryFields` row |
        `textModulesData` front row |

        | `back[]` | `backFields` (all rows) | text modules (**first 10 shown**
        - Google's per-object cap; remaining rows are Apple-only) |


        The PRIMARY front area (the big number - stamps, points, balance) is
        **owned by the card's mechanics** and never configurable here. Typed
        values (`{{custom.<key>}}` of type date/number/currency/boolean) render
        locale-formatted on both platforms automatically.


        **PATCH replaces this object as a WHOLE, it does not merge per slot.**
        Any slot you omit is REMOVED from the card. Sending `{"pass_fields":
        {"header": {...}}}` on a card that also had `front_left`, `front_right`,
        `back` or `barcode_alt_text` clears those. To change one slot, send the
        full `pass_fields` object including every slot you want to keep, or read
        the card first and send it back with your edit applied. This differs
        from `metadata` and `custom` on other resources, which ARE JSON Merge
        Patch (a per-key merge where `null` deletes one key).
      properties:
        header:
          anyOf:
            - $ref: '#/components/schemas/PassFieldSlot'
            - type: 'null'
              title: Empty slot
          description: Top-of-pass slot, next to the logo. Null = empty.
        front_left:
          anyOf:
            - $ref: '#/components/schemas/PassFieldSlot'
            - type: 'null'
              title: Empty slot
          description: Left front slot under the primary area. Null = empty.
        front_right:
          anyOf:
            - $ref: '#/components/schemas/PassFieldSlot'
            - type: 'null'
              title: Empty slot
          description: Right front slot under the primary area. Null = empty.
        back:
          type: array
          items:
            $ref: '#/components/schemas/PassBackField'
          maxItems: 20
          description: >-
            Back-of-pass rows in display order. Max 20 (Google shows the first
            10 - order accordingly).
        barcode_alt_text:
          type:
            - string
            - 'null'
          maxLength: 100
          description: >-
            Template rendered per-customer into the text shown directly BELOW
            the barcode/QR on the pass (Apple `barcode.altText`). Single
            language, short - Apple truncates hard. e.g. "Member
            {{custom.member_id}}". Null or omitted = no alt text, consistent
            with header/front_left/front_right, which all document 'Null =
            empty'. Previously this one slot rejected null while its three
            siblings accepted it, so a partner clearing every slot at once got a
            422 on this field alone.
      example:
        header:
          label:
            en: Member
            sk: Člen
          value_template: '{{custom.membership_no}}'
          change_message: null
        front_left:
          label:
            en: Department
            sk: Oddelenie
          value_template: '{{custom.department}}'
          change_message: null
        front_right:
          label:
            en: Valid until
            sk: Platné do
          value_template: '{{custom.valid_until}}'
          change_message: Validity extended to {{value}}
        back:
          - key: opening_hours
            label:
              en: Opening hours
              sk: Otváracie hodiny
            value_template: Mon-Fri 8:00-18:00
            change_message: null
          - key: support
            label:
              en: Support
              sk: Podpora
            value_template: +421 900 000 000
            change_message: null
    CardMechanics_Stamp:
      type: object
      description: >-
        Mechanics configuration for STAMP cards ('buy 10 get 1 free' style).
        Includes welcome bonus, prepaid subscription mode, per-stamp image
        variants. Note: visits are always tracked automatically; no opt-in
        needed.
      required:
        - type
        - stamps_required
      properties:
        type:
          type: string
          const: stamp
          description: Discriminator. Always 'stamp' for this mechanics block.
        stamps_required:
          type: integer
          minimum: 1
          maximum: 20
          default: 10
          description: Total stamps the customer must collect to earn the reward.
          example: 10
        is_prepaid:
          type: boolean
          default: false
          description: >-
            Prepaid subscription mode. When true, the customer pays upfront for
            a package of stamps that staff load via the scanner 'Charge card'
            action, and each use deducts one stamp. Welcome bonus
            (starting_stamps) is ignored in this mode (always starts at 0).
            Cannot be changed after creation: PATCH with the other value returns
            400 field_immutable with param mechanics.is_prepaid.
          example: false
        starting_stamps:
          type: integer
          minimum: 0
          default: 0
          description: >-
            Welcome bonus stamps the customer receives automatically on
            enrollment (when not in prepaid mode). Per-customer override
            available via initial_state.stamps_collected on POST /v1/customers.
            Limit (VEXiON-231 round 13): at most the card's `stamps_required`,
            and never above 1,000. Above the limit the request answers 422
            `validation_failed` with `field_errors[].code` `out_of_range` on
            `mechanics.starting_stamps`. On PATCH only a changed value is
            checked, plus the stored value again when the change lowers
            `stamps_required`.
          example: 1
          maximum: 1000
        reward_at_completion:
          maxLength: 500
          description: >-
            Description of what the customer gets when they fill the card (e.g.,
            'Free coffee', 'Free haircut'). Shown on the pass and in
            notifications. When the customer redeems, the system deducts
            stamps_required from their current count (if they have 12 stamps on
            a 10-stamp card, redemption deducts 10, leaving 2).
          example: Free coffee of your choice
          type:
            - string
            - 'null'
        stamp_count_images:
          description: >-
            STAMP cards in INDIVIDUAL design_mode only. Per-position custom
            images for each stamp slot. Length MUST equal stamps_required + 1
            (enforced; a mismatched set would leave stamp positions without
            artwork) (last image is shown when the card is full / reward
            earned). Customer-uploaded full-card images for fully bespoke
            designs.
          maxItems: 51
          items:
            type: object
            required:
              - stamp_number
              - image_url
            properties:
              stamp_number:
                type: integer
                minimum: 0
                description: >-
                  Position (0 = before any stamps, 1 = first stamp filled, ...,
                  N = full card / reward).
              image_url:
                description: >-
                  Image URL: either a single URL string (used for all languages)
                  or an object mapping language codes (sk, cs, en, de) to URLs
                  for per-language variants (use when text is baked into the
                  image). OPTIONAL per position: a single URL is valid and
                  serves every customer via fallback. WRITE: a map stores every
                  variant. Map keys must be a subset of the card's
                  languages_supported (defaulting to [default_language]) or the
                  server returns 422 parameter_invalid naming the offending key
                  and the allowed set. UNIFORM COVERAGE: every position in
                  stamp_count_images must carry the SAME language set - a
                  per-language set is complete or absent across ALL positions;
                  mixing a plain string with a map, or maps with different keys,
                  returns 422 stamp_count_images_language_mismatch naming the
                  first mismatching position. SERVE: each customer sees the
                  variant in their language, else 'sk', else 'en', else the
                  first one. Must match the pass strip aspect ratio (~2.61:1,
                  i.e. 527:202) within 5% tolerance and be at least 527px wide
                  (every language variant is checked); the server returns 422
                  image_aspect_ratio_invalid otherwise. Image rules per variant:
                  https only, PNG only (magic-byte verified), ≤ 3MB, width
                  between 527px and 4000px - 422 image_format_invalid /
                  image_too_large / image_resolution_too_small /
                  image_resolution_too_large on violation. VEXiON stores a copy
                  of the image. Reads return the VEXiON copy URL, not the URL
                  you sent. A URL that VEXiON returned earlier (for example one
                  you got back from this API, or a library icon) is kept as is.
                  Each position and each language variant is mirrored to its OWN
                  object, keyed by stamp count (not array position),
                  independently.
                anyOf:
                  - type: string
                    format: uri
                    title: Single URL (all languages)
                    example: >-
                      https://stamp-icons.s3.eu-central-1.amazonaws.com/samples/backdrop-527x202.png?v=2026-09-17
                  - type: object
                    title: Per-language URLs
                    additionalProperties:
                      type: string
                      format: uri
                    minProperties: 1
                    example:
                      sk: >-
                        https://stamp-icons.s3.eu-central-1.amazonaws.com/samples/backdrop-527x202.png?v=2026-09-17
                      en: >-
                        https://stamp-icons.s3.eu-central-1.amazonaws.com/samples/backdrop-527x202.png?v=2026-09-17
                    properties: {}
          example: null
          type:
            - array
            - 'null'
        stamp_icon_url:
          format: uri
          description: >-
            STAMP cards in STANDARD design_mode only. Single icon URL used for
            all stamp positions on the wallet pass (the final position is
            automatically replaced with a gift icon for the reward). Pick from
            our library via GET /v1/icons or upload your own URL. Tightly
            coupled with this mechanics block because the icon is repeated
            stamps_required times. A non-library URL MUST be a square image
            (width equal to height, within 2% tolerance); the server returns 422
            image_not_square otherwise. A GET /v1/icons library icon is trusted
            pre-vetted square. Full asset contract for a non-library URL: https
            only, PNG only (magic-byte verified), ≤ 3MB, square (±2% tolerance)
            between 88x88 and 2000x2000px - 422 image_format_invalid /
            image_too_large / image_resolution_too_small /
            image_resolution_too_large on violation, in addition to
            image_not_square above. VEXiON stores a copy of the image. Reads
            return the VEXiON copy URL, not the URL you sent. A URL that VEXiON
            returned earlier (for example one you got back from this API, or a
            library icon) is kept as is.
          example: https://stamp-icons.s3.eu-central-1.amazonaws.com/coffee.png
          type:
            - string
            - 'null'
    CardMechanics_Reward:
      type: object
      description: >-
        Reward card mechanics. THREE LOGICAL CONFIGURATIONS:
          1. Flat + Product Rewards: single earn_rate for all, customer redeems for items from rewards[] catalog
          2. Flat + Cashback: single earn_rate for all, customer redeems points as cash discount (cashback_point_value)
          3. Tiered + Cashback: tiers[].earn_multiplier scales the base rate per tier, customer redeems as cashback
          4. (Allowed but rare) Tiered + Product: tiered earn + product catalog redemption

        earn_mode and redemption_mode are INDEPENDENT - any combination is
        valid. tier_demotion only fires when earn_mode='tiered' (no-op for
        flat).
      required:
        - type
        - earn_mode
        - earn_rate_per_unit
        - redemption_mode
      properties:
        type:
          type: string
          const: reward
          description: Discriminator. Always 'reward' for this mechanics block.
        earn_mode:
          type: string
          enum:
            - flat
            - tiered
          default: flat
          description: >-
            How points are earned: 'flat' = single earn_rate_per_unit for all
            customers; 'tiered' = base earn_rate scaled by
            tiers[].earn_multiplier based on the customer's qualifying spend
            (purchases that earned points, minus reversals). INDEPENDENT of
            redemption_mode - any combination works. Levels switch. Send
            earn_mode flat to switch levels off: the levels stay stored but GET
            returns tiers as an empty array and tier_demotion as null. Send
            tiered to switch them on: at least one level is required (422
            levels_need_a_level; a multiplier below 1 or a spend below 0 gives
            422 levels_invalid_level, both on mechanics.tiers). On switch-on
            every holder is recounted from the ledger in the background, so
            holder levels settle shortly after the 200 response, and a running
            level retention restarts its clock. When you edit levels while they
            are on, send the existing id of each level so renamed levels keep
            their holders.
          example: tiered
        earn_rate_per_unit:
          type: number
          minimum: 0
          default: 1
          description: >-
            Points earned per 1 unit of organization's currency spent (e.g., 1.0
            = 1 point per €1). Currency is set at Organization level
            (`default_currency` on GET /v1/organization) - not per card. For
            tiered mode, this is the BASE rate before tier multipliers. Capped
            at 1,000,000 to prevent overflow/absurd point calculations. 0 means
            the card earns no points from spend (on a cashback card it pays
            nothing). Leave the field out to get the default 1; a card with no
            rate set earns 1 point per currency unit. Cashback percent is
            earn_rate_per_unit x cashback_point_value x 100, times the level
            multiplier. The limit is 30 percent on a cashback card without
            levels and 60 percent per level (the level multiplier counts). Above
            the limit the request answers 422 `validation_failed` with
            `field_errors[].code` `out_of_range` on
            `mechanics.earn_rate_per_unit`, `mechanics.cashback_point_value` or
            `mechanics.tiers`, whichever you changed. Only a changed value is
            checked: a card that already pays more keeps saving other fields. On
            a card connected to Oberon or to Papaya with the card basis, the
            rate of the till counts for the limit instead of
            `cashback_point_value`.
          example: 1
          maximum: 1000000
        starting_points:
          type: integer
          minimum: 0
          default: 0
          description: >-
            Welcome bonus points awarded IMMEDIATELY when the customer enrolls
            (before any transaction). Set to 0 for no welcome bonus. The schema
            bound is 100,000,000,000, but the limit below applies. 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 (VEXiON-231 round 13, same as the referral bonus): a
            points card 100,000 points. A cashback card about EUR 50 of credit
            in the business currency (EUR 50, CZK 1,250, HUF 20,000, PLN 200,
            RON 250, GBP 50, CHF 50, USD 50), so the most points are that amount
            divided by `cashback_point_value` (EUR 50 at 0.01 allows 5,000
            points). Above the limit the request answers 422 `validation_failed`
            with `field_errors[].code` `out_of_range` on
            `mechanics.starting_points`. On PATCH only a changed value is
            checked, plus the stored value again when the change makes a point
            worth more (a higher `cashback_point_value`, or a switch into
            cashback).
          example: 100
          maximum: 100000000000
        redemption_mode:
          type: string
          enum:
            - product
            - cashback
          default: product
          description: >-
            How points are redeemed: 'product' = customer picks an item from
            rewards[] catalog (each item has points_required); 'cashback' =
            customer redeems points as cash discount at POS (1 point =
            cashback_point_value of currency). INDEPENDENT of earn_mode.
          example: cashback
        rewards:
          type: array
          description: >-
            PRODUCT REDEMPTION ONLY. Catalog of redeemable rewards, at most 20
            (more answers 422 `validation_failed` on `mechanics.rewards`). Each
            entry costs points_required and represents a specific product or
            perk.
          items:
            type: object
            required:
              - name
              - points_required
            properties:
              id:
                type: string
                description: >-
                  Stable identifier for this reward. GET always returns the
                  server's own opaque rwd_ id for a persisted reward; an id you
                  invent at create time is accepted but only becomes this
                  identity once you round-trip the real id back on a later
                  PATCH.
                example: rwd_uas4xm344kos2gac5ky77axquy
              points_required:
                type: integer
                minimum: 0
                description: >-
                  Points needed to redeem this reward, a whole number of at
                  least 1. Maximum 100,000,000,000.
                example: 500
                maximum: 100000000000
              value:
                description: >-
                  Fixed amount off in minor currency units, a whole number of at
                  least 1 (450 is EUR 4.50). It reads back for every currency,
                  not only the euro. A reward with neither `value` nor
                  `percentage` is a free product (100 percent off). A
                  percent-off reward carries `percentage` and reads back without
                  `value`. Send `value` or `percentage`, not both (422
                  `validation_failed` on the reward). You can send a reward back
                  unchanged in a PATCH: a reward with its `id` and with neither
                  `value` nor `percentage` keeps the stored reward. Maximum
                  100,000,000,000.
                example: 450
                type:
                  - number
                  - 'null'
                minimum: 0
                maximum: 100000000000
              percentage:
                type:
                  - integer
                  - 'null'
                minimum: 1
                maximum: 99
                description: >-
                  Percent off for a percent-off reward, a whole number from 1 to
                  99 (20 is 20 percent off the bill). Send it instead of
                  `value`. A free product has neither field. Outside 1 to 99
                  answers 422 `validation_failed` with `field_errors[].code`
                  `out_of_range`.
                example: 20
              name:
                type: string
                maxLength: 100
                description: >-
                  Display name of this reward (e.g., 'Free coffee', 'Free
                  t-shirt', '€5 off').
          example:
            - id: rwd_uas4xm344kos2gac5ky77axquy
              points_required: 500
              value: 450
              name: Free coffee
            - id: rwd_z6dnezeod6ee4ingbnd5mxai4m
              points_required: 300
              percentage: 20
              name: 20 percent off
            - id: rwd_3q8vfa7x2k5mnb4dcr6tywh9pe
              points_required: 800
              name: Free pastry
        cashback_point_value:
          default: 0.01
          description: >-
            CASHBACK REDEMPTION ONLY. Currency value of one point, already in
            MAJOR currency units (e.g., 0.01 = 1 point = EUR 0.01 = 100 points =
            EUR 1). This is a rate, not a minor-unit amount - **do not multiply
            by 100** before using it. The server does that conversion exactly
            once, internally, as `points * cashback_point_value * 100`, to
            produce `balance_after.cashback_minor`; multiplying it yourself
            first double-converts and inflates the money figure 100x. Capped at
            1,000,000 to prevent overflow/absurd cashback calculations. On a
            card connected to Dotykacka, Storyous, Papaya or Oberon, the rate of
            the connected till is used for money values and overrides this
            field. Send a value above 0 (0 and negative values answer 422
            `validation_failed`). The percent you pay back is limited, see
            `earn_rate_per_unit`.
          example: 0.01
          type:
            - number
            - 'null'
          maximum: 1000000
          minimum: 0
        tiers:
          type: array
          description: >-
            TIERED EARN MODE ONLY. Reward tier progression. Order from lowest to
            highest threshold (spend_to_achieve_minor, or visits_to_achieve for
            a Booqme/Bookio-linked card). Lowest tier MUST have that threshold =
            0 (everyone starts here). Each tier has an earn_multiplier that
            scales the base earn_rate_per_unit. Customer's tier is auto-computed
            from qualifying spend (purchases that earned points, minus
            reversals; or visit count, for a visit-gated card). Levels switch.
            Send earn_mode flat to switch levels off: the levels stay stored but
            GET returns tiers as an empty array and tier_demotion as null. Send
            tiered to switch them on: at least one level is required (422
            levels_need_a_level; a multiplier below 1 or a spend below 0 gives
            422 levels_invalid_level, both on mechanics.tiers). On switch-on
            every holder is recounted from the ledger in the background, so
            holder levels settle shortly after the 200 response, and a running
            level retention restarts its clock. When you edit levels while they
            are on, send the existing id of each level so renamed levels keep
            their holders. Level names are unique (a repeated name answers 422
            `validation_failed` with code `invalid_value` on `mechanics.tiers`),
            and each level needs a spend above the level before it.
          items:
            $ref: '#/components/schemas/CardTier_Reward'
          example:
            - id: tir_swbxxw6ogmmj2muabpnadygkee
              name: Bronze
              earn_multiplier: 1
              spend_to_achieve_minor: 0
            - id: tir_r4se33bb5tvfzqmm6czx3zltq4
              name: Silver
              earn_multiplier: 1.5
              spend_to_achieve_minor: 50000
            - id: tir_bu23sde3h6xbyb2vjplaehu2va
              name: Gold
              earn_multiplier: 2
              spend_to_achieve_minor: 200000
        tier_demotion:
          description: >-
            ONLY APPLIES when earn_mode='tiered' AND tiers.length >= 2. Level
            retention: a customer who makes no qualifying transaction for
            inactivity_days drops one tier (points are kept). Runs as a daily
            job; customers get a reminder 14 and 3 days before the drop. The
            date is on the customer as `level_valid_until`. No-op for flat earn
            mode.
          properties:
            enabled:
              type: boolean
              default: false
              description: >-
                Level retention switch. Omitted on create = false; omitted on
                PATCH keeps the stored value. Switching it on, or changing
                inactivity_days while it is on, starts one fresh window for
                every customer from that moment. Switching it off keeps every
                customer on the current tier.
            inactivity_days:
              type: integer
              minimum: 1
              maximum: 1095
              default: 365
              description: >-
                Number of days without a qualifying transaction after which the
                customer drops one tier. Range 1-1095 (up to ~3 years). Omitted
                on PATCH keeps the stored window (365 on create).
          example:
            enabled: true
            inactivity_days: 365
          type:
            - object
            - 'null'
    CardMechanics_Discount:
      type: object
      description: >-
        Discount card mechanics. Customer's current tier is determined by
        qualifying spend: cumulative spend, reset to the new tier's threshold
        when level retention drops the customer one tier. Single-tier cards:
        tiers array with 1 entry (everyone gets that percentage). Tiered cards:
        2+ entries, tier auto-computed from qualifying spend. Employee discount
        cards (is_employee_discount=true) add a monthly budget cap. Level
        retention (tier_demotion.enabled) lets dormant customers drop one tier
        after inactivity_days - applies only to tiered cards, not to employee
        discount cards.
      required:
        - type
        - tiers
      properties:
        type:
          type: string
          const: discount
          description: Discriminator. Always 'discount' for this mechanics block.
        tiers:
          type: array
          description: >-
            Discount tier progression. REQUIRED. Order from lowest to highest
            spend_to_achieve_minor. Lowest tier MUST have
            spend_to_achieve_minor=0 (everyone starts here). Single-tier cards:
            array with 1 entry. Tiered cards: 2+ entries. Customer's tier is
            computed from qualifying spend: lifetime cumulative spend minus the
            part removed when level retention dropped the customer a tier, so
            after a drop the customer climbs again from the lower tier's
            threshold. On an employee discount card the tiers are steps on this
            month's counted spend, not on lifetime spend. The counter follows
            employee.budget_counts_as and restarts on employee.reset_day. A
            higher threshold may carry a lower percentage (for example 100
            percent on the first 200, then 20 percent). The discount stops when
            the monthly limit is reached, unless employee.no_limit is true. A
            discount card needs at least 1 level (422 `validation_failed` on
            `mechanics.tiers`). Level names are unique and each level needs a
            spend above the level before it (code `invalid_value`). On an
            employee discount card the first step is at least 1 percent (code
            `out_of_range`). Only a changed value is checked: a card that was
            saved with an older step keeps saving other fields.
          items:
            $ref: '#/components/schemas/CardTier_Discount'
          example:
            - id: tir_xcwxqlyszbgqxkqtgszyqe6n3q
              name: Base
              spend_to_achieve_minor: 0
              percentage: 5
            - id: tir_tch3s2svgbwvmefsacsbcufp5q
              name: Silver
              spend_to_achieve_minor: 50000
              percentage: 10
            - id: tir_qbknazkjhjittxzwp2ijjvkqlq
              name: Gold
              spend_to_achieve_minor: 200000
              percentage: 15
        is_employee_discount:
          type: boolean
          default: false
          description: >-
            Employee discount mode. When true, the `employee` block sets a
            monthly limit or no limit. A new employee discount (on create, or
            when you switch a card to one) needs the `employee` block, else 422
            `parameter_missing` on `mechanics.employee.monthly_budget_minor`.
          example: false
        starting_points:
          type: integer
          minimum: 0
          default: 0
          description: >-
            Starting balance granted at enrollment, applied to the discount
            starting tier balance. Same field/semantics as reward mechanics'
            starting_points. Ignored (never applied) for an employee discount
            card (is_employee_discount=true) - the dashboard never offers a
            starting bonus for employee discount cards. The schema bound is
            100,000,000,000. The unit is whole or decimal major currency units
            (EUR 12.50 is 12.5), not minor units. It counts as spend toward the
            first level thresholds. Limit (VEXiON-231 round 13, same as the
            referral bonus): about EUR 1,000 of spend (CZK 25,000, HUF 400,000,
            PLN 4,000, RON 5,000, GBP 1,000, CHF 1,000, USD 1,000), in major
            currency units. Above the limit the request answers 422
            `validation_failed` with `field_errors[].code` `out_of_range` on
            `mechanics.starting_points`. On PATCH only a changed value is
            checked.
          example: 5
          maximum: 100000000000
        employee:
          description: >-
            EMPLOYEE DISCOUNT MODE ONLY. Budget configuration. With a monthly
            limit, the employee can spend up to monthly_budget_minor per period
            before discounts stop applying. With no_limit true, there is no
            stop: the highest step applies until the reset day.
          properties:
            monthly_budget_minor:
              type: integer
              minimum: 1
              description: >-
                Spending cap in minor currency units (e.g., 50000 = EUR 500.00).
                Maximum 100,000,000,000. Required when you send `employee`
                unless `no_limit` is true. Left out of responses when `no_limit`
                is true, because the card has no limit. Do not send it together
                with `no_limit` true. To switch a card from no limit to a limit,
                send it in the same request.
              example: 50000
              maximum: 100000000000
            no_limit:
              type: boolean
              default: false
              description: >-
                No monthly limit: the discount does not stop at a cap, and the
                highest step applies until the reset day. With no_limit true,
                the highest step must be below 100 % (else 422
                `validation_failed` on `mechanics.tiers`). Omitted on create =
                false. Omitted on PATCH keeps the stored value, except that
                sending a positive `monthly_budget_minor` without `no_limit`
                switches the limit back on. Sending `employee` with neither a
                limit nor `no_limit` true answers 422 `parameter_missing` on
                `mechanics.employee.monthly_budget_minor`. Sending `no_limit`
                true together with `monthly_budget_minor` answers 422
                `validation_failed` on the same parameter.
              example: false
            reset_day:
              type: integer
              minimum: 1
              maximum: 28
              default: 1
              description: >-
                EMPLOYEE DISCOUNT ONLY. Day of month when the monthly budget
                resets (1-28). Default 1 (first of month). The month counter
                restarts on this day.
            budget_counts_as:
              type: string
              enum:
                - grossSpend
                - netSpend
                - discountValueGiven
              default: grossSpend
              description: >-
                What counts against the budget. 'grossSpend' = full purchase
                amount. 'netSpend' = amount after discount. 'discountValueGiven'
                = only the discount value given. With netSpend every discount
                step must be below 100 percent, otherwise 422 validation_failed
                on mechanics.employee.budget_counts_as (or on mechanics.tiers
                when only the tiers are sent).
            hide_budget_on_pass:
              type: boolean
              default: false
              description: >-
                Hide the remaining budget from the customer's wallet pass (some
                employers do not want to disclose spending limits).
          example:
            monthly_budget_minor: 50000
            reset_day: 1
            budget_counts_as: grossSpend
            hide_budget_on_pass: false
          type:
            - object
            - 'null'
        tier_demotion:
          description: >-
            Level retention, same engine as reward tiers: a customer who makes
            no qualifying transaction for inactivity_days drops one tier. Runs
            as a daily job; customers get a reminder 14 and 3 days before the
            drop. The date is on the customer as `level_valid_until`. Applies
            only to cards with 2 or more tiers; Booqme and Bookio cards are not
            affected. On an employee discount card (is_employee_discount=true)
            the field is ignored on write and reads back as null. Default off.
          properties:
            enabled:
              type: boolean
              default: false
              description: >-
                Level retention switch. Omitted on create = false; omitted on
                PATCH keeps the stored value. Switching it on, or changing
                inactivity_days while it is on, starts one fresh window for
                every customer from that moment. Switching it off keeps every
                customer on the current tier.
            inactivity_days:
              type: integer
              minimum: 1
              maximum: 1095
              default: 365
              description: >-
                Number of days without a qualifying transaction after which the
                customer drops one tier. Range 1-1095 (up to ~3 years). Omitted
                on PATCH keeps the stored window (365 on create).
          example:
            enabled: true
            inactivity_days: 365
          type:
            - object
            - 'null'
    CardMechanics_Coupon:
      type: object
      description: >-
        Mechanics for COUPON cards. One-time or reusable single-redemption
        coupons. Used for promo campaigns, birthday rewards, and re-engagement.
      required:
        - type
        - is_reusable
      properties:
        type:
          type: string
          const: coupon
          description: Discriminator. Always 'coupon' for this mechanics block.
        is_reusable:
          type: boolean
          default: false
          description: >-
            When true, the coupon card can be mass-reactivated via POST
            /v1/cards/{id}/reissue_coupons after customers redeem. When false,
            the coupon is single-use per customer and reissue_coupons refuses it
            with 422 coupon_not_reissuable.
          example: false
        reset_count:
          type: integer
          minimum: 0
          readOnly: true
          description: >-
            REUSABLE COUPONS ONLY. Number of times this coupon card has been
            reset (read-only).
          example: 0
        last_reset_at:
          readOnly: true
          description: >-
            Unix timestamp of the most recent coupon reset (read-only). Null if
            never reset.
          example: null
          type:
            - integer
            - 'null'
        reward_value:
          description: >-
            A coupon needs reward_value or a linked_card_template. With neither,
            422 validation_failed on mechanics.reward_value. Freeform reward
            text the customer redeems (e.g., '33% off', '1+1 Drink', 'Free
            coffee', '€5 off any purchase'). Rendered on the wallet pass and on
            the scanner preview at redeem time. Single string OR per-language
            object.
          anyOf:
            - type: string
              title: Single string (all languages)
              maxLength: 200
              example: 33% off
            - type: object
              title: Per-language object
              additionalProperties:
                type: string
                maxLength: 200
              minProperties: 1
              example:
                sk: 33% ZĽAVA
                en: 33% off
                cs: 33% sleva
                de: 33% Rabatt
              properties: {}
        linked_card_template:
          description: >-
            OPTIONAL upgrade-on-redeem card. When the customer redeems this
            coupon, the server ATOMICALLY transforms the customer's card record
            into a NEW card of the type referenced here. Used to retain
            customers after a one-shot conversion event (e.g., welcome coupon →
            stamp card).


            Value: 'without' (or null) = no transform. Otherwise a card_id
            (crd_*) referencing the upgrade target. A card that does not exist,
            or belongs to another organization, answers 422
            `linked_card_not_found`.


            FOR COUPON: only available when is_reusable=false (single-use
            coupons). Reusable + linked are mutually exclusive - wizard hides
            the selector when is_reusable=true and the server returns
            `reusable_coupon_cannot_link` if you try via API. The transform is
            done by VEXiON.


            **TARGET-TYPE ALLOWLIST**: the linked card MUST be of type Stamp /
            Reward / Discount / Membership. Coupon→Coupon and any *→GiftCard
            transforms are rejected server-side (`unsupported_target_type`). The
            wizard surfaces a filtered card picker; the API enforces the same
            constraint.
          example: null
          type:
            - string
            - 'null'
    CardMechanics_Membership:
      type: object
      description: >-
        Mechanics for MEMBERSHIP cards (gym passes, club memberships, season
        tickets). Supports multiple duration options, validity schedules, and
        daily usage caps.
      required:
        - type
        - membership_plans
      properties:
        type:
          type: string
          const: membership
          description: Discriminator. Always 'membership' for this mechanics block.
        validity_schedule:
          type: string
          enum:
            - everyday
            - specific
          default: everyday
          description: >-
            When the membership is usable. 'everyday' = any day, any time
            (subject to valid_times). 'specific' = only the days listed in
            specific_days[].
          example: everyday
        specific_days:
          type: array
          description: >-
            SPECIFIC SCHEDULE ONLY. Per-day validity windows. day uses ISO
            0=Sunday, 6=Saturday.
          items:
            type: object
            properties:
              day:
                type: integer
                minimum: 0
                maximum: 6
                description: Day of week. 0=Sunday, 1=Monday, ..., 6=Saturday.
              all_day:
                type: boolean
                description: When true, valid the entire day (ignores start_time/end_time).
              start_time:
                type: string
                pattern: ^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$
                description: HH:MM 24h format start time.
                example: '06:00'
              end_time:
                type: string
                pattern: ^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$
                description: HH:MM 24h format end time.
                example: '22:00'
          example:
            - day: 1
              all_day: false
              start_time: '06:00'
              end_time: '22:00'
            - day: 2
              all_day: true
        valid_times:
          type: object
          description: >-
            Daily time window applied to all valid days (overridden by
            specific_days entries).
          properties:
            start:
              type: string
              pattern: ^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$
              default: '00:00'
              description: Earliest usable time each day.
            end:
              type: string
              pattern: ^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$
              default: '23:59'
              description: Latest usable time each day.
          example:
            start: '06:00'
            end: '22:00'
        daily_usage_limit:
          type: object
          description: Cap on how many times the membership can be used per calendar day.
          properties:
            enabled:
              type: boolean
              default: false
              description: When true, the cap is enforced. When false, unlimited daily use.
            limit:
              type: integer
              minimum: 1
              maximum: 99
              default: 1
              description: Max activations per calendar day.
          example:
            enabled: true
            limit: 1
        membership_plans:
          type: array
          minItems: 1
          description: >-
            List of membership plans customers can purchase (e.g., 'Monthly
            Premium €30', '3-month Standard €60', 'Annual Premium €200'). Each
            plan has duration, durationType, displayName, and price. Customer
            picks one plan at activation time via mechanics
            initial_state.duration_id.
          items:
            type: object
            required:
              - id
              - duration
              - duration_type
              - display_name
              - price_minor
            properties:
              id:
                type: string
                description: >-
                  Stable identifier for this duration option. Use in customer
                  initial_state.duration_id. GET always returns the server's own
                  opaque mpl_ id for a persisted plan; an id you invent at
                  create time is accepted but only becomes this identity once
                  you round-trip the real id back on a later PATCH.
                example: mpl_c4zdlocdwajf6wtmnvvkonv4oa
              duration:
                type: integer
                minimum: 1
                description: Length in units of duration_type.
              duration_type:
                type: string
                enum:
                  - days
                  - weeks
                  - months
                  - years
                description: Unit of duration.
              display_name:
                type: string
                maxLength: 100
                description: Human-readable label (e.g., 'Monthly Unlimited').
                example: Monthly Unlimited
              price_minor:
                type: integer
                minimum: 0
                description: >-
                  Price in minor currency units (e.g., 4900 = EUR 49.00).
                  Maximum 100,000,000,000.
                example: 4900
                maximum: 100000000000
          example:
            - id: mpl_dmpvemj7nhjqi7wvi757ymxuu4
              duration: 1
              duration_type: months
              display_name: Monthly Unlimited
              price_minor: 4900
            - id: mpl_ma63prjwtyni6bqpkojwc7j6gu
              duration: 1
              duration_type: years
              display_name: Annual Premium
              price_minor: 49000
    CardMechanics_GiftCard:
      type: object
      description: >-
        Mechanics for GIFT CARD cards. Supports preset values with optional
        bonus tiers, partial use, expiration, employee monthly-credit mode, and
        online sales.
      required:
        - type
        - expiration_months
      properties:
        type:
          type: string
          const: gift_card
          description: Discriminator. Always 'gift_card' for this mechanics block.
        preset_values:
          type: array
          description: >-
            Predefined purchase amounts shown to the buyer (e.g., EUR 25 / EUR
            50 / EUR 100). Each entry can include a bonus that gets added on
            top. Empty array = free amount entry only.
          items:
            type: object
            required:
              - amount_minor
            properties:
              amount_minor:
                type: integer
                minimum: 1
                description: >-
                  Purchase amount the buyer pays, in minor units (e.g., 5000 =
                  EUR 50.00). Maximum 100,000,000,000.
                example: 5000
                maximum: 100000000000
              bonus_minor:
                type: integer
                minimum: 0
                default: 0
                description: >-
                  Bonus added to the gift card balance on top of amount_minor.
                  Requires bonus_enabled=true. E.g., buy EUR 50 get EUR 5 extra
                  -> bonus_minor: 500. Maximum 100,000,000,000.
                example: 500
                maximum: 100000000000
              total_minor:
                type: integer
                minimum: 1
                description: >-
                  Resulting gift card balance (amount_minor + bonus_minor).
                  Server-computed; provided here for clarity. Maximum
                  100,000,000,000.
                example: 5500
                maximum: 100000000000
          example:
            - amount_minor: 2500
              bonus_minor: 0
              total_minor: 2500
            - amount_minor: 5000
              bonus_minor: 500
              total_minor: 5500
            - amount_minor: 10000
              bonus_minor: 1500
              total_minor: 11500
        bonus_enabled:
          type: boolean
          default: false
          description: >-
            When true, preset_values[].bonus_minor is honored. When false, bonus
            amounts are ignored.
          example: true
        expiration_months:
          type: integer
          minimum: 1
          default: 12
          description: >-
            Default expiration window in months from purchase date. Default: the
            organization's default expiry (24 months unless changed), then 12.
            Per-customer override available via initial_state.expires_at on POST
            /v1/customers.
          example: 12
        is_employee_card:
          type: boolean
          default: false
          description: >-
            Employee gift card mode. When true, the card is treated as a
            company-issued spending allowance rather than a regular gift.
            Activates employee_card config.
          example: false
        employee_card:
          description: >-
            EMPLOYEE GIFT CARD MODE ONLY. Monthly auto-refill configuration.
            Each refill_day the card balance is topped up to
            monthly_credit_minor.
          properties:
            enabled:
              type: boolean
              default: true
              description: When true, the monthly refill cron runs for this card.
            monthly_credit_minor:
              type: integer
              minimum: 1
              default: 20000
              description: >-
                Credit added each refill cycle, in minor units (e.g., 20000 =
                EUR 200.00). Maximum 100,000,000,000.
              example: 20000
              maximum: 100000000000
            refill_day:
              type: integer
              minimum: 1
              maximum: 28
              default: 1
              description: >-
                Day of month when the refill runs (1-28 to avoid month-length
                issues).
            hide_balance_on_pass:
              type: boolean
              default: true
              description: >-
                When true, the wallet pass renders an 'Employee Card' badge
                instead of the numeric balance (employer does not want to expose
                the spending limit to staff).
          example:
            enabled: true
            monthly_credit_minor: 20000
            refill_day: 1
            hide_balance_on_pass: true
          type:
            - object
            - 'null'
        allow_online_sales:
          type: boolean
          default: false
          description: >-
            When true, this gift card is listed on the merchant's public
            bio-link aggregator (e.g., links.vexioncards.one/<org>) for direct
            online purchase.
          example: false
        online_sale_location_id:
          pattern: ^loc_[A-Za-z0-9]+$
          description: >-
            Pinned location for online gift card sales attribution (when
            allow_online_sales=true). All online purchases of this gift card are
            attributed to this location for revenue reporting and POS
            reconciliation. Does NOT restrict the card to a single physical
            location - customers can REDEEM at any location where the card is
            enabled. When null, online sales fall back to the organization
            default location.
          example: null
          type:
            - string
            - 'null'
        linked_card_template:
          description: >-
            OPTIONAL upgrade-on-depletion card. When the customer's gift card
            balance hits 0 (via a gift_card.spend transaction), the server
            ATOMICALLY transforms the customer's card record into a NEW card of
            the type referenced here. Used to retain customers after they finish
            spending a gift card (e.g., gift card → reward card).


            Value: 'without' (or null) = no transform. Otherwise a card_id
            (crd_*) referencing the upgrade target. A card that does not exist,
            or belongs to another organization, answers 422
            `linked_card_not_found`.


            FOR POS-DRIVEN GIFT CARDS: when the depletion arrives from the
            connected POS, the POS has already deducted the balance server-side.
            The transform service skips the FE-only 'drain' step in that case
            (alreadyDrained:true) to avoid double-deducting. The transform is
            done by VEXiON.


            **TARGET-TYPE ALLOWLIST**: the linked card MUST be of type Stamp /
            Reward / Discount / Membership. Coupon→Coupon and any *→GiftCard
            transforms are rejected server-side (`unsupported_target_type`). The
            wizard surfaces a filtered card picker; the API enforces the same
            constraint.
          example: null
          type:
            - string
            - 'null'
    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).
    PassFieldSlot:
      type: object
      description: >-
        One front-of-pass slot. The platform renders it on BOTH wallets from the
        same definition - Apple as a native pass field (typed formatting: dates,
        numbers, and currency render in the device locale automatically), Google
        as the matching text module (the platform pre-formats typed values
        server-side).
      required:
        - label
        - value_template
      properties:
        label:
          anyOf:
            - type: string
              title: Single label (all languages)
              maxLength: 40
            - type: object
              title: Per-language labels
              additionalProperties:
                type: string
                maxLength: 40
              minProperties: 1
          description: >-
            Small caption above the value. Keep short - wallet UIs truncate
            aggressively (~20 visible chars).
        value_template:
          type: string
          minLength: 1
          maxLength: 200
          description: >-
            Static text or `{{variables}}`: any `{{custom.<key>}}` (typed
            rendering follows the field's type), identity variables
            (`{{first_name}}`, ...), and the card-type mechanics variables. A
            slot whose variables all resolve empty renders as an empty slot.
          example: '{{custom.department}}'
        change_message:
          anyOf:
            - type: string
              title: Single template
              maxLength: 200
            - type: object
              title: Per-language templates
              additionalProperties:
                type: string
                maxLength: 200
              minProperties: 1
            - type: 'null'
              title: Silent (no banner)
          description: >-
            Apple Wallet lock-screen banner fired when this slot's rendered
            value changes - `{{value}}` substitutes the fresh value. Null
            (default) = silent. Apple only (Google caps visible messages at
            3/day, reserved for broadcasts). Fires ONLY on value change -
            broadcasts cannot trigger it.
    PassBackField:
      type: object
      description: >-
        One back-of-pass row. `key` is YOUR stable handle for addressing the row
        across updates (the platform derives the wallet-level identifiers from
        it).
      required:
        - key
        - label
        - value_template
      properties:
        key:
          type: string
          pattern: ^[a-z0-9_]{1,40}$
          description: >-
            Stable row identifier, unique within the card - lets you update one
            row without touching the rest.
          example: opening_hours
        label:
          anyOf:
            - type: string
              title: Single label (all languages)
              maxLength: 40
            - type: object
              title: Per-language labels
              additionalProperties:
                type: string
                maxLength: 40
              minProperties: 1
          description: >-
            Small caption above the value. Keep short - wallet UIs truncate
            aggressively (~20 visible chars).
        value_template:
          type: string
          minLength: 1
          maxLength: 2000
          description: >-
            Static text or `{{variables}}` - back rows fit long content (URLs
            and phone numbers become tappable on Apple).
          example: Mon-Fri 8:00-18:00
        change_message:
          anyOf:
            - type: string
              title: Single template
              maxLength: 200
            - type: object
              title: Per-language templates
              additionalProperties:
                type: string
                maxLength: 200
              minProperties: 1
            - type: 'null'
              title: Silent (no banner)
          description: >-
            Apple Wallet lock-screen banner fired when this slot's rendered
            value changes - `{{value}}` substitutes the fresh value. Null
            (default) = silent. Apple only (Google caps visible messages at
            3/day, reserved for broadcasts). Fires ONLY on value change -
            broadcasts cannot trigger it.
    CardTier_Reward:
      type: object
      description: >-
        Reward tier definition. Used when
        CardMechanics_Reward.earn_mode='tiered'. Customer's current tier is
        computed as the highest tier where their qualifying spend >=
        spend_to_achieve_minor (or, for a Booqme/Bookio-linked card, their
        cumulative visit count >= visits_to_achieve - see both fields' own
        descriptions; a tier carries exactly one of the two). Tier scales the
        base earn_rate via earn_multiplier.
      required:
        - name
        - earn_multiplier
      properties:
        id:
          type: string
          description: >-
            Stable identifier for this tier (auto-generated if omitted). GET
            always returns the server's own opaque tir_ id for a persisted tier;
            an id you invent at create time is accepted but only becomes this
            identity once you round-trip the real id back on a later PATCH.
          example: tir_c4zdlocdwajf6wtmnvvkonv4oa
        name:
          description: >-
            Display name of the tier shown on the wallet pass and notifications.
            Single string OR per-language object.
          anyOf:
            - type: string
              title: Single name (all languages)
              maxLength: 100
              example: Silver
            - type: object
              title: Per-language names
              additionalProperties:
                type: string
                maxLength: 100
              minProperties: 1
              example:
                sk: Strieborný
                cs: Stříbrný
                en: Silver
                de: Silber
              properties: {}
        earn_multiplier:
          type: number
          minimum: 1
          description: >-
            Multiplier applied to base earn rate at this tier (e.g., 1.0 = no
            bonus, 1.5 = 1.5x points, 2.0 = double points). Final points =
            mechanics.earn_rate_per_unit × spend × earn_multiplier. Capped at
            1,000 to prevent overflow/absurd point calculations.
          example: 1.5
          maximum: 1000
        background_image_urls:
          description: >-
            Per-language background image URLs shown when the customer reaches
            this tier. Keys: language codes (sk, cs, en, de) - must be a subset
            of the card's languages_supported (defaulting to
            [default_language]), or the server returns 422 parameter_invalid
            naming the offending key and the allowed set. OVERRIDES card-level
            backgrounds for customers at this tier. Same STRIP asset contract as
            the other backdrop fields: https only, PNG only (magic-byte
            verified), ≤ 3MB, matches the pass strip aspect ratio (~2.61:1, i.e.
            527:202) within 5% tolerance, width between 527px and 4000px - 422
            invalid_image_url / image_format_invalid / image_too_large /
            image_aspect_ratio_invalid / image_resolution_too_small /
            image_resolution_too_large on violation. VEXiON stores a copy of the
            image. Reads return the VEXiON copy URL, not the URL you sent. A URL
            that VEXiON returned earlier (for example one you got back from this
            API, or a library icon) is kept as is. Each language variant is
            mirrored to its OWN object, independently.
          additionalProperties:
            type: string
            format: uri
            description: >-
              HTTPS URL to the tier background image. Recommended 1125x432px.
              Must match the pass strip aspect ratio (~2.61:1) within 5%
              tolerance and be at least 527px wide; 422
              image_aspect_ratio_invalid otherwise.
          example:
            sk: >-
              https://stamp-icons.s3.eu-central-1.amazonaws.com/samples/backdrop-527x202.png?v=2026-09-17
            en: >-
              https://stamp-icons.s3.eu-central-1.amazonaws.com/samples/backdrop-527x202.png?v=2026-09-17
          properties: {}
          type:
            - object
            - 'null'
        spend_to_achieve_minor:
          type:
            - integer
            - 'null'
          minimum: 0
          description: >-
            Qualifying spend (purchases that earned points, minus reversals; it
            restarts at the lower level's threshold after a retention drop), in
            minor units, that unlocks this tier - the MONEY-gated form. Set on
            every reward tier EXCEPT a Booqme/Bookio-linked card's tiers, which
            are VISIT-gated instead (see visits_to_achieve). Exactly one of
            spend_to_achieve_minor / visits_to_achieve is present per tier,
            never both - which one applies is derived from the card's own POS
            integration linkage, not chosen by the request. Supplying the field
            that does not match the card's mode returns 422.
          example: 500
        visits_to_achieve:
          type:
            - integer
            - 'null'
          minimum: 0
          description: >-
            Cumulative VISIT COUNT that unlocks this tier - the visit-gated
            form, set ONLY on a Booqme- or Bookio-linked reward card's tiers (a
            POS-integration-only linkage; a card created directly through this
            API can never be visit-gated, so writing this field is not possible
            via POST /v1/cards today). Every other reward card uses
            spend_to_achieve_minor instead. Exactly one of the two is present
            per tier, never both.
          example: null
    CardTier_Discount:
      type: object
      description: >-
        Discount tier definition. Customer's current tier is computed as the
        highest tier where their cumulative spend >= spend_to_achieve_minor.
        Discount tiers are always money-gated - there is no visit-based discount
        card.
      required:
        - name
        - spend_to_achieve_minor
        - percentage
      properties:
        id:
          type: string
          description: >-
            Stable identifier for this tier (auto-generated if omitted, used to
            reference the tier in mechanics initial_state.tier_id). GET always
            returns the server's own opaque tir_ id for a persisted tier; an id
            you invent at create time is accepted but only becomes this identity
            once you round-trip the real id back on a later PATCH.
          example: tir_c4zdlocdwajf6wtmnvvkonv4oa
        name:
          description: >-
            Display name of the tier shown on the wallet pass and notifications.
            Single string OR per-language object.
          anyOf:
            - type: string
              title: Single name (all languages)
              maxLength: 100
              example: Silver
            - type: object
              title: Per-language names
              additionalProperties:
                type: string
                maxLength: 100
              minProperties: 1
              example:
                sk: Strieborný
                cs: Stříbrný
                en: Silver
                de: Silber
              properties: {}
        spend_to_achieve_minor:
          type: integer
          minimum: 0
          description: >-
            Cumulative qualifying spend (minor units) at which this tier
            activates. Tiers auto-promote as spend crosses thresholds. When
            `tier_demotion` is enabled and the customer drops a tier, qualifying
            spend restarts at the lower tier's threshold. Maximum
            100,000,000,000.
          example: 50000
          maximum: 100000000000
        percentage:
          type: number
          minimum: 0
          maximum: 100
          description: >-
            Discount percentage applied at POS when the customer is in this tier
            (e.g., 10 = 10% off every purchase).
          example: 10
        background_image_urls:
          description: >-
            Per-language background image URLs shown when the customer reaches
            this tier. Keys: language codes (sk, cs, en, de) - must be a subset
            of the card's languages_supported (defaulting to
            [default_language]), or the server returns 422 parameter_invalid
            naming the offending key and the allowed set. OVERRIDES card-level
            backgrounds for customers at this tier. Same STRIP asset contract as
            the other backdrop fields: https only, PNG only (magic-byte
            verified), ≤ 3MB, matches the pass strip aspect ratio (~2.61:1, i.e.
            527:202) within 5% tolerance, width between 527px and 4000px - 422
            invalid_image_url / image_format_invalid / image_too_large /
            image_aspect_ratio_invalid / image_resolution_too_small /
            image_resolution_too_large on violation. VEXiON stores a copy of the
            image. Reads return the VEXiON copy URL, not the URL you sent. A URL
            that VEXiON returned earlier (for example one you got back from this
            API, or a library icon) is kept as is. Each language variant is
            mirrored to its OWN object, independently.
          additionalProperties:
            type: string
            format: uri
            description: >-
              HTTPS URL to the tier background image. Recommended 1125x432px.
              Must match the pass strip aspect ratio (~2.61:1) within 5%
              tolerance and be at least 527px wide; 422
              image_aspect_ratio_invalid otherwise.
          example:
            sk: >-
              https://stamp-icons.s3.eu-central-1.amazonaws.com/samples/backdrop-527x202.png?v=2026-09-17
            en: >-
              https://stamp-icons.s3.eu-central-1.amazonaws.com/samples/backdrop-527x202.png?v=2026-09-17
          properties: {}
          type:
            - object
            - 'null'
  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.