Push
A client sends its local changes with POST /lve/v1/sync/push. The server judges every change on its own and answers with one result per change.
Request
Section titled “Request”POST /lve/v1/sync/push HTTP/1.1Authorization: Bearer 7|kM2pQ9xV3rT8wZ1yB6nH4jL0sD5fG7aC2eR9tY1u3f9a1c07Content-Type: application/jsonAccept-Language: en{ "device_id": "3F2A9C1B-7E4D-4B2A-9C1E-5D8F0A2B4C6E", "changes": [ { "entity": "location", "id": "2B0E6C1A-7D44-4F1B-9C3E-5A8D2F6B1C07", "op": "upsert", "hlc": "001791792000123-0000-3F2A9C1B", "data": { "code": "N-A01", "name": "Shelf A01" } }, { "entity": "movement", "id": "77AA0C41-2E9B-4F7D-8B1A-5D3C2E1F0A99", "op": "upsert", "hlc": "001791792000123-0002-3F2A9C1B", "data": { "type": "receipt", "article_id": "8C1F3D2E-5B7A-4A61-9F0E-2B4C6D8E0A13", "to_location_id": "2B0E6C1A-7D44-4F1B-9C3E-5A8D2F6B1C07", "quantity": 20, "booked_at": "2026-10-12T08:00:00Z", "booked_by": "Erika Mustermann" } } ]}| Field | Type | Rules |
|---|---|---|
device_id |
string | Required, at most 64 characters. The device.id the client signed in with. The server records it with every applied change. |
changes |
array | Required (may be empty), at most 500 entries. Change envelopes in the order they were made. |
A request with more than 500 changes is refused as a whole with 422 and too_many_changes; nothing is stored. A missing device_id or a changes value that is not an array gives 422 and validation_failed. A body that is not JSON gives 400 and malformed_request.
Clients send their outbox oldest first, in batches of at most 500, and upload the attachments a batch references before the batch (see Attachments).
Response
Section titled “Response”{ "results": [ { "id": "2B0E6C1A-7D44-4F1B-9C3E-5A8D2F6B1C07", "entity": "location", "status": "applied" }, { "id": "77AA0C41-2E9B-4F7D-8B1A-5D3C2E1F0A99", "entity": "movement", "status": "ignored" } ]}results has exactly one entry per change, at the same index. Each entry:
| Field | Type | Meaning |
|---|---|---|
id |
string or null | id of the change exactly as sent (not normalised); null when the change had no string id |
entity |
string or null | entity of the change as sent; null when the change had no string entity |
status |
string | applied, ignored or rejected |
reason |
string | Only for rejected, see below |
message |
string | Only for rejected: text for the user in the requested language |
Status
Section titled “Status”| Status | Meaning | Client action |
|---|---|---|
applied |
The change won and is now the current version. It is in the change log. | Remove from the outbox |
ignored |
The server already has this version, a newer one, or (for a movement) any version with this id. Nothing was stored. | Remove from the outbox |
rejected |
The server refuses the change. Nothing was stored. | Keep, mark as rejected, show message |
Rejection reasons
Section titled “Rejection reasons”The server checks in this order and reports the first failure:
| Reason | Cause | Example message (en) |
|---|---|---|
invalid_change |
The change is not a JSON object | The change must be a JSON object. |
invalid_change |
entity missing, not a string, longer than 64 characters or not matching ^[a-z][a-z0-9_]*$ |
The entity may only contain lower-case letters, digits and underscores and must start with a letter. |
invalid_change |
id missing or not a UUID |
The ID field must be a valid UUID. |
invalid_change |
op not upsert or delete |
The selected operation is invalid. |
invalid_change |
hlc missing or not matching the stamp pattern |
The version stamp must look like 001791792000123-0000-3F2A9C1B. |
invalid_change |
upsert without an object in data |
An upsert needs data as JSON object. |
invalid_change |
delete with data other than null |
A delete must not carry data. |
immutable |
delete of a movement |
Movements cannot be deleted. Correct a wrong booking with a counter-booking. |
invalid_payload |
data of a known entity type breaks the server's payload rules |
The SKU field is required. |
locked |
Change to a document_template whose stored version has locked: true |
Your company sets this template on the server. It cannot be changed in the app. |
message for invalid_change and invalid_payload names the first failing field. The payload rules of the reference server are listed per entity under "Server validation" (article and so on). Unknown entity types are never checked.
The set of reasons is closed in version 1. A client that receives an unknown reason treats it like invalid_payload.
Processing rules
Section titled “Processing rules”- Independent changes. A rejected change does not affect the others. The reference server stores each change in its own database transaction; when the server fails in the middle of a batch (
5xx), the changes before the failure stay stored and the client resends the whole batch, gettingignoredfor those. - Order. Changes are processed in request order. Two changes for the same record in one batch are merged one after the other, so the later stamp wins as usual.
- Idempotency. Pushing the same change again is answered
ignored, because its stamp is not greater than the stored one. Clients resend a batch after a timeout without risk. - Concurrency. Two devices pushing the same record at the same time are serialised by the server; the merge decision and the write happen atomically. The reference server serialises all writes to its change log with one row lock (see Pull).
- No semantic checks. The server does not check references (an article id that does not exist yet), stock levels or uniqueness of item numbers. Records arrive in any order and offline devices may legitimately create duplicates.
- Payload storage. The server stores
dataas it was sent, apart from re-encoding the JSON (key order and number formats are kept,1.0stays1.0). Unknown fields are kept. - Id and stamp normalisation. The reference server stores record ids and stamps in upper case. Pulled changes therefore show the upper-case form.
Validation errors versus rejections
Section titled “Validation errors versus rejections”A malformed request as a whole is answered with an HTTP error and no results. A malformed change inside a well-formed request is answered with 200 and a rejected result. Clients therefore check the HTTP status first and the per-change results second.
| Status | Code | Meaning |
|---|---|---|
200 |
Results for every change | |
400 |
malformed_request |
Body is not JSON |
401 |
unauthenticated |
Token missing or revoked |
422 |
too_many_changes |
More than 500 changes |
422 |
validation_failed |
device_id or changes missing or of the wrong type |
5xx |
server_error |
Retry the batch later |
The test vector test-vectors/push.json shows a batch with every outcome and the exact answer of the reference server.