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

# Create a custom field

> Creates a reusable field definition. Scope: `custom_fields:write`.

One definition serves the whole platform: reference it from any card's `form_builder` (signup form), read/write values via `Customer.custom.<key>`, render it on wallet passes (`{{custom.<key>}}` in `pass_fields`) and in notification templates.

- `key` is the stable machine handle (`^[a-z][a-z0-9_]{1,49}$`, unique per organization) - `409 key_already_exists` on collision
- `field_type` + `options` + `date_granularity` + `number_format` define the value contract (immutable once used)
- `position` / `label` translations are DEFAULTS - each card's form can override them
- Organization cap: 50 definitions (`409 custom_field_limit_reached`)



## OpenAPI

````yaml /openapi.json post /v1/custom_fields
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/custom_fields:
    post:
      tags:
        - Custom fields
      summary: Create a custom field
      description: >-
        Creates a reusable field definition. Scope: `custom_fields:write`.


        One definition serves the whole platform: reference it from any card's
        `form_builder` (signup form), read/write values via
        `Customer.custom.<key>`, render it on wallet passes (`{{custom.<key>}}`
        in `pass_fields`) and in notification templates.


        - `key` is the stable machine handle (`^[a-z][a-z0-9_]{1,49}$`, unique
        per organization) - `409 key_already_exists` on collision

        - `field_type` + `options` + `date_granularity` + `number_format` define
        the value contract (immutable once used)

        - `position` / `label` translations are DEFAULTS - each card's form can
        override them

        - Organization cap: 50 definitions (`409 custom_field_limit_reached`)
      operationId: createCustomField
      parameters:
        - name: Idempotency-Key
          in: header
          description: UUIDv4 recommended. Replays cached 24h.
          required: true
          example: 8d37b133-db25-412b-9ae3-4c84e142591c
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomFieldCreateRequest'
            examples:
              Select with translations:
                summary: Clinic department picker
                value:
                  key: department
                  field_type: select
                  options:
                    - value: cardio
                      label: Cardiology
                    - value: neuro
                      label: Neurology
                  label:
                    sk: Oddelenie
              Date with year granularity:
                summary: Stores YYYY only
                value:
                  key: first_visit_year
                  field_type: date
                  date_granularity: year
                  label: First visit year
              Multiselect:
                summary: Dietary preferences
                value:
                  key: dietary_prefs
                  field_type: multiselect
                  options:
                    - value: vegan
                      label: Vegan
                    - value: gluten_free
                      label: Gluten-free
                    - value: lactose_free
                      label: Lactose-free
                  label: Dietary preferences
              Free text:
                summary: Simple text field
                value:
                  key: patient_id
                  field_type: text
                  label: Patient identifier
        required: true
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomField'
              examples:
                Created:
                  summary: usage_count starts at 0
                  value:
                    id: cf_01HW9R9Z4EF7N8MTX1RVCFLD01
                    object: custom_field
                    key: department
                    description: Clinic department the patient is assigned to.
                    field_type: select
                    position: 2
                    options:
                      - value: cardio
                        label: Cardiology
                      - value: neuro
                        label: Neurology
                      - value: ortho
                        label: Orthopedics
                    date_granularity: date
                    usage_count: 0
                    cards_using: []
                    created_at: 1750000000
                    updated_at: 1750000000
                    label:
                      sk: Oddelenie
                      en: Department
          headers: {}
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers: {}
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers: {}
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers: {}
        '409':
          description: >-
            Conflict (duplicate label). A label must be unique among the
            organization's active definitions. Deleting a definition frees its
            label; choose a different label or delete the definition that holds
            it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          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: Unprocessable entity
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers: {}
        '429':
          description: Rate limited
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          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:
    CustomFieldCreateRequest:
      type: object
      description: >-
        Define a reusable custom field. Non-applicable config is normalized
        automatically (options with text → empty array; date_granularity with
        number → reset to `date`).
      required:
        - label
        - field_type
      properties:
        key:
          type: string
          pattern: ^[a-z][a-z0-9_]{1,49}$
          description: >-
            Org-unique machine identifier (optional - derived from the label
            when omitted). Immutable once the field is in use.
          example: department
        label:
          anyOf:
            - type: string
              title: Single label (all languages)
              minLength: 1
              maxLength: 50
            - type: object
              title: Per-language labels
              additionalProperties:
                type: string
                minLength: 1
                maxLength: 50
              minProperties: 1
              description: >-
                Keys are language codes (`sk`, `cs`, `en`, `de`, `hu`, `hr`,
                `ro`). On write this collapses immediately to a single stored
                string - `en` if present, otherwise the first language present -
                it is not resolved per customer language at read time. Real
                per-language display lives per CARD: see
                `Card.formBuilder.fields[].translations[]`.
          description: >-
            Customer-facing label - shown on the signup form, referenced on
            wallet passes, and used in the dashboard. Plain string or
            per-language object (the platform localization pattern, same as
            `Card.name`). This is the DEFAULT - each card's `form_builder` can
            override the label per card.


            **Uniqueness is among active definitions.** A label must be unique
            among the organization's active definitions (`409` on conflict).
            Deleting a definition frees its label for reuse.
        description:
          type: string
          maxLength: 500
          description: Optional dashboard helper text.
        field_type:
          type: string
          enum:
            - text
            - textarea
            - number
            - currency
            - date
            - boolean
            - select
            - multiselect
          description: >-
            Field type - see the type table on the Custom Field object.
            Immutable while in use.
          example: select
        position:
          type: integer
          description: Dashboard ordering hint.
        options:
          type: array
          items:
            type: object
          description: >-
            Required non-empty for select/multiselect. Ignored (forced empty)
            otherwise.
        date_granularity:
          type: string
          enum:
            - year
            - monthYear
            - date
          description: Date type only. Default `date`.
        number_format:
          type: string
          enum:
            - integer
            - decimal
          description: '`number` fields only. Default `decimal`.'
      example:
        key: department
        field_type: select
        options:
          - value: cardio
            label: Cardiology
          - value: neuro
            label: Neurology
        label:
          sk: Oddelenie
    CustomField:
      type: object
      description: >-
        A reusable custom field DEFINITION - the single vocabulary used across
        the whole platform: the signup form (via each card's `form_builder`),
        the customer record (`Customer.custom.<key>`), wallet pass slots
        (`{{custom.<key>}}` in `Card.pass_fields`), and notification templates.
        Define once, reference everywhere.


        The definition owns IDENTITY (key, type, options, granularity) and
        DEFAULTS (label translations, required, position); each card's form owns
        USAGE (which fields, per-card required/order/label overrides). Limits:
        50 definitions per organization (`409 custom_field_limit_reached`), 20
        fields per card form.
      required:
        - id
        - object
        - key
        - label
        - field_type
        - created_at
      properties:
        id:
          type: string
          pattern: ^cf_[A-Za-z0-9]+$
          description: Opaque, stable, immutable. `cf_` prefix.
          example: cf_01HW9R9Z4EF7N8MTX1RVCFLD01
        object:
          type: string
          const: custom_field
          description: Always `"custom_field"`.
        key:
          type: string
          pattern: ^[a-z][a-z0-9_]{1,49}$
          description: >-
            Stable machine identifier, unique within the organization (2-50
            chars, lowercase snake). Used in `{{custom.<key>}}` templates and
            Customer `custom.<key>` payloads. **Renames are blocked while the
            field is in use** (usage_count > 0) to prevent keys diverging across
            cards.
          example: department
        label:
          anyOf:
            - type: string
              title: Single label (all languages)
              minLength: 1
              maxLength: 200
            - type: object
              title: Per-language labels
              additionalProperties:
                type: string
                minLength: 1
                maxLength: 200
              minProperties: 1
              description: >-
                Keys are language codes (`sk`, `cs`, `en`, `de`, `hu`, `hr`,
                `ro`). On write this collapses immediately to a single stored
                string - `en` if present, otherwise the first language present -
                it is not resolved per customer language at read time. Real
                per-language display lives per CARD: see
                `Card.formBuilder.fields[].translations[]`.
          description: >-
            Customer-facing label - shown on the signup form, referenced on
            wallet passes, and used in the dashboard. Plain string or
            per-language object (the platform localization pattern, same as
            `Card.name`). This is the DEFAULT - each card's `form_builder` can
            override the label per card.
        description:
          type:
            - string
            - 'null'
          maxLength: 500
          description: Internal helper text for the dashboard.
          example: Clinic department the patient is assigned to.
        field_type:
          type: string
          enum:
            - text
            - textarea
            - number
            - currency
            - date
            - boolean
            - select
            - multiselect
          description: >-
            What kind of data the field holds - drives form input, validation,
            and typed pass/notification rendering:


            | type | value shape | pass rendering |

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

            | `text` | string ≤200 | as-is |

            | `textarea` | string ≤2000 | back of pass only |

            | `number` | number (`number_format` controls integer vs decimal) |
            locale-formatted number |

            | `currency` | integer, minor units, organization currency |
            locale-formatted amount (native on Apple via ISO 4217;
            server-formatted on Google) |

            | `date` | string per `date_granularity` (`YYYY` / `YYYY-MM` /
            `YYYY-MM-DD`) | locale-formatted date |

            | `boolean` | true / false | localized Yes/No |

            | `select` | one option `value` | the option's label, customer
            language |

            | `multiselect` | array of option `value`s | comma-joined labels |


            Immutable while the field is used by any card (`409
            custom_field_in_use`).
          example: select
        position:
          type: integer
          description: >-
            DEFAULT form position when added to a card; each card's
            `form_builder` ordering overrides.
          example: 2
        options:
          type: array
          items:
            type: object
            properties:
              value:
                type: string
                description: >-
                  Stored machine value - immutable once any customer has stored
                  it.
              label:
                anyOf:
                  - type: string
                    title: Single label (all languages)
                    minLength: 1
                    maxLength: 100
                  - type: object
                    title: Per-language labels
                    additionalProperties:
                      type: string
                      minLength: 1
                      maxLength: 100
                    minProperties: 1
                    description: >-
                      Keys are language codes (`sk`, `cs`, `en`, `de`, `hu`,
                      `hr`, `ro`). On write this collapses immediately to a
                      single stored string - `en` if present, otherwise the
                      first language present - it is not resolved per customer
                      language at read time. Real per-language display lives per
                      CARD: see `Card.formBuilder.fields[].translations[]`.
                description: >-
                  Display label for the option - localizable; shown on the form
                  and rendered on passes/notifications in the customer's
                  language. Always editable.
          description: >-
            Choices for `select` / `multiselect` (required for those types).
            Each option is `{value, label}` - **labels are always editable**
            (display only); **values are immutable once any customer has stored
            them** and removing a used value is blocked (`409
            custom_field_in_use`). Adding options is always allowed.
          example:
            - value: cardio
              label: Cardiology
            - value: neuro
              label: Neurology
        date_granularity:
          type: string
          enum:
            - year
            - monthYear
            - date
          description: >-
            DATE type only - controls the stored format: `date` → YYYY-MM-DD,
            `monthYear` → YYYY-MM, `year` → YYYY. Forced to `date` for non-date
            types.
          example: date
        usage_count:
          type: integer
          description: >-
            Read-only. Number of cards referencing this field. Renames + type
            changes + deletes are blocked while > 0.
          example: 3
        cards_using:
          type: array
          items:
            type: string
          description: Read-only. The referencing card ids.
          example:
            - crd_sophcrprtvpmmnwxah2lomnkee
        created_at:
          type: integer
          description: Unix seconds.
          example: 1750000000
        updated_at:
          type: integer
          description: Unix seconds.
          example: 1750000000
        number_format:
          type:
            - string
            - 'null'
          enum:
            - integer
            - decimal
            - null
          description: >-
            `number` fields only: `integer` rejects fractional input; `decimal`
            (default) allows it. Null on other types.
    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.
  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.