Objects and fields
Attach typed, versioned fields to items, guests, reservations, tables, locations, tabs, orders and purchases, and read or write them through the API.
Merchants can extend Karma's objects with their own fields: a producer and vintage on a wine, a purchase-order reference on a tab, a delivery zone on a table. Fields are typed, inherit down the location tree, carry a version for safe concurrent edits, and appear on the object's normal API responses.
Use fields when the data has a type and a meaning. For opaque integration identifiers such as your own SKU or shop ID, the free-form metadata object on items and locations is still the right place.
For group attributes, linked-item values on variants, and customer checkout input, see Variant custom fields.
Objects
Fields attach to one of ten object types. The objectType path segment and the record identifier are:
objectType | Record id |
|---|---|
items | inventory item id |
guests | user id, scoped to the location |
reservations | booking id |
tables | table UUID |
locations | the location id itself |
tabs | tab UUID |
orders | kitchen order UUID |
purchases | purchase id |
retailer_users | staff user id, scoped to the location |
cost_centers | cost center id |
variant_groups | variant group id |
loyalty_members | loyalty membership account id |
Every route lives under /api/v1/locations/{locationId}/object-types/{objectType} and uses the standard envelope. Reading and writing values takes the object's own read and update permissions; managing definitions takes the settings permission.
Definitions
A definition has an immutable id, an immutable key, a type, a label, a group, validation config and channel policies. Keys are namespaced by owner: merchant fields are loc<ownerLocationId>.<key>, Karma-provided fields are karma.*.
| Type | Value on the wire |
|---|---|
text, long_text | string |
number | decimal as a string, e.g. "12.5" |
currency | { "amount": "80", "currency": "SEK" } |
boolean | true / false |
date | "2026-09-24" |
select, taxonomy | an option id, never its label |
multiselect | array of option ids |
formula, rollup | computed, read-only |
relation | id of a record of config.relationObject |
Definitions inherit: a field created at a parent location applies to every descendant. A descendant may customise the label, group and placement and add its own options through the extensions route, but cannot change the type, validation or access policy.
Fetch the effective schema for a location and object:
curl "$COMMON_API/api/v1/locations/1234/object-types/items/fields" \
-H "X-API-Key: $KARMA_API_KEY"The response lists fields with their config, channels, revision, inherited and canWrite, plus capabilities telling you whether this location may customise fields (custom_fields, part of Intelligence) and use Kaia suggestions.
Create a field:
curl -X POST "$COMMON_API/api/v1/locations/1234/object-types/tabs/fields" \
-H "X-API-Key: $KARMA_API_KEY" -H 'Content-Type: application/json' \
-d '{
"key": "po_reference",
"label": "PO reference",
"group": "Accounting",
"type": "text",
"config": { "maxLength": 40 },
"channels": {
"staff": true, "table": true, "tableDefault": false,
"guestDisplay": false, "guestInput": false,
"apiRead": true, "apiWrite": true,
"analytics": true, "aiSource": false
}
}'Returns 201 with a Location header. Updating goes through PATCH .../fields/{id} with the revision you last read; a stale revision is rejected with 409. Fields are archived, never deleted, and a key stays reserved after archival.
Channels are the access policy
An API key only sees fields with apiRead and can only write fields with apiWrite. A field that is staff-only is simply absent from API responses. Guest surfaces see only guestDisplay fields, and never schema or configuration.
Values
Read a record's values:
curl "$COMMON_API/api/v1/locations/1234/object-types/tabs/records/59e4ce7d-b801-4323-af9d-ba0ca4bd757a/values" \
-H "X-API-Key: $KARMA_API_KEY"{
"status": "success",
"data": {
"values": [
{
"fieldId": "2c6f…",
"value": "PO-42",
"version": 3,
"source": "api",
"status": "fresh",
"sourceLocationId": 1234,
"updatedAt": "2026-09-24T09:12:40.000Z"
}
]
}
}source is who last set it: manual, api, ai, formula or rollup. status matters for computed fields: fresh, stale (an input changed and the recompute is pending), error (see error) or frozen (the plan no longer covers calculations, the last value is kept). Unset fields are omitted; an explicitly cleared field is present with value: null.
Write values atomically, one call per record, up to 100 changes. Use the expectedVersion you last read to reject stale edits; a mismatch fails the whole request with 409 VERSION_CONFLICT and nothing is written. It is optional: omitting it intentionally updates the latest value. Identify each change by fieldId or a stable key.
curl -X PATCH "$COMMON_API/api/v1/locations/1234/object-types/tabs/records/59e4ce7d-…/values" \
-H "X-API-Key: $KARMA_API_KEY" -H 'Content-Type: application/json' \
-d '{
"changes": [
{ "fieldId": "2c6f…", "expectedVersion": 3, "value": "PO-43" },
{ "fieldId": "9a10…", "expectedVersion": 0, "value": { "amount": "1250", "currency": "SEK" } }
]
}'A never-set field has version 0. Send "value": null to clear a field, or "reset": true on a shared item at a child location to drop the local override and inherit the parent's value again. Validation failures return 422 FIELD_VALIDATION with the offending field in the message.
Read many records at once with POST .../values/batch and up to 200 ids.
Values on normal create and update calls
Authenticated staff and API integrations can include optional customFields operations in the normal record request. They are saved in the same transaction as the record, so invalid values or a stale version roll back the whole edit.
For example, update a booking's guest count and meal abbreviation in one call:
PUT /api/v1/bookings/{bookingId}
Content-Type: application/json{
"guestCount": 4,
"customFields": [
{ "fieldId": "<MA-field-UUID>", "expectedVersion": 1, "value": "LU" }
]
}The same array is accepted on booking creation, with expectedVersion: 0 for each initial value. Definitions must already exist; inline operations set values, they do not create field definitions.
| Object | Normal calls accepting inline operations |
|---|---|
| Reservations | POST /bookings, PUT /bookings/{id} |
| Items | POST /inventory, PUT /inventory/{id}; bulk-upsert item rows and the bulk update patch |
| Guests | POST /locations/{locationId}/customers, PATCH /locations/{locationId}/customers/{userId} |
| Tables | POST /tables, PUT /tables/{id}, PUT /tables/bulk |
| Locations | POST /locations, PATCH /locations/{locationId}/settings |
| Tabs | POST /tabs/resolve, PATCH /tabs/{id}, PATCH /tabs/{id}/metadata |
| Orders | POST /orders/create, POST /orders/append-to-dispatch, PATCH /orders/{id} |
| Purchases | PATCH /purchases/{id}, POST /purchases/{purchaseIntentId}/connect-customer |
| Staff users | PUT /retailer/users/{id}, PUT /retailer/users/me |
| Cost centers | POST /cost-centers, PUT /cost-centers/{id} |
| Variant groups | POST /variant-groups, PUT /variant-groups/{id}; top-level customFields edits group values |
| Variant linked items | PATCH /variants/{id}?locationId={locationId}, or variants[].customFields in group writes |
All paths above start with /api/v1. The general PATCH calls on tabs, orders and purchases edit custom fields only; their lifecycle and payment actions keep their dedicated APIs. Staff-user creation creates an invitation, so values attach to the actual user after acceptance. Purchases are created by payment flows, rather than a general record-create endpoint.
- Omit
customFieldsto preserve values on updates. An empty array makes no field changes. - You do not need to include unset optional fields. On creation, writable required fields must have a value; conditional required fields apply when their condition matches. A submitted update checks the resulting required values, preserving existing values for omitted fields.
- Send
value: nullto clear an optional field. Required fields cannot be cleared. Usereset: truewithoutvalueto drop an inherited-item override; a required field must still have an inherited value. - Send the last-read
expectedVersionfor updates. A conflict returns 409VERSION_CONFLICT; invalid values return 422FIELD_VALIDATION. Permissions, API channel policies and plan requirements are identical to the values endpoint. - Guest checkout continues to use the guest form's schema token and answers, rather than staff field operations.
The response customFields array contains labels and display values as well as raw values and versions. Do not copy that entire read projection into a write: send only fieldId, expectedVersion and value (or reset).
Values on normal responses
You do not need the fields API to read values. Native detail and list responses for supported object types carries a read-only projection of the fields your key may read:
{
"id": 456,
"title": "Chianti Classico",
"customFields": [
{ "fieldId": "…", "key": "loc1234.producer", "label": "Producer", "group": "Wine",
"type": "text", "value": "Antinori", "displayValue": "Antinori", "version": 2, "status": "fresh" }
],
"metadata": {
"externalId": "sku-3732185",
"customFields": { "loc1234.producer": "Antinori" }
}
}metadata.customFields is the same data keyed by field key, convenient for lookups. Both are current values, not a snapshot from the time of a sale. Writing metadata.customFields through a generic metadata update is rejected; values change through inline write operations or the values route above. Webhook payloads carry the same projection, described in Receiving webhooks.
Searching by field
POST .../search filters and sorts records by typed values with cursor pagination:
curl -X POST "$COMMON_API/api/v1/locations/1234/object-types/items/search" \
-H "X-API-Key: $KARMA_API_KEY" -H 'Content-Type: application/json' \
-d '{
"filters": [
{ "field": "7b3e…", "operator": "eq", "value": "opt_red" },
{ "field": "c412…", "operator": "gte", "value": "2018" }
],
"sort": { "field": "c412…", "descending": true },
"limit": 50
}'Operators are eq, gte, lte, contains (multiselect membership or text) and unset. Up to five filters; query searches titles; ids restricts to a set. Follow nextCursor from the response for the next page. Filtering by generic metadata keys still goes through the object's own list endpoint; this route is for typed field values.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_INPUT, INVALID_FILTER | Malformed body, or a filter names a field you cannot read |
| 403 | FORBIDDEN, PLAN_REQUIRED | Missing permission, channel, or the custom_fields entitlement |
| 404 | NOT_FOUND | Record or field does not exist at this location |
| 409 | VERSION_CONFLICT, CONFLICT | Stale version or revision, or the key already exists |
| 422 | FIELD_VALIDATION, FIELD_LIMIT, SCHEMA_LIMIT | Value rejected, or a bound reached |
Bounds: 100 merchant fields per location, 500 effective fields per object type, 1,000 options per field, 200 records per batch or search page.