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

# Update a customer

> Updates a customer record. Only fields present in the body are changed.

## What can be updated

- **Identity** - `first_name`, `last_name`, `email`, `phone`, `language`, `external_id`
- **Custom fields** (`custom.*`) - validated against the card's `form_builder` schema
- **Metadata** (`metadata.*`) - free-form key/value bag

## What is READ-ONLY

Balances (stamps, points, tier, gift card balance) are computed from the transaction ledger and cannot be patched. To adjust a balance, create a transaction with `action` set to `award` or `redeem` and `value` through `POST /v1/transactions`. A balance cannot be written directly.

## Notification

**Every** successful PATCH bumps `passVersion` and the pass content timestamp, not only the fields that are visible on the pass. Patching `external_id` or `metadata` alone still bumps them, which is harmless but means the version is not a reliable signal that anything the customer can see has changed.

**No push is sent from here.** The pass is NOT eagerly regenerated and no Apple silent push is fired on this endpoint: that path is certificate-gated and can report a false 404 even after a successful save. Google Wallet reflects the change on its next server-side object sync. So treat a 200 as "stored", not as "delivered to the device".



## OpenAPI

````yaml /openapi.json patch /v1/customers/{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/customers/{id}:
    patch:
      tags:
        - Customers
      summary: Update a customer
      description: >-
        Updates a customer record. Only fields present in the body are changed.


        ## What can be updated


        - **Identity** - `first_name`, `last_name`, `email`, `phone`,
        `language`, `external_id`

        - **Custom fields** (`custom.*`) - validated against the card's
        `form_builder` schema

        - **Metadata** (`metadata.*`) - free-form key/value bag


        ## What is READ-ONLY


        Balances (stamps, points, tier, gift card balance) are computed from the
        transaction ledger and cannot be patched. To adjust a balance, create a
        transaction with `action` set to `award` or `redeem` and `value` through
        `POST /v1/transactions`. A balance cannot be written directly.


        ## Notification


        **Every** successful PATCH bumps `passVersion` and the pass content
        timestamp, not only the fields that are visible on the pass. Patching
        `external_id` or `metadata` alone still bumps them, which is harmless
        but means the version is not a reliable signal that anything the
        customer can see has changed.


        **No push is sent from here.** The pass is NOT eagerly regenerated and
        no Apple silent push is fired on this endpoint: that path is
        certificate-gated and can report a false 404 even after a successful
        save. Google Wallet reflects the change on its next server-side object
        sync. So treat a 200 as "stored", not as "delivered to the device".
      operationId: updateCustomer
      parameters:
        - name: id
          in: path
          description: Customer id with cus_ prefix.
          required: true
          example: cus_a4f72c91e0bd4185b6c2
          schema:
            type: string
            pattern: ^cus_[A-Za-z0-9]+$
        - name: Idempotency-Key
          in: header
          description: Recommended on PATCH and DELETE for safe retries.
          required: false
          example: 1c4f7d2e-9b3a-4f1e-8b7c-7e2a91cdef34
          schema:
            type: string
            maxLength: 255
            format: uuid
            minLength: 8
            pattern: ^[A-Za-z0-9_-]{8,255}$
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerPatchRequest'
            examples:
              Set marketing opt in:
                summary: Set marketing opt in
                value:
                  marketing_opt_in: true
              Change language:
                summary: Change language
                value:
                  language: de
              Update phone:
                summary: Update phone
                value:
                  phone: '+421911999888'
              Merge custom fields:
                summary: Merge custom fields
                value:
                  custom:
                    pet_name: Maxim
                    favourite_treat: carrot stick
              Clear one custom value:
                summary: Clear one custom value
                value:
                  custom:
                    favourite_treat: null
              Clear top level field:
                summary: Clear top level field
                value:
                  date_of_birth: null
        required: true
      responses:
        '200':
          description: Updated customer.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
              example:
                id: cus_a4f72c91e0bd4185b6c2
                object: customer
                external_id: crm-customer-7421
                email: thomas.wright@example.com
                phone: null
                first_name: Tomáš
                last_name: Varga
                date_of_birth: '1991-04-12'
                language: de
                marketing_opt_in: true
                gdpr_redacted_at: null
                card_id: crd_2hh2fekk5zazfyyvian4uhepyq
                card_type: reward
                card:
                  card_id: crd_2hh2fekk5zazfyyvian4uhepyq
                  card_type: reward
                  status: installed
                  total_visits: 12
                  first_transaction_at: 1779580800
                  last_transaction_at: 1782345600
                  created_at: 1779494400
                  updated_at: 1782345600
                  points: 480
                  points_pending: 0
                  reward_tier:
                    tier_name: Silver
                    earn_multiplier: 1.5
                    tier_effective_date: 1781481600
                    previous_tier_name: Bronze
                signup_location_id: loc_ibjeubrxvvruf6aq5jfnhrz6fa
                status: installed
                device: apple_wallet
                installed_at: 1779494400
                claim_url: https://vexioncards.one/card/add/afbd9cbc8b9ab72f43bf6ffc
                custom: {}
                metadata:
                  crm_secondary_id: marketing_456
                referral_stats: null
                created_at: 1779494400
                updated_at: 1782691200
                last_visited_at: 1782345600
                last_visited_location_id: loc_ibjeubrxvvruf6aq5jfnhrz6fa
                deleted_at: null
              examples:
                Updated:
                  summary: Updated
                  value:
                    id: cus_yovo5wk634htfojgkzdpmzvu5a
                    object: customer
                    external_id: crm-customer-7421
                    email: thomas.wright@example.com
                    phone: null
                    first_name: Tomáš
                    last_name: Varga
                    date_of_birth: '1991-04-12'
                    language: de
                    marketing_opt_in: true
                    gdpr_redacted_at: null
                    card_id: crd_2hh2fekk5zazfyyvian4uhepyq
                    card_type: reward
                    card:
                      card_id: crd_2hh2fekk5zazfyyvian4uhepyq
                      card_type: reward
                      status: installed
                      total_visits: 12
                      first_transaction_at: 1779580800
                      last_transaction_at: 1782345600
                      created_at: 1779494400
                      updated_at: 1782345600
                      points: 480
                      points_pending: 0
                      reward_tier:
                        tier_name: Silver
                        earn_multiplier: 1.5
                        tier_effective_date: 1781481600
                        previous_tier_name: Bronze
                    signup_location_id: loc_ibjeubrxvvruf6aq5jfnhrz6fa
                    status: installed
                    device: apple_wallet
                    installed_at: 1779494400
                    claim_url: https://vexioncards.one/card/add/afbd9cbc8b9ab72f43bf6ffc
                    custom: {}
                    metadata:
                      crm_secondary_id: marketing_456
                    referral_stats: null
                    created_at: 1779494400
                    updated_at: 1782691200
                    last_visited_at: 1782345600
                    last_visited_location_id: loc_ibjeubrxvvruf6aq5jfnhrz6fa
                    deleted_at: null
          headers: {}
        '400':
          description: >-
            Malformed request body (unparseable JSON, oversized payload, or a
            bad Idempotency-Key header). A field that is unknown, non-settable
            or outside its enum is a 422, not a 400.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          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_01jBhZHND4E5F6G7H8J9K0L1M2
              examples:
                No API key:
                  summary: No API key
                  value:
                    error:
                      type: authentication_error
                      code: missing_api_key
                      message: >-
                        Authorization header is required. Send 'Authorization:
                        Bearer vxn_sk_...'.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#missing_api_key
                      request_id: req_01jBhZHND4E5F6G7H8J9K0L1M2
                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_a1t6YF1XB9Qf1CJGvCsajezgqs
                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_5kShy0GBZkY4PBbTNdZazeYT9h
          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 customers:write scope.
                  doc_url: >-
                    https://vexioncards.mintlify.site/api-reference/errors#insufficient_scope
                  request_id: req_01jBhZHYC5F6G7H8J9K0L1M2N3
              examples:
                Insufficient scope:
                  summary: Insufficient scope
                  value:
                    error:
                      type: permission_error
                      code: insufficient_scope
                      message: This API key does not have the customers:write scope.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#insufficient_scope
                      request_id: req_01jBhZHYC5F6G7H8J9K0L1M2N3
          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: >-
                    Customer 'cus_does_not_exist' was not found in this
                    organization.
                  param: id
                  doc_url: >-
                    https://vexioncards.mintlify.site/api-reference/errors#resource_missing
                  request_id: req_01jBhZJ9D6G7H8J9K0L1M2N3P4
              examples:
                Customer not found:
                  summary: Customer not found
                  value:
                    error:
                      type: not_found_error
                      code: resource_missing
                      message: >-
                        Customer 'cus_does_not_exist' was not found in this
                        organization.
                      param: id
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#resource_missing
                      request_id: req_01jBhZJ9D6G7H8J9K0L1M2N3P4
          headers: {}
        '409':
          description: >-
            Idempotency-key reuse, or `external_id_conflict` (another customer
            on this card already has that `external_id`). A `customer_erased`
            conflict is terminal: once GDPR erasure has run on a customer the
            record is a tombstone that keeps its transaction ledger but never
            accepts fresh PII again, and there is no un-erase flow.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  type: invalid_request_error
                  code: idempotency_key_in_use
                  message: >-
                    An earlier request with the same Idempotency-Key is still
                    processing. Retry in a few seconds.
                  doc_url: >-
                    https://vexioncards.mintlify.site/api-reference/errors#idempotency_key_in_use
                  request_id: req_3f1e9d7c5b2a4
              examples:
                Http 409 default:
                  summary: Http 409 default
                  value:
                    error:
                      type: invalid_request_error
                      code: idempotency_key_in_use
                      message: >-
                        An earlier request with the same Idempotency-Key is
                        still processing. Retry in a few seconds.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#idempotency_key_in_use
                      request_id: req_3f1e9d7c5b2a4
                Customer already erased (GDPR):
                  summary: Customer already erased (GDPR)
                  value:
                    error:
                      type: invalid_request_error
                      code: customer_erased
                      message: >-
                        This customer has been erased (GDPR
                        right-to-be-forgotten) and can no longer be modified.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#customer_erased
                      request_id: req_7d2e9a1c4b8f6
          headers: {}
        '413':
          description: >-
            Request body exceeds the 1 MB cap. The body was never parsed, so the
            request had no effect.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  type: invalid_request_error
                  code: request_too_large
                  message: The request body exceeds the maximum allowed size.
                  doc_url: >-
                    https://vexioncards.mintlify.site/api-reference/errors#request_too_large
                  request_id: req_5f8b2c4a9e1d3
        '422':
          description: Body parsed but semantically invalid. See field_errors[].
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                Unknown custom field:
                  summary: Unknown custom field
                  value:
                    error:
                      type: invalid_request_error
                      code: unknown_custom_field
                      message: >-
                        custom.favourite_color is not declared in the card's
                        form_builder. Add a CustomField definition first via
                        POST /v1/custom_fields and reference it in the card.
                      param: custom.favourite_color
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#unknown_custom_field
                      request_id: req_01jBhZJKE7H8J9K0L1M2N3P4Q5
                Invalid custom field type:
                  summary: Invalid custom field type
                  value:
                    error:
                      type: invalid_request_error
                      code: validation_failed
                      message: One or more custom fields failed validation.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#validation_failed
                      request_id: req_01jBhZJWF8J9K0L1M2N3P4Q5R6
                      field_errors:
                        - param: custom.pet_birthday
                          code: invalid_value
                          message: Invalid date
                Card ID is not settable on PATCH:
                  summary: card_id sent on PATCH
                  value:
                    error:
                      type: invalid_request_error
                      code: parameter_unknown
                      message: >-
                        Unknown or non-settable parameter: card_id. Remove it -
                        it is not accepted on this operation. To enrol the same
                        person on a different card, POST a new customer with the
                        target card_id.
                      param: card_id
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#parameter_unknown
                      request_id: req_01jBhZH1A2B3C4D5E6F7G8H9J0
                Invalid language:
                  summary: language outside the enum
                  value:
                    error:
                      type: invalid_request_error
                      code: validation_failed
                      message: One or more fields failed validation.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#validation_failed
                      request_id: req_01jBhZHCB3C4D5E6F7G8H9J0K1
                      field_errors:
                        - param: language
                          code: invalid_enum_value
                          message: 'language must be one of: sk, cs, en, de.'
          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. The request may be retried with
                    the same Idempotency-Key.
                  doc_url: >-
                    https://vexioncards.mintlify.site/api-reference/errors#internal_error
                  request_id: req_5f8b2c4a9e1d3
              examples:
                Http 500 default:
                  summary: Http 500 default
                  value:
                    error:
                      type: api_error
                      code: internal_error
                      message: >-
                        An internal error occurred. The request may be retried
                        with the same Idempotency-Key.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#internal_error
                      request_id: req_5f8b2c4a9e1d3
          headers: {}
      deprecated: false
components:
  schemas:
    CustomerPatchRequest:
      type: object
      description: Sparse update. card_id is immutable.
      properties:
        external_id:
          description: >-
            Update the customer's external reference (CRM id, internal account
            number, etc.). Send `null` to clear it. This is the same field POST
            /v1/customers uses as the highest-priority dedupe key on creation,
            but PATCH here only updates the stored value - it never re-triggers
            dedupe.
          example: crm-customer-7421
          type:
            - string
            - 'null'
        email:
          format: email
          description: >-
            Update the customer's email. Re-enables email delivery channel if
            previously blocked.
          maxLength: 255
          example: customer@example.com
          type:
            - string
            - 'null'
        phone:
          description: >-
            Contact phone. Accepted in any common written form -
            `+421908123456`, `+421 908 123 456`, `+421-908-123-456`,
            `00421908123456`, or a bare Slovak local number like `0908123456`.
            The platform canonicalises it to E.164 on write, and every read
            returns strict E.164 (`^\\+[1-9]\\d{1,14}$`) or null. A value that
            cannot be resolved without guessing a country code is rejected with
            `422 invalid_format` rather than stored and silently reported as
            null.
          pattern: ^\+?[0-9][0-9\s().\-]{6,24}$
          minLength: 8
          maxLength: 16
          example: '+14155552671'
          type:
            - string
            - 'null'
        first_name:
          description: >-
            Update the customer's given name. Pass updates via silent push
            within 2 seconds.
          type:
            - string
            - 'null'
          maxLength: 50
        last_name:
          description: >-
            Update the customer's family name. Pass updates via silent push
            within 2 seconds.
          type:
            - string
            - 'null'
          maxLength: 50
        date_of_birth:
          format: date
          description: Update date of birth (ISO 8601). Used for birthday automation.
          type:
            - string
            - 'null'
        language:
          type: string
          enum:
            - sk
            - cs
            - en
            - de
          description: >-
            Change preferred language. Pass content re-renders in the new
            language on next sync.
        marketing_opt_in:
          type: boolean
          description: >-
            Toggle the stored marketing-consent record. This changes the stored
            value only. VEXiON does not apply it when sending notifications: a
            notification sent through the API is not filtered by this flag or by
            the customer's marketing switch in the dashboard. To respect
            opt-outs on API sends, send only to customers with marketing_opt_in
            true, using explicit `customer_ids`.
        custom:
          type: object
          additionalProperties: true
          description: >-
            Merge into existing custom field values. Keys not provided remain
            unchanged. To clear a single key, set its value to null.
          properties: {}
        metadata:
          type:
            - object
            - 'null'
          description: >-
            Partner-internal `{key: value}` map (max 50 keys, key
            `^[a-zA-Z0-9_]{1,40}$`, string values ≤500 chars). NEVER rendered to
            the customer - for CRM ids, import batches, integration markers.
            Customer-facing typed data belongs in `custom` (validated against
            custom field definitions, renderable on passes/notifications).


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


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


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


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

````

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