# Variant custom fields (/docs/variant-custom-fields)



Variant groups and their linked inventory items have separate custom fields:

* The group's top-level `customFields` belong to object type `variant_groups`.
* Each item-based variant's `customFields` belong to object type `items`, identified by that selection's `itemId`. Selections referencing the same item share its stored values at the same location.
* Customer answers collected during checkout are order-line snapshots, submitted as `guest_field_answers`. They do not overwrite stored group or inventory values.

## Stored group attributes [#stored-group-attributes]

Create definitions through `/api/v1/locations/{locationId}/object-types/variant_groups/fields`. Read values in the group's normal GET response. Set values using top-level `customFields` in `POST /api/v1/variant-groups` or `PUT /api/v1/variant-groups/{id}`. These writes are atomic with the group, like reservation custom fields.

Use `fieldId` or a stable `key` from the effective definitions. `expectedVersion` is optional; include the last-read `version` to reject stale edits with `409 VERSION_CONFLICT`. A new value starts at version `0`. See [Inline values](/docs/custom-fields#values-on-normal-create-and-update-calls).

## Stored values directly on a variant [#stored-values-directly-on-a-variant]

Read `variants[].customFields` and `variants[].metadata.customFields` in a group response, or fetch the selected option directly:

```bash
curl "$COMMON_API/api/v1/variants/34?locationId=1234" \
  -H "X-API-Key: $KARMA_API_KEY"
```

Patch its linked item's values without replacing the group's selections:

```bash
curl -X PATCH "$COMMON_API/api/v1/variants/34?locationId=1234" \
  -H "X-API-Key: $KARMA_API_KEY" -H 'Content-Type: application/json' \
  -d '{"customFields":[{"key":"engraving","expectedVersion":0,"value":"Alex"}]}'
```

Replace `engraving` with your actual item-field key. Omit `expectedVersion` only when you intend to overwrite the latest value. Each change accepts either `fieldId` or `key`, and either `value` or `reset: true`. Omitted fields and an empty array preserve existing values. `value: null` clears an optional field; resetting removes the local override and restores an inherited item value.

The response contains `id` (the variant ID), `itemId`, `locationId`, `customFields`, and `metadata.customFields`. It is a field projection; use the linked item's normal GET for full inventory details. Legacy selections with `itemId: null` have no linked form or fields and return 422 on the direct field route.

The API key needs `variant_groups.read` and `inventory.read`; writes also need `inventory.update` and the field's `apiWrite` channel. Only API-readable fields are returned. The requested location must have access to both the group and the linked item, including inheritance rules. Responses are private and must not be cached across callers.

Group create/update requests can also include `variants[].customFields` alongside each selection's `itemId`. Those changes and the group edit share one transaction. When selections share an item, include its changes once or submit identical changes. A PUT of `variants` replaces the selection list; retain every selection you still need. Neither a new variant ID nor a new group resets the linked item's existing field versions.

Reads batch and deduplicate item references. Writes resolve the item schema once per group write and fetch existing field versions together per item. Invalid values and stale versions roll back the native edit and custom field changes together.

## Customer answers when ordering [#customer-answers-when-ordering]

Use `POST /api/v1/orders/create` to submit customer answers. The simplified `POST /api/v1/public/orders/create` endpoint currently does not forward them.

## 1. Configure the fields [#1-configure-the-fields]

Create definitions through the [Objects and fields API](/docs/custom-fields#definitions), using `/api/v1/locations/{locationId}/object-types/items/fields`. Enable `channels.guestInput` for fields customers should fill in; `guestDisplay` controls display and does not enable input. Set `required` and validation in the definition as needed.

Definitions apply to items at the location and inherit down the location tree. To limit an input to a particular item, use a condition on a native item property. For example, this definition asks for an engraving only when the linked item's title is `Engraved cup`:

```json
{
  "key": "engraving",
  "label": "Engraving",
  "type": "text",
  "required": true,
  "config": {
    "maxLength": 40,
    "condition": { "field": "native.title", "equals": "Engraved cup" }
  },
  "channels": {
    "staff": true,
    "table": false,
    "tableDefault": false,
    "guestDisplay": false,
    "guestInput": true,
    "apiRead": true,
    "apiWrite": false,
    "analytics": true,
    "aiSource": false
  }
}
```

This title condition uses an exact match; review it if the item is renamed. `apiWrite` governs editing stored object values through the values API. Customer answers use the guest-input form instead and are saved with the order line, without changing the inventory item's stored values.

## 2. Discover the selected variant's form [#2-discover-the-selected-variants-form]

Read the group with `GET /api/v1/variant-groups/{id}`. Each selected option has a variant `id` and, for item-based variants, an `itemId` pointing to its inventory item. A legacy variant with `itemId: null` has no linked item form.

For example, if variant `34` has `itemId: 789`, fetch the linked item at the ordering location:

```bash
curl "$COMMON_API/api/v1/inventory/789?locationId=1234" \
  -H "X-API-Key: $KARMA_API_KEY"
```

Read `data.guestInput`, which contains `schemaToken` and `fields`. Use this form rather than the item's `customFields` display values or a variant-group ID. An illustrative fragment of the item response is:

```json
{
  "data": {
    "id": 789,
    "guestInput": {
      "schemaToken": "<copy the returned 64-character token>",
      "fields": [
        {
          "id": "2c6f83c9-993f-4c39-a75d-b78cc89f0c10",
          "key": "loc1234.engraving",
          "label": "Engraving",
          "description": "",
          "group": "",
          "type": "text",
          "required": true,
          "revision": 1,
          "config": { "maxLength": 40 }
        }
      ]
    }
  }
}
```

Render each field using its label, type, required flag and config. Native item conditions have already been evaluated by the server. If `config.condition` is returned, its `field` is another field's **key**; show the dependent input only when the other answer matches `equals`. For a multiselect, matching means membership; for a taxonomy, a descendant also matches its ancestor. Omit answers to hidden fields.

The token is tied to the location, linked inventory item and effective form. Load the parent item's form separately and load a form for each selected linked item; do not reuse the parent's token for a variant.

## 3. Submit answers on the selected variant [#3-submit-answers-on-the-selected-variant]

The API key needs `inventory.read` to load item forms, `variant_groups.read` to read groups, and `inventory.create` to place the order, with access to the ordering location.

Use the **variant ID** in `variants[].id`. Place the linked item's token and answers in that selection's `guest_field_answers`. The parent item's answers belong in `items[].guest_field_answers` instead. These request properties use snake\_case; `schemaToken` and `fieldId` use camelCase.

For parent item `456` and selected variant `34`, submit the following to `POST /api/v1/orders/create`. Replace the placeholder token and field UUID with values from variant `34`'s linked item form. This example assumes the parent item has no guest input fields; otherwise include its own submission too.

```json
{
  "locationId": 1234,
  "order_channel": "ONLINE",
  "order_type": "TAKEAWAY",
  "order_destination": "NONE",
  "order_destination_id": null,
  "order_flow": "OPEN_TAB",
  "tab_member": { "identifier": "d7372e2f-abcd-486e-a6b7-e5ad1e401f81" },
  "vouchers": [],
  "items": [
    {
      "id": 456,
      "quantity_cents": 100,
      "comments": [],
      "variants": [
        {
          "id": 34,
          "quantity_cents": 100,
          "guest_field_answers": {
            "schemaToken": "<token from item 789 at location 1234>",
            "answers": [
              {
                "fieldId": "2c6f83c9-993f-4c39-a75d-b78cc89f0c10",
                "value": "Alex"
              }
            ]
          }
        }
      ]
    }
  ]
}
```

`fieldId` must be the form field's UUID, not its key or label. Submit only `{ "schemaToken", "answers" }`; each answer contains only `{ "fieldId", "value" }`. Labels, revisions and display values are generated by the server. Answers are stored as snapshots on the corresponding order lines.

Use a fresh `tab_member.identifier` for a new anonymous guest, or supply the guest identity supported by the [Ordering API](/docs/ordering/orders). `order_flow: "OPEN_TAB"` leaves payment for later; choose the flow appropriate to your integration.

## Value formats and validation [#value-formats-and-validation]

Use the [documented wire types](/docs/custom-fields#definitions): text and dates are strings, numbers are decimal **strings**, booleans are JSON booleans, currency is `{ "amount": "80", "currency": "SEK" }`, and select/taxonomy/multiselect values use option IDs from the form's `config.options`. Relations, formulas and rollups are not guest input types.

Each submission replaces the whole form's answers. Include every visible required field; omitted optional visible fields become `null`. `false`, `"0"` and `null` are distinct values. When a form has fields, submit its token even if all inputs are optional and `answers` is empty. A form with no fields can be omitted. The limit is 100 answers per submission.

| Status | Code                 | Recovery                                                                                                                                                                |
| ------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 409    | `FIELD_FORM_CHANGED` | Reload the linked item's form at the same location, let the customer review the inputs, and retry with the new token. A token from another item or location also fails. |
| 422    | `FIELD_VALIDATION`   | Correct missing required values, invalid types or option IDs, duplicate field IDs, hidden/unknown fields, or a missing form submission. Follow the error message.       |

Field validation happens within the order transaction. A rejected submission does not create a partial order.
