Records and payloads
Records
Section titled “Records”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 types
Section titled “Entity types”| 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-000000000001are valid. - Ids are unique per entity type: an
articleand alocationmay 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.
The change envelope
Section titled “The change envelope”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.
Payload conventions
Section titled “Payload conventions”Field names and structure
Section titled “Field names and structure”- 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,signaturesandreturnsare ordered as written.
Unknown fields
Section titled “Unknown fields”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.
Absent and null
Section titled “Absent and null”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.
Lenient reading
Section titled “Lenient reading”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
Section titled “Strings”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".
Dates and times
Section titled “Dates and times”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 (
Zor an offset). The reference app cannot read2026-10-12or2026-10-12T08:15Zand 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_atof a loan due on 19 October 2026, written in Berlin, is2026-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".
Numbers, money and quantities
Section titled “Numbers, money and quantities”- 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, defaultStk, German for pieces). Fractional quantities are allowed (2.5metres 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.0and1apart; it projects quantities into its read model with three decimals.
Attachments in payloads
Section titled “Attachments in payloads”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.
Copies instead of joins
Section titled “Copies instead of joins”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.
JSON Schemas
Section titled “JSON Schemas”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".