App data
App data lets your app attach its own typed fields to a business’s records — a tracking number on an invoice, a loyalty tier on a customer, a sync status on a purchase. Fields are visible to the merchant with your app’s own labels, optionally merchant-editable, and namespaced to your app: no other app can read or write yours.
Definitions
Section titled “Definitions”Before you can store a value, declare a definition — the field’s shape — on your app’s App data tab in the developer console (org admins only; there’s no CLI command for this today). Up to 50 per app, across all resource types.
{ "resourceType": "INVOICE", "key": "tracking_number", "type": "TEXT", "name": { "en": "Tracking number", "ar": "رقم التتبع" }, "merchantVisible": true, "merchantEditable": false}| Setting | Rules |
|---|---|
resourceType | One of the resource types below |
key | ^[a-z][a-z0-9_]{0,63}$ — lowercase letters, digits and underscores, starting with a letter; unique per resource type within your app |
type | One of the types below |
name | Localized { en, ar?, es?, fr?, pt? }, ≤ 60 characters per language; English required. It’s the label merchants see. |
description | Optional, localized, ≤ 300 characters per language; null clears it |
choices | CHOICE fields only, and required there: 1–50 distinct, non-empty options of ≤ 60 characters each. Options are stored trimmed, so two that differ only by surrounding spaces count as duplicates — any breach fails with INVALID_CHOICES. |
validation | Optional: { maxLength } for TEXT (1–255) and MULTILINE_TEXT (1–5,000); { min?, max? } for INTEGER (whole numbers) and DECIMAL, with min ≤ max. Not allowed on other types. null clears it. |
merchantVisible | Show the field to merchants on the record |
merchantEditable | Let merchants edit it — requires merchantVisible |
position | Optional whole number, 0 or more (default 0) — the order merchants see your fields in on a record, lowest first. The console’s reorder arrows set it. |
Editing a field changes only the settings you send — the console saves it with
PATCH /developer/apps/:appId/app-data/definitions/:defId — so omit a setting
to keep its current value. null clears description and validation.
choices: null clears the list, which a CHOICE field can’t be without — so
there it fails with INVALID_CHOICES (on any other type the list is already
empty). name, merchantVisible, merchantEditable and position have no
empty state: they can’t be null, and a null is rejected with a 400.
resourceType, key and type are fixed once a field exists — to change one,
delete the field and create it again. Deleting a field deletes every value
stored for it, in every business. A JSON field can’t be visible or
editable to merchants; it’s for your app’s own structured data.
Types and limits
Section titled “Types and limits”null is the universal “delete this value” sentinel for every type — send it
in place of a real value to clear a field. Every other type has its own
validation:
| Type | Value shape | Rules |
|---|---|---|
TEXT | string | No line breaks; ≤ 255 characters by default (a definition’s validation.maxLength can set a stricter cap) |
MULTILINE_TEXT | string | Line breaks allowed; ≤ 5,000 characters by default (validation.maxLength can lower it) |
INTEGER | number | Must be a safe integer; validation.min/max if set |
DECIMAL | number | Must be finite, at most 6 decimal places; validation.min/max if set |
BOOLEAN | true/false | — |
DATE | string | YYYY-MM-DD, and must be a real calendar date (2026-02-30 is rejected) |
DATETIME | string | A strict ISO-8601 date-time; stored normalized to Date.prototype.toISOString() output, so two equivalent inputs settle on one representation |
URL | string | Must be an absolute https: URL (not http:), ≤ 2,048 characters |
CHOICE | string | Must be exactly one of the definition’s choices[] as stored — trimmed (1–50 options, each ≤ 60 characters) |
JSON | object or array | Not a bare string/number/boolean at the top level — must be a JSON container; ≤ 16 KB serialized |
Other limits: batch writes ≤ 25 values per call, and a batch is
all-or-nothing — if any value fails, none is written; reads accept
resourceIds (comma-separated) for up to 50 ids in one call; ≤ 50
definitions per app in total.
Resource types and scopes
Section titled “Resource types and scopes”App data never widens access — to store a value on a resource, your installation must already hold that resource’s read scope:
| Resource type | Read scope required |
|---|---|
BUSINESS | profile:read |
CUSTOMER | customers:read |
SUPPLIER | suppliers:read |
ITEM | items:read |
INVOICE | invoices:read |
QUOTE | quotes:read |
SALES_RETURN | sales-returns:read |
PURCHASE | purchases:read |
PURCHASE_RETURN | purchase-returns:read |
PAYMENT | payments:read |
SUPPLIER_PAYMENT | supplier-payments:read |
EXPENSE | expenses:read |
Partner API
Section titled “Partner API”Everything below needs app-data:read (reads) or app-data:write (writes) —
see Scopes.
# List your app's definitionscurl -s https://api.getonebooks.com/app-data/definitions \ -H "Authorization: Bearer $ACCESS_TOKEN"
# Read values on one or more resourcescurl -s "https://api.getonebooks.com/app-data?resourceType=INVOICE&resourceIds=$ID_1,$ID_2" \ -H "Authorization: Bearer $ACCESS_TOKEN"
# Write (batch, up to 25) — value: null deletescurl -s -X PUT https://api.getonebooks.com/app-data \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "values": [ { "resourceType": "INVOICE", "resourceId": "'"$ID_1"'", "key": "tracking_number", "value": "1Z999AA10123456784" } ] }'
# Delete one value by idcurl -s -X DELETE https://api.getonebooks.com/app-data/$VALUE_ID \ -H "Authorization: Bearer $ACCESS_TOKEN"import { OneBooksClient } from '@onebooks/node';
const client = new OneBooksClient({ accessToken });
const definitions = await client.appData.definitions();
const values = await client.appData.get({ resourceType: 'INVOICE', resourceIds: [id1, id2],});
await client.appData.set([ { resourceType: 'INVOICE', resourceId: id1, key: 'tracking_number', value: '1Z999AA10123456784' },]);
await client.appData.delete(valueId);Reads and writes answer { "data": [...] }. A stored value looks like this —
updatedVia tells you whether your app (APP) or a merchant (MERCHANT) set
it last:
{ "id": "cm1appdatavalue00000000001", "resourceType": "INVOICE", "resourceId": "cm1invoice0000000000000001", "key": "tracking_number", "value": "1Z999AA10123456784", "updatedVia": "APP", "updatedAt": "2026-09-22T10:03:11.000Z" }A PUT echoes every value it wrote, in order (with id: null for a value you
deleted with null); DELETE /app-data/:id answers 204. Writing needs the
record to exist in the token’s business and your installation to hold the
resource type’s read scope; failures carry the codes listed on
Errors.
Merchant visibility and editing
Section titled “Merchant visibility and editing”A field shows to the merchant (with your name/description in their own
language, falling back to English) only when merchantVisible: true — and,
like the partner API, only while your installation in that business holds the
record type’s read scope (an app reinstalled with narrower scopes shows
nothing on a type it can no longer read). It’s editable by the merchant only
when you additionally set merchantEditable: true and the merchant holds
the write permission for that record type — the same permission editing the
record itself requires (e.g. invoices.create for an INVOICE field). When a
merchant edits a value, OneBooks emits app_data.updated to your app — see
the Event catalog. Its resourceType
and resourceId (in the payload and on the event itself) name the record the
field is on, not the app data value.
Merchant-side reads and edits go through a different, session-authenticated
route (GET /apps/data/:resourceType/:resourceId,
PUT /apps/data/:resourceType/:resourceId/:definitionId) — that’s the
OneBooks frontend calling on the merchant’s behalf, not something your
integration calls directly.
Where next
Section titled “Where next”- Webhooks — subscribe to
app_data.updatedto hear about merchant edits. - Hosted functions — run your own code on business events (an invoice paid, a customer created) without a server.