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

# Custom Cards

> The card type with no loyalty mechanics: model your own data with custom fields, write it per customer, and render it on the wallet pass.

A **custom card** is the blank-slate card type. It carries no loyalty mechanics at all: no stamps, no points, no discount tiers, no stored balance. You decide what it holds by defining custom fields, attaching them to the card's form, and writing a value for each field on every customer.

Use it when your data is not a loyalty balance. A member record, a patient record, a ticket, a service history, a white-label pass you drive from your own system.

## What a custom card is not

Three properties follow from having no mechanics. Plan around all three before you choose this type.

| Property | Consequence |
| - | - |
| `mechanics` is always `null` | Send `null` or omit the field on create. Sending a mechanics object returns `422 mechanics_type_mismatch` on `mechanics`. |
| There is no balance | Nothing accumulates. Every value on the card is one you wrote through the API. |
| `POST /v1/transactions` is refused | Posting a transaction against a custom card returns `422` [`card_type_not_writable`](https://vexioncards.mintlify.site/api-reference/errors#card_type_not_writable) with `param: "card_id"`. A custom card has no balance for a transaction to change. |

```json theme={null}
{
  "error": {
    "type": "invalid_request_error",
    "code": "card_type_not_writable",
    "message": "Transactions cannot be created for custom cards through the API yet.",
    "request_id": "req_3jo0RGUdF2Asaq3A7LBJgv",
    "param": "card_id",
    "doc_url": "https://vexioncards.mintlify.site/api-reference/errors#card_type_not_writable"
  }
}
```

Because nothing is derived from mechanics, `pass_fields` is **required** on a custom card. It is the only thing that decides what the pass shows. Omitting it returns `400` [`parameter_missing`](https://vexioncards.mintlify.site/api-reference/errors#parameter_missing) naming `pass_fields`.

> [!NOTE]
>
> Broadcasts reach custom cards like every other card type, so `POST /v1/notifications` works normally. See [Webhooks](https://vexioncards.mintlify.site/api-reference/webhooks.md) for the delivery events.

## The seven card types

| `type` | Created through | `mechanics` | `POST /v1/transactions` | Custom fields |
| - | - | - | - | - |
| `stamp` | API or dashboard | stamp block | Yes, `award` and `redeem` | Yes |
| `reward` | API or dashboard | reward block | Yes, `award` and `redeem` | Yes |
| `discount` | API or dashboard | discount block | `award` only. `redeem` returns `422 action_not_valid_for_card_type` | Yes |
| `coupon` | API or dashboard | coupon block | `redeem` only. `award` returns `422 action_not_valid_for_card_type` | Yes |
| `membership` | Dashboard only | membership block | No, `422 card_type_not_writable` | Definable, but no customer can be enrolled through the API |
| `gift_card` | Dashboard only | gift card block | No, `422 card_type_not_writable` | Definable, but no customer can be enrolled through the API |
| `custom` | API or dashboard | always `null` | No, `422 card_type_not_writable` | Yes, and they are the whole point of the type |

`POST /v1/cards` accepts five values for `type`: `stamp`, `reward`, `discount`, `coupon`, `custom`. `membership` and `gift_card` are dashboard-managed, so sending either fails schema validation as `422 validation_failed` on `type`.

`POST /v1/customers` enrolls on `stamp`, `reward`, `discount`, `coupon` and `custom`. On a `membership` or `gift_card` card it returns `422` [`unsupported_card_type`](https://vexioncards.mintlify.site/api-reference/errors#unsupported_card_type).

The dashboard names some variants as their own card. In the API each one is one of the seven types plus a setting:

| Dashboard card | API `type` | Setting in `mechanics` |
| - | - | - |
| Stamp | `stamp` | none |
| Prepaid stamp | `stamp` | `is_prepaid: true` (cannot be changed after creation) |
| Reward (points) | `reward` | `redemption_mode: "product"` |
| Cashback | `reward` | `redemption_mode: "cashback"`. `value` on a transaction is points, also on redeem |
| Discount | `discount` | `is_employee_discount: false` |
| Employee | `discount` | `is_employee_discount: true` |
| Coupon | `coupon` | none |
| Gift card | `gift_card` | dashboard only |
| Membership | `membership` | dashboard only |

A reward or cashback card has levels when `earn_mode` is `tiered`.

## How the pieces fit together

Custom fields live at three levels, and each level owns a different thing.

```mermaid theme={null}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E5F6FF','primaryBorderColor':'#36C6FA','primaryTextColor':'#0B3A4A','lineColor':'#2BA8D9','secondaryColor':'#F4FCFF','tertiaryColor':'#FFFFFF','fontFamily':'Manrope, ui-sans-serif, system-ui, sans-serif'}}}%%
flowchart LR
    A["POST /v1/custom_fields<br/>the definition (cf_...)<br/>type, options, format"] --> B["POST /v1/cards<br/>form_builder.fields[]<br/>type: custom + custom_field_id"]
    B --> C["POST /v1/customers<br/>a custom object of field_id to value<br/>the per-customer value"]
    B --> D["pass_fields<br/>a custom.field_id token<br/>what the pass shows"]
    C --> D

    classDef step fill:#E5F6FF,stroke:#36C6FA,color:#0B3A4A,font-weight:600;
    class A,B,C,D step;
```

| Level | Endpoint | Owns |
| - | - | - |
| Definition | `/v1/custom_fields` | The field type, its options, its number format, its date granularity. Organization-scoped and reusable. Maximum 50 per organization. |
| Attachment | `/v1/cards` | Which fields this card collects, in what order, which are required, and the `id` each value is stored under. |
| Value | `/v1/customers` | What one customer answered. |

There are eight field types: `text`, `textarea`, `number`, `currency`, `date`, `boolean`, `select`, `multiselect`. Only `select` and `multiselect` take `options`; only `number` takes `number_format`; only `date` takes `date_granularity`. Every other combination is normalised away on write.

> [!TIP]
>
> Give the `form_builder` entry the **same** `id` as the definition's `key`. The `id` is the storage key, the payload key and the template token, so keeping the two identical means one name to remember. Every example on this page does that.

## End to end

Set `VXN_API_KEY` once and every request below runs as written.

```bash theme={null}
export VXN_API_KEY=vxn_test_sk_...
```

**1. Define the fields**

Four definitions, four field types. `Idempotency-Key` is required on every `POST`.

```bash theme={null}
curl -X POST https://api.vexioncards.one/v1/custom_fields \
  -H "Authorization: Bearer $VXN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cf-member-note-1" \
  -d '{
    "key": "member_note",
    "label": "Note",
    "field_type": "text"
  }'
```

```bash theme={null}
curl -X POST https://api.vexioncards.one/v1/custom_fields \
  -H "Authorization: Bearer $VXN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cf-member-vip-1" \
  -d '{
    "key": "member_vip",
    "label": "VIP",
    "field_type": "boolean"
  }'
```

```bash theme={null}
curl -X POST https://api.vexioncards.one/v1/custom_fields \
  -H "Authorization: Bearer $VXN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cf-member-size-1" \
  -d '{
    "key": "member_size",
    "label": "Locker size",
    "field_type": "select",
    "options": [
      { "value": "small", "label": "Small" },
      { "value": "large", "label": "Large" }
    ]
  }'
```

```bash theme={null}
curl -X POST https://api.vexioncards.one/v1/custom_fields \
  -H "Authorization: Bearer $VXN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cf-member-visits-1" \
  -d '{
    "key": "member_visits",
    "label": "Visits",
    "field_type": "number",
    "number_format": "integer"
  }'
```

Each returns `201` with the definition. A fresh definition is unattached, so both usage counters start at zero. `label` and each option `label` are single strings. If you send a per-language object such as `{ "en": "Note", "sk": "Poznamka" }`, the definition keeps the `en` value (or the first language) as its one label. Per-language text for each card goes in the form entry's `translations_override`.

```json theme={null}
{
  "id": "cf_62c5biji5jplvw3jj5bljukosq",
  "object": "custom_field",
  "key": "member_size",
  "label": "Locker size",
  "description": null,
  "field_type": "select",
  "options": [
    { "value": "small", "label": "Small" },
    { "value": "large", "label": "Large" }
  ],
  "date_granularity": "date",
  "number_format": null,
  "usage_count": 0,
  "cards_using": [],
  "created_at": 1788416485,
  "updated_at": 1788416485
}
```

> [!NOTE]
>
> `key` is optional. Omit it and the server derives one from the label, suffixing a number if that slug is taken. An **explicit** `key` that is already used by an active definition is a `409 key_already_exists` instead, never a silent rename. A `label` that is already used by an active definition returns the same code.

**2. Create the custom card**

Send `mechanics: null`, a populated `pass_fields`, and one `form_builder` entry per field. `form_builder.enabled` is set to `false` here, which turns off the public signup page. Enrollment through this API is unaffected.

```bash theme={null}
curl -X POST https://api.vexioncards.one/v1/cards \
  -H "Authorization: Bearer $VXN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: card-member-record-1" \
  -d '{
    "type": "custom",
    "name": "Member Record",
    "description": "Studio member record, driven entirely through custom fields.",
    "default_language": "en",
    "languages_supported": ["en", "sk"],
    "issuer": { "company_name": "Demo Studio", "email": "hello@demo-studio.example" },
    "mechanics": null,
    "design": {
      "design_mode": "standard",
      "bg_color": "#123456",
      "text_color": "#FFFFFF",
      "strip_color": "#F6F6F6",
      "logo_url": "https://cdn.demo-studio.example/logo.png",
      "icon_url": "https://cdn.demo-studio.example/icon.png"
    },
    "form_builder": {
      "enabled": false,
      "default_language": "en",
      "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": "member_note", "type": "custom", "custom_field_id": "cf_wm5htxbxckxd3xvdmgqo6mruri", "required": false, "order": 2 },
        { "id": "member_vip", "type": "custom", "custom_field_id": "cf_uw5wzviinigabvsxixqz6rxrvu", "required": false, "order": 3 },
        { "id": "member_size", "type": "custom", "custom_field_id": "cf_62c5biji5jplvw3jj5bljukosq", "required": false, "order": 4 },
        { "id": "member_visits", "type": "custom", "custom_field_id": "cf_n6juz26kta2kba2fbifsdjf5ne", "required": false, "order": 5 }
      ]
    },
    "pass_fields": {
      "header": null,
      "front_left": {
        "label": { "en": "Locker", "sk": "Skrinka" },
        "value_template": "{{custom.member_size}}"
      },
      "front_right": {
        "label": { "en": "Visits", "sk": "Navstevy" },
        "value_template": "{{custom.member_visits}}"
      },
      "back": [
        {
          "key": "note",
          "label": { "en": "Note", "sk": "Poznamka" },
          "value_template": "{{custom.member_note | default: \"None\"}}"
        }
      ]
    }
  }'
```

The response is `201` with `"type": "custom"`, `"mechanics": null`, and the form you sent, including all four custom entries. Save the `id` (`crd_...`).

> [!WARNING]
>
> `mechanics` may be `null` or omitted, never an object. `pass_fields` is required and every `{{custom.<id>}}` token in it must name a `type: "custom"` entry you declared on this same request. A token that names nothing returns `422` [`unknown_form_field_reference`](https://vexioncards.mintlify.site/api-reference/errors#unknown_form_field_reference) on `pass_fields`.

**3. Enroll a customer with values**

Keys of the `custom` object are the `form_builder` field ids.

```bash theme={null}
curl -X POST https://api.vexioncards.one/v1/customers \
  -H "Authorization: Bearer $VXN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: enrol-member-1001" \
  -d '{
    "card_id": "crd_2g2xtrntkbpusoysnkyvnzpqqy",
    "email": "jana@example.com",
    "first_name": "Jana",
    "language": "en",
    "custom": {
      "member_note": "Prefers morning classes",
      "member_vip": true,
      "member_size": "large",
      "member_visits": 3
    }
  }'
```

`201`, and the values come back on the customer in the type of their field: a boolean stays a boolean, a number stays a number, a select stays the option value. A value sent in a look-alike form is converted to the field type. For example, the string `"42.00"` on a `number` field is stored as the number `42`, and `"true"` on a `boolean` field is stored as `true`.

```json theme={null}
{
  "id": "cus_p6d2fcgm3qw5ugfkugddwjinpy",
  "object": "customer",
  "card_id": "crd_2g2xtrntkbpusoysnkyvnzpqqy",
  "email": "jana@example.com",
  "first_name": "Jana",
  "language": "en",
  "status": "not_installed",
  "custom": {
    "member_note": "Prefers morning classes",
    "member_vip": true,
    "member_size": "large",
    "member_visits": 3
  }
}
```

**4. Read it back**

```bash theme={null}
curl https://api.vexioncards.one/v1/customers/cus_p6d2fcgm3qw5ugfkugddwjinpy \
  -H "Authorization: Bearer $VXN_API_KEY"
```

The `custom` object round-trips unchanged. It is also carried on the list endpoint, so you can page through every holder and read their values without a request per customer:

```bash theme={null}
curl "https://api.vexioncards.one/v1/customers?card_id=crd_2g2xtrntkbpusoysnkyvnzpqqy&limit=100" \
  -H "Authorization: Bearer $VXN_API_KEY"
```

**5. Update one field, clear another**

`PATCH` merges. Named keys are replaced, omitted keys are untouched, and a key set to `null` is deleted.

```bash theme={null}
curl -X PATCH https://api.vexioncards.one/v1/customers/cus_p6d2fcgm3qw5ugfkugddwjinpy \
  -H "Authorization: Bearer $VXN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: patch-member-1001-a" \
  -d '{
    "custom": {
      "member_size": "small",
      "member_note": null
    }
  }'
```

Afterwards the customer reads:

```json theme={null}
{
  "member_vip": true,
  "member_size": "small",
  "member_visits": 3
}
```

`member_size` changed, `member_note` is gone, and `member_vip` and `member_visits` were never mentioned so they stand.

> [!NOTE]
>
> Required-ness is judged against the merged result, not against the patch. Updating one field never re-demands another field the customer already has.

**6. See what a bad value does**

A value outside the definition's rules is `422 validation_failed` with the offending key named in `field_errors[]`. The per-field code is `invalid_value`, and the message names the rule that failed.

```bash theme={null}
curl -X POST https://api.vexioncards.one/v1/customers \
  -H "Authorization: Bearer $VXN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: enrol-member-bad-1" \
  -d '{
    "card_id": "crd_2g2xtrntkbpusoysnkyvnzpqqy",
    "email": "bad@example.com",
    "first_name": "Test",
    "custom": { "member_size": "medium" }
  }'
```

```json theme={null}
{
  "error": {
    "type": "invalid_request_error",
    "code": "validation_failed",
    "message": "One or more custom fields failed validation.",
    "request_id": "req_4AKP5oNlqhpeuSQGZdoFfU",
    "doc_url": "https://vexioncards.mintlify.site/api-reference/errors#validation_failed",
    "field_errors": [
      {
        "param": "custom.member_size",
        "code": "invalid_value",
        "message": "Invalid selection"
      }
    ]
  }
}
```

A key the card's form does not declare is a different error, and it is top level rather than inside `field_errors[]`:

```json theme={null}
{
  "error": {
    "type": "invalid_request_error",
    "code": "unknown_custom_field",
    "message": "Unknown custom field: member_locker. Define fields on the card's form_builder first (see GET /v1/cards/{id}).",
    "request_id": "req_Z6gp97X2vfJzK1kdrfygn2",
    "param": "custom.member_locker",
    "doc_url": "https://vexioncards.mintlify.site/api-reference/errors#unknown_custom_field"
  }
}
```

## Validation, by field type

Validation is driven entirely by `field_type`. There is nothing to configure per field beyond the type and its own options.

| `field_type` | Accepted value | Notes |
| - | - | - |
| `text` | string, 1 to 255 characters | Letters, digits, spaces and common punctuation only. Emoji and other symbols are rejected with `422 invalid_value`. |
| `textarea` | string, 1 to 1000 characters | Back of the pass only. |
| `number` | JSON number from -999999 to 999999 | `number_format: "integer"` rejects a fractional value. `decimal` is the default and allows one. A numeric string such as `"42"` is converted. |
| `currency` | integer from -99,999,999 to 99,999,999, in minor units of your organization currency | `2500` is 25.00 in a euro organization. |
| `date` | string within 100 years back and 10 years ahead | `date_granularity` sets the format: `date` is `YYYY-MM-DD`, `monthYear` is `YYYY-MM`, `year` is `YYYY`. |
| `boolean` | `true` or `false` | Also accepts `"true"`, `"false"`, `1` and `0`, and stores a real boolean. Renders as a localized Yes or No. |
| `select` | one of `options[].value` | |
| `multiselect` | array of 1 to 10 `options[].value` | |

A value that breaks one of these limits returns `422 validation_failed`. Anything beyond them, such as a regex or a cross-field rule, belongs in your own layer before the write.

## Showing values on the wallet pass

A custom card has no mechanics-derived template variables, so `{{balance}}`, `{{card.points}}` and the rest do not exist here. What you have is the universal customer variables plus your own fields.

Use `{{custom.<id>}}`, where `<id>` is the `id` you gave the `form_builder` entry. Not the `custom_field_id`, and not the definition's `key` unless you kept them the same.

```json theme={null}
"pass_fields": {
  "front_left": {
    "label": { "en": "Locker", "sk": "Skrinka" },
    "value_template": "{{custom.member_size}}"
  }
}
```

Four rules govern these tokens.

1. **Every token must be declared.** A `{{custom.<id>}}` naming a field the card's `form_builder` does not carry is rejected at write time with `422 unknown_form_field_reference` on `pass_fields`. This runs on both `POST /v1/cards` and `PATCH /v1/cards/{id}`.
2. **A `PATCH` resolves against whichever form is authoritative.** If your patch resends `form_builder`, the new field ids are the only legal ones, because a resent form replaces the old one wholesale. If your patch changes only `pass_fields`, the tokens are checked against the card's stored fields.
3. **`select` and `multiselect` render the label, not the stored value.** A customer whose `member_size` is `large` sees `Large` on an English pass and `Velka` on a Slovak one. Set `translations_override` on the form entry to change that per card.
4. **An unresolved value renders empty, never as the literal token.** Use the `default:` filter to control what appears instead: `{{custom.member_note | default: "None"}}`.

## Lifecycle

`usage_count` and `cards_using[]` on a definition track which cards reference it. Both move when a card write changes the form: creating a card with the field, patching a form to add or drop it, archiving a card, unarchiving one, and deleting one. A field listed twice on the same form counts once.

Those two counters are what the guard rails read.

| Action | Result |
| - | - |
| `PATCH /v1/custom_fields/{id}` changing `label`, `description` or `position` | Always allowed. |
| `PATCH` adding an option to a `select` or `multiselect` | Always allowed. |
| `PATCH` renaming `key` or changing `field_type` while attached | `409` [`custom_field_in_use`](https://vexioncards.mintlify.site/api-reference/errors#custom_field_in_use). |
| `PATCH` removing an option some customer has stored | `409 custom_field_in_use` on `options`. An option nobody selected can be removed even while the field is attached. |
| `DELETE /v1/custom_fields/{id}` while attached | `409 custom_field_in_use` on `id`. |
| `DELETE /v1/custom_fields/{id}` while unattached | `200`. Stored customer values survive, and re-adding the same key exposes them again. |
| `POST /v1/custom_fields` beyond 50 active definitions | `409 custom_field_limit_reached`. |

To free a definition, detach it first. `PATCH /v1/cards/{card_id}` with a `form_builder.fields[]` array that omits the entry, then delete the definition.

Deleting the card itself also frees its definitions. `DELETE /v1/cards/{card_id}` archives the template, which releases every field the form referenced, and enrolled customers keep their passes. Unarchiving the card takes the references back.

## Custom fields on other card types

Nothing on this page is exclusive to `custom`. A stamp, reward, discount or coupon card attaches definitions the same way and stores values on customers the same way. The only difference is that those types also have mechanics, a balance, and a writable transactions endpoint alongside the custom data.

## The public signup form

`form_builder.enabled` controls one thing: whether the public signup URL is served.

* `enabled: true` serves the public signup page for this card.
* `enabled: false` disables that page. Customers are enrolled through this API or the dashboard instead.

The flag does not change validation, storage, template rendering, or anything else about custom fields. A card with `enabled: false` keeps every field you defined on it, and `POST /v1/customers` accepts values for all of them.

Per-field `required` is a different setting and does apply to the API. A `form_builder` entry with `"required": true` must be present and non-empty on `POST /v1/customers`, and must not be cleared to empty by a `PATCH`, whether or not the public form is enabled.

### Two system fields are the exception

`required` is enforced on API enrollment for the five system fields the customer request carries.

| `system_key` | Request field | `required` enforced on the API |
| - | - | - |
| `email` | `email` | Yes |
| `first_name` | `first_name` | Yes |
| `last_name` | `last_name` | Yes |
| `phone` | `phone` | Yes |
| `date_of_birth` | `date_of_birth` | Yes |
| `consent` | none | No, public signup page only |
| `preferred_language` | `language` | No, public signup page only |

`consent` and `preferred_language` are collected by the public signup page and nowhere else. The customer request declares no `consent` property at all, and `language` always resolves to the card's `default_language`, so neither can ever be absent from a request. Marking either one required on a card changes nothing about `POST /v1/customers`.

One consequence to plan around: an API-created customer records no marketing consent from a form consent checkbox. If you collect consent in your own flow and want it on the record, send `marketing_opt_in` explicitly.

One more thing to know about that flag: `marketing_opt_in` is a stored record, not a switch. No send reads it today. A broadcast sent through `POST /v1/notifications` with an API key is not filtered by the flag, and it is not filtered by the customer's **Marketing messages** switch in the dashboard either. A broadcast sent from the dashboard, or from an AI assistant through the VEXiON MCP server, skips customers who switched marketing off. If you must honour opt-outs on a `POST /v1/notifications` send with an API key, keep your own list and send to explicit `customer_ids`.

> [!NOTE]
>
> Custom fields are unaffected by any of this. A `type: "custom"` entry marked `required: true` is enforced on the API exactly as it is on the public form.

## Webhooks

Subscribe through `POST /v1/webhook_endpoints`. Nine events cover this surface, and all of them use the standard envelope with the resource in `data.object`. See [Webhooks](https://vexioncards.mintlify.site/api-reference/webhooks.md) for signatures, retries and the full catalog.

| Event | Fires when |
| - | - |
| `custom_field.created` | A definition is created. |
| `custom_field.updated` | An allowed `PATCH` on a definition succeeds. `previous_attributes` carries the old value of each changed property. |
| `custom_field.deleted` | A definition is deleted. `data.object` is the full definition as it was. |
| `card.created` | A card is created, including the form and its attachments. |
| `card.updated` | A card is patched, including a form change that attaches or detaches a field. |
| `card.deleted` | A card is archived or erased. |
| `customer.created` | A customer is enrolled, with their `custom` values on the snapshot. |
| `customer.updated` | A customer is patched, including a `custom` merge or clear. |
| `customer.deleted` | A customer is deleted. |

> [!NOTE]
>
> Attaching a field to a card moves that definition's `usage_count` and `cards_using[]`, but it fires `card.updated`, not `custom_field.updated`. Treat those two counters as a snapshot and re-read the definition with `GET /v1/custom_fields/{id}` before acting on them in a long-running job.

## Rate limits

Every bucket below is the standard one for its endpoint. Nothing on this page has a limit of its own. See [Rate Limits](https://vexioncards.mintlify.site/api-reference/rate-limits.md) for headers and `429` handling.

| Request | Bucket | Limit |
| - | - | - |
| Any verb on `/v1/custom_fields` | default | 60 per minute, per API key |
| Any verb on `/v1/cards` | default | 60 per minute, per API key |
| `POST /v1/customers` | customer dispatch | 60 per minute, per API key |
| `PATCH` or `DELETE /v1/customers/{id}` | default | 60 per minute, per API key |
| `GET /v1/customers` filtered by `email`, `phone` or `external_id` | customer lookup | 5 per **second**, per **organization** |

The lookup bucket is the only org-scoped one here. Every key in your organization shares that budget, so a second key does not double it. A lookup also counts against the default 60 per minute, per API key.

## Scopes

| Request | Scope |
| - | - |
| `GET` on `/v1/custom_fields` | `custom_fields:read` |
| `POST`, `PATCH`, `DELETE` on `/v1/custom_fields` | `custom_fields:write` |
| `GET` on `/v1/cards` | `cards:read` |
| `POST`, `PATCH`, `DELETE` on `/v1/cards` | `cards:write` |
| `GET` on `/v1/customers` | `customers:read` |
| `POST`, `PATCH`, `DELETE` on `/v1/customers` | `customers:write` |

## Errors on this path

| Code | HTTP | Where |
| - | - | - |
| [`parameter_missing`](https://vexioncards.mintlify.site/api-reference/errors#parameter_missing) | 400 | `pass_fields` omitted on a custom card create. |
| [`mechanics_type_mismatch`](https://vexioncards.mintlify.site/api-reference/errors#mechanics_type_mismatch) | 422 | A mechanics object sent on a custom card. |
| [`unknown_form_field_reference`](https://vexioncards.mintlify.site/api-reference/errors#unknown_form_field_reference) | 422 | A `pass_fields` token names a field the form does not declare. |
| [`parameter_unknown`](https://vexioncards.mintlify.site/api-reference/errors#parameter_unknown) | 422 | A top-level `custom_fields` array sent on a card. Use `form_builder.fields[]`. |
| [`unknown_custom_field`](https://vexioncards.mintlify.site/api-reference/errors#unknown_custom_field) | 422 | A `custom` key the card's form does not define. |
| [`validation_failed`](https://vexioncards.mintlify.site/api-reference/errors#validation_failed) | 422 | A custom value that fails its field type, or a missing required field. Details in `field_errors[]`. |
| [`card_type_not_writable`](https://vexioncards.mintlify.site/api-reference/errors#card_type_not_writable) | 422 | A transaction posted against a custom card. |
| [`custom_field_in_use`](https://vexioncards.mintlify.site/api-reference/errors#custom_field_in_use) | 409 | A rename, type change, option removal or delete blocked by an attachment. |
| [`custom_field_limit_reached`](https://vexioncards.mintlify.site/api-reference/errors#custom_field_limit_reached) | 409 | Past 50 active definitions in the organization. |
| [`key_already_exists`](https://vexioncards.mintlify.site/api-reference/errors#key_already_exists) | 409 | An explicit `key`, or a `label`, that an active definition already uses. |

## Where next

> [card] [Errors](https://vexioncards.mintlify.site/api-reference/errors.md)
>
> Every code, with the envelope it arrives in.

> [card] [Webhooks](https://vexioncards.mintlify.site/api-reference/webhooks.md)
>
> Signatures, retries, the 22-event catalog.

> [card] [Rate Limits](https://vexioncards.mintlify.site/api-reference/rate-limits.md)
>
> Buckets, headers, and handling 429.

> [card] [Idempotency](https://vexioncards.mintlify.site/api-reference/idempotency.md)
>
> Why every POST carries the key.


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