Conventions
The things that are true of every endpoint, so no reference page has to repeat them.
The envelope#
Every response — success or failure — is an object with data and meta. A list puts its array in data and its cursor in meta; a single record puts the object in data. An error replaces data with errors. Nothing is ever a bare array, which is what lets us add a field to meta without breaking a parser.
{ "data": [ { "id": 14868, "status": "requested" } ], "meta": { "message": "OK", "has_more": true, "next_cursor": "aWQ6MTQ4Njg", "limit": 25 }}Versioning#
The version is in the path: https://api.zinevu.com/api/public/v1. Inside v1 we only ever add — new fields, new endpoints, new values of an existing enum. Read a response as an open object rather than a closed one and a new field is a non-event. A removal or a rename would be v2.
The assistant endpoint speaks somebody else's protocol, whose version travels in a header rather than in the path — so it sits at /api/mcp, outside this prefix.
Ids and codes#
Two kinds of identifier, and which one an endpoint takes is deliberate:
idis ours. It is stable, numeric, and what/leads/{id}takes. It also orders the cursor.codeis yours. Price matrices are addressed by it, because your catalogue decides what a grid is called and you should not have to store an identifier of ours to update your own prices.article_numberis your ERP's. Offer items can be fetched by it directly, in any case, and it is copied onto every offer line it writes.uuidappears on leads and offers. It is the identifier customer-facing links carry; it is never a path parameter here.
Timestamps and time zones#
Everything we return is ISO 8601 with an offset. Send the same: a timestamp without an offset is read as Dutch local time rather than UTC — the same rule the portal uses, so a window saved through either surface lands on the same instant — but sending +02:00 or Z removes the question entirely.
Money and VAT#
price | A plain JSON number, never a string and never cents. 1176 is €1176,00. |
null | A cell with no price. A gap in a grid is not a free product, so it is never 0. |
currency | The account's currency, lowercase ISO 4217. null means EUR. |
vat_rate | A percentage on the line, not on the matrix: one grid can feed lines at different rates. |
total_includes_vat | Whether the total beside it is gross. Read it before you add anything up. |
Nulls, absences and empty lists#
A field we know nothing about is null and still present — we do not drop keys, so your parser can be strict. An empty list is [] with has_more: false, never a 404: "no leads changed since Tuesday" is a successful answer.
Filters#
Filters are query parameters, combined with AND, and an unknown one is ignored — with one deliberate exception. An unparseable updated_since is refused with 422, because a date filter that silently returns everything looks exactly like one that works, and you would only find out when the nightly sync started taking an hour.
Safe by default#
GET never changes anything. Writes carry their own budget and accept an idempotency key. Nothing in this API sends an e-mail to your customer — not creating a lead, not writing a price, not starting a promotion. Sending stays a deliberate act inside the portal.