Skip to content

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.

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
}
SettingRules
resourceTypeOne 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
typeOne of the types below
nameLocalized { en, ar?, es?, fr?, pt? }, ≤ 60 characters per language; English required. It’s the label merchants see.
descriptionOptional, localized, ≤ 300 characters per language; null clears it
choicesCHOICE 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.
validationOptional: { 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.
merchantVisibleShow the field to merchants on the record
merchantEditableLet merchants edit it — requires merchantVisible
positionOptional 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.

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:

TypeValue shapeRules
TEXTstringNo line breaks; ≤ 255 characters by default (a definition’s validation.maxLength can set a stricter cap)
MULTILINE_TEXTstringLine breaks allowed; ≤ 5,000 characters by default (validation.maxLength can lower it)
INTEGERnumberMust be a safe integer; validation.min/max if set
DECIMALnumberMust be finite, at most 6 decimal places; validation.min/max if set
BOOLEANtrue/false—
DATEstringYYYY-MM-DD, and must be a real calendar date (2026-02-30 is rejected)
DATETIMEstringA strict ISO-8601 date-time; stored normalized to Date.prototype.toISOString() output, so two equivalent inputs settle on one representation
URLstringMust be an absolute https: URL (not http:), ≤ 2,048 characters
CHOICEstringMust be exactly one of the definition’s choices[] as stored — trimmed (1–50 options, each ≤ 60 characters)
JSONobject or arrayNot 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.

App data never widens access — to store a value on a resource, your installation must already hold that resource’s read scope:

Resource typeRead scope required
BUSINESSprofile:read
CUSTOMERcustomers:read
SUPPLIERsuppliers:read
ITEMitems:read
INVOICEinvoices:read
QUOTEquotes:read
SALES_RETURNsales-returns:read
PURCHASEpurchases:read
PURCHASE_RETURNpurchase-returns:read
PAYMENTpayments:read
SUPPLIER_PAYMENTsupplier-payments:read
EXPENSEexpenses:read

Everything below needs app-data:read (reads) or app-data:write (writes) — see Scopes.

Terminal window
# List your app's definitions
curl -s https://api.getonebooks.com/app-data/definitions \
-H "Authorization: Bearer $ACCESS_TOKEN"
# Read values on one or more resources
curl -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 deletes
curl -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 id
curl -s -X DELETE https://api.getonebooks.com/app-data/$VALUE_ID \
-H "Authorization: Bearer $ACCESS_TOKEN"

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.

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.

  • Webhooks — subscribe to app_data.updated to hear about merchant edits.
  • Hosted functions — run your own code on business events (an invoice paid, a customer created) without a server.