Skip to content

Records and payloads

All data of an inventory is a set of records. A record has four parts:

Part Field Meaning
Type entity What the record describes, for example article
Identity id A UUID, unique within the entity type
Version hlc A hybrid logical clock stamp of the device that wrote this version
Content data A JSON object, the payload; null for a deleted record (tombstone)

A record is always written as a whole: a new version replaces the complete payload of the previous one. There are no partial updates and no field-level merging. Two devices that edit the same record concurrently produce two complete versions, and the one with the greater hlc wins everywhere (see Merge rules).

Deleting a record writes a tombstone: a version with op: "delete" and data: null. Tombstones are kept forever, so that a device that comes back after a long time cannot bring the deleted record back with an older version.

Entity Mutable One per inventory Page
article yes no article
location yes no location
recipient yes no recipient
movement no no movement
delivery_note yes no delivery_note
company_profile yes yes, id 00000000-0000-4000-8000-000000000001 company_profile
document_template yes one per document kind, ids ...000000000002 (delivery notes) and ...000000000003 (loans) document_template
loan yes no loan

Entity names are lower case letters, digits and underscores, start with a letter and have at most 64 characters (^[a-z][a-z0-9_]*$). A client or server that receives an entity type it does not know MUST store it, merge it with the rules for mutable records and relay it unchanged. This lets newer apps add types without breaking older servers.

Record ids are UUIDs in the 8-4-4-4-12 hexadecimal form, for example 8C1F3D2E-5B7A-4A61-9F0E-2B4C6D8E0A13.

  • Writers MUST send the canonical upper-case form. The reference app generates random (version 4) UUIDs and always writes upper case.
  • Readers and servers MUST compare ids case-insensitively. The reference server stores and returns ids in upper case, so a lower-case id that was pushed comes back upper case on pull.
  • The UUID version is not checked. Fixed ids such as 00000000-0000-4000-8000-000000000001 are valid.
  • Ids are unique per entity type: an article and a location may in theory share an id, and the pair (entity, id) identifies a record. Writers SHOULD still use fresh random UUIDs for every record.

Ids referenced inside payloads (article_id, parent_id, movement_id and so on) follow the same rules.

A change is the transport form of one record version. The same envelope is used in push requests, pull responses and the iCloud mapping.

{
"entity": "article",
"id": "8C1F3D2E-5B7A-4A61-9F0E-2B4C6D8E0A13",
"op": "upsert",
"hlc": "001791792000123-0000-3F2A9C1B",
"data": { "sku": "100101", "name": "Wireless access point AP-7", "unit": "Stk" }
}
Field Type Rules
entity string ^[a-z][a-z0-9_]*$, at most 64 characters
id string UUID, see above
op string upsert or delete
hlc string ^[0-9]{15}-[0-9A-F]{4}-[0-9A-F]{8}$, see Hybrid logical clock
data object or null An object for upsert, null for delete. Required for upsert; for delete it may be null or left out

A change that breaks these rules is rejected by a server with reason invalid_change. The schema is schemas/change.json; additional envelope fields are allowed and ignored.

  • Field names are snake_case.
  • A payload is one JSON object. Nested objects and arrays are allowed and are part of the payload; they are replaced as a whole with the record.
  • Arrays keep their order. Lists such as items, signatures and returns are ordered as written.

Every payload may contain fields this specification does not define, written by a newer app or by another system.

  • Servers MUST store and relay the complete payload, including unknown fields, without changing it.
  • A client that writes a new version of a record MUST keep the unknown fields of the version it is replacing. The reference app does this for top-level fields: it decodes the payload into its model, encodes the model again and adds every top-level key of the previous version that the new encoding lacks.
  • Unknown fields nested inside objects or array elements (for example an unknown key inside a loan item) are lost when the reference app rewrites the record. Writers that add fields SHOULD add them at the top level of the payload.

For a reader, an absent field and a field with value null mean the same thing: no value. The default listed for the field on its entity page applies.

For a writer, the difference matters because of the unknown-field rule above: a client that leaves a known field out of a new version cannot be told apart from an older client that did not know the field. Writers SHOULD therefore send null to clear a field rather than leaving it out.

Readers MUST accept payloads with missing optional fields and apply the documented defaults. A reader that fails on a missing optional field would make the record invisible to its user. The reference app decodes every payload leniently in this way; it skips (does not show) a record only when a field it requires is missing, or when any field it knows has a value of the wrong type (a string where a number belongs, an unparsable date). The required fields per entity are listed in each entity page and in the JSON Schemas.

Types are strict: numbers are JSON numbers, booleans are JSON true and false, ids and dates are strings. The reference server accepts some looser forms (numeric strings, "1" and "0" for booleans), but the reference app cannot read them and does not show the record. Writers MUST use the strict JSON types.

Strings are UTF-8 JSON strings. Free text may contain line breaks (\n). Empty strings are allowed where a field is optional, but writers SHOULD send null (or leave the field out) instead of an empty string; the reference app turns blank input into "no value".

Timestamps (booked_at, issued_at, signed_at, returned_at, due_at, created_at) are ISO 8601 / RFC 3339 date-time strings with a time zone, for example 2026-10-12T08:15:30Z.

  • The reference app writes UTC with whole seconds: 2026-10-12T08:15:30Z.
  • Writers MUST include seconds and a time zone (Z or an offset). The reference app cannot read 2026-10-12 or 2026-10-12T08:15Z and does not show a record that contains such a value, even in an optional field. The reference server accepts anything PHP can parse as a date, so it does not catch this.
  • Readers MUST accept an offset (+02:00) and fractional seconds (2026-10-12T08:15:30.123456Z). The reference app ignores fractional seconds.
  • Dates that mean a calendar day are stored as the start of that day in the time zone of the writing device. due_at of a loan due on 19 October 2026, written in Berlin, is 2026-10-18T22:00:00Z.
  • Licence expiry dates are plain dates, 2027-12-31 (see Organisation licence).

The hlc stamp also carries a time, but it is a version stamp and not a business date. Clients may show it as "last changed".

  • Quantities, prices, replacement values and penalty amounts are JSON numbers and are treated as IEEE 754 double precision values. Integers may be written without a fraction (4) or with one (4.0); both mean the same.
  • Quantities are in the unit of the article (unit, default Stk, German for pieces). Fractional quantities are allowed (2.5 metres of cable).
  • Money amounts are in the currency of the inventory (company_profile.currency, an ISO 4217 code). There is one currency per inventory; amounts carry no currency of their own. Prices are net amounts per unit.
  • The protocol does not round. Readers that compare quantities SHOULD use a tolerance; the reference app treats differences below 0.0005 as zero, hides stock rows whose absolute value is at most 0.0005, and formats money with the currency's usual number of decimals only for display.
  • The reference server copies the payload as text and keeps 1.0 and 1 apart; it projects quantities into its read model with three decimals.

Binary content (photos, logos, signatures, PDFs) is never embedded. A payload references it by the lowercase hexadecimal SHA-256 of its bytes, for example "image": "35a12c5c...". See Attachments for the keys that hold hashes.

Documents (delivery notes and loans) copy the data they print at the time of issue: the recipient's address, the article's item number, name and unit, the location code. A later change to the article or recipient does not change an issued document. Clients read the copies when they show a document and use the referenced ids only for navigation and for derived state.

Each entity has a JSON Schema (draft 2020-12) in schemas/. The schemas describe what a conforming writer sends: required fields are those the reference app needs to show a record, defaults are those readers apply. They allow additional properties everywhere. The reference server checks a smaller set of rules, listed on each entity page under "Server validation".