Collections & records
The twenty-four record types beyond deliveries — the snapshot envelope they share, what a snapshot is, and how to walk one.
Deliveries are one curated resource, shaped by hand because goods-in was the first thing anyone asked for. Collections are the rest of the record: temperatures, cooling cycles, cleaning, fryer checks, labels, twenty-four of them, served through one generic shape.
Where the delivery endpoint gives you a designed object, a collection gives you the record as the app holds it, wrapped in an envelope that tells you when it was captured and whether it still exists. That trade is deliberate: it is what lets a new module reach this API the week it ships rather than the quarter after.
Each collection is a separate grant. A key reads
temperature-records because someone granted temperature-records:read, and
nothing else comes with it.
What you can read
GET /v1/restaurants/{restaurantId}/collections
GET /v1/restaurants/{restaurantId}/collections/{collection}/records?limit=…&cursor=…
GET /v1/restaurants/{restaurantId}/collections/{collection}/records/{recordId}
GET /v1/restaurants/{restaurantId}/collections/{collection}/records/{recordId}/assets
Start with the first one. It lists only the collections your key was granted, each with the scope that opened it, so you never have to guess:
{
"data": [
{
"collection": "temperature-records",
"readScope": "temperature-records:read",
"description": "Received shadow snapshots for temperature-records; not complete primary-store history.",
"stateKind": "received-shadow-snapshot",
"payloadVersion": 1
}
]
}
An empty data is not an error and not an outage: it is a key with no
collection grants. Most keys issued before this surface existed have exactly
that, and widening one is an email.
The envelope
Every record in every collection carries the same outer fields. Only data
changes shape.
| Field | Type | Meaning |
|---|---|---|
id | string | The record's identifier, stable and unique within the collection. |
restaurantId | string | The restaurant it belongs to. Echoes the path. |
collection | string | Which collection it came from. Echoes the path. |
data | object | absent | The record itself. A tombstone is the only case where it can be missing, so treat it as present everywhere else. |
deleted | boolean | true means a tombstone: the record was deleted in the app. |
capturedAt | string | When the device recorded the change, epoch milliseconds as a string. |
receivedAt | string | When this API received it. Later than capturedAt, sometimes by hours. |
sequence | string | A monotonically increasing position in the stream of changes. Compare two of them; do not do arithmetic on one. |
mutationId | string | The identifier of the change that produced this state. Idempotency key on our side; a deduplication key on yours. |
sourceVersion | string | null | The version stamp of the originating record, when it has one. |
payloadVersion | number | Currently always 1. It bumps if the meaning of data ever changes. |
stateKind | string | Always received-shadow-snapshot. Read the next section. |
capturedAt and receivedAt both matter. A tablet in a cold room with no
signal records at 09:00 and uploads at 14:00; ordering your own pipeline on
receivedAt keeps it correct, and reporting on capturedAt keeps it true.
What a snapshot is, and is not
stateKind says received-shadow-snapshot, and the wording is careful.
- It is the current state of a record, not a log of every edit. Read a record twice and you get what it looks like now, both times.
- It is what we received, not what the restaurant holds. A device that never uploaded a change means a record this API has never seen.
- It is not a backfill. A collection begins for a restaurant the day capture is switched on for it. Records created before that are in the app and not here.
So an absent record is genuinely ambiguous: never recorded, or recorded and not yet arrived. Do not build a figure that reads as an audit on top of it, and see the completeness note before you report a count to anyone.
Tombstones are the one case where absence is unambiguous. deleted: true with
no data means the record existed and was deleted, and it is the only way you
learn that something went away.
The twenty-four collections
Every one of these takes <collection>:read as its scope —
cooling needs cooling:read, and so on down the list.
| Collection | What a record is |
|---|---|
restaurants | The site itself: name, address, closing days, subscription and settings. |
users | The staff accounts that record checks, and which modules they use. |
areas | The zones a restaurant is divided into — kitchen, cold room, bar. |
equipment | Fridges, freezers and cold-chain units, with their min/max thresholds. |
sensors | The wireless probes, by MAC address, and the equipment each one watches. |
temperature-records | A temperature a person took on a piece of equipment, with the shift and any corrective action. |
temperature-readings | A temperature a sensor reported on its own, unattended. |
suppliers | Who delivers, with contact methods and account number. |
products | The products received and used. |
preparations | In-house preparations, with shelf life and allergens. |
cleaning-tasks | The cleaning plan: each task, its area, its recurrence, whether it demands a photograph. |
cleaning-task-records | A cleaning task actually done — when, and by whom. |
cleaning-task-pictures | The photograph proving it was. Carries a file; see the assets section below. |
cooling | A cooling cycle: product, start and end temperature, start and end time. |
freezing | A freezing operation, same shape as cooling. |
reheating | A reheating operation, same shape as cooling. |
transport | A transported product, with departure and arrival location, time and temperature. |
fryer-equipment | The fryers. |
fryer-checks | An oil quality check and what was decided — filtered, changed, left. |
cooking-equipment | Ovens and cooking units, with their thresholds. |
cooking-temperature-records | A cooking temperature, taken by a person, on a cooking unit. |
surface-analyses | A surface swab: what was tested, whether it passed, the action plan if not. |
traceability-labels | A printed traceability label. Carries a file; see the assets section below. |
drive-files | A document filed in the restaurant's drive. Carries a file; see the assets section below. |
Field-by-field shapes for every data object are in the
OpenAPI document, which is generated
from the running service — it is the one description that cannot drift from what
is deployed.
Walking a collection
GET /v1/restaurants/{restaurantId}/collections/{collection}/records?limit=100
| Parameter | Required | Rules |
|---|---|---|
limit | no | 1 to 100. Defaults to 50. |
cursor | no | The nextCursor of the previous page, verbatim. |
There is no time range here, unlike deliveries: you walk a collection, not a window of it.
A walk is a consistent snapshot. The first page pins the position of the
stream, and every later page is answered as of that same position. Records
written while you are paging do not shift the pages under you and do not appear
mid-walk — you see them on your next walk. Ordering is by sequence, ascending.
The cursor is bound to the restaurant, the collection and the limit. Change the page size mid-walk and it is rejected: pick a limit, keep it for the whole walk.
async function* records({ apiKey, restaurantId, collection }) {
const base = `https://api.backresto.com/v1/restaurants/${restaurantId}/collections/${collection}/records`;
let cursor = null;
do {
const url = new URL(base);
url.searchParams.set('limit', '100');
if (cursor !== null) {
url.searchParams.set('cursor', cursor);
}
const response = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` }
});
if (!response.ok) {
throw new Error(`BackResto ${response.status}`);
}
const page = await response.json();
yield* page.data;
cursor = page.nextCursor;
} while (cursor !== null);
}
Keeping a copy up to date
There is no since parameter today. A refresh is another walk, and you
reconcile with what you already hold:
- Key on
idwithin a collection, and overwrite when thesequenceyou receive is higher than the one you stored. - Honour tombstones.
deleted: trueis the delete instruction; applying it is the only way your copy stops diverging. - Walk at a sane hour. A full collection at
limit=100is a handful of requests for a single restaurant, and the budget is 300 a minute — but several hundred restaurants on the same cron minute is a spike you own.
If an incremental cursor would change what you can build, say so. It is a small change to a stream that is already ordered — the reason it does not exist is that nobody has needed it yet.
Assets
Three collections carry a file: cleaning-task-pictures,
traceability-labels and drive-files. The record names it in data.asset;
the bytes come from the assets endpoint.
GET /v1/restaurants/{restaurantId}/collections/{collection}/records/{recordId}/assets
{
"data": [
{
"id": "a17c93be4f02",
"contentType": "application/pdf",
"byteLength": 184320,
"sha256": "9f2a…",
"uploadedAt": "1789000000000",
"url": "https://…?X-Amz-Signature=…",
"urlExpiresAt": "1789000900000"
}
]
}
It answers much as delivery photographs do: a list of signed URLs, each valid for fifteen minutes, each carrying its own authorisation so the download needs no header. Two differences are worth knowing.
These are files, not only pictures. A cleaning proof is a photograph, but a
traceability label or a drive file can be a PDF, a CSV, a spreadsheet or a Word
document — read contentType rather than assuming an image, and do not rename
everything .jpg on the way in.
sha256 is there so you can skip work. It is the digest of the bytes: if it
matches something you already stored, you do not need to download it again.
Only assets whose upload completed and was verified appear — one still in flight is absent rather than broken, and the same is true once the parent record is deleted.
The rest of the advice is identical, and worth repeating because the failure is quiet: fetch the bytes now, store the bytes rather than the link, and call the endpoint again when you need a fresh URL.
The failures you will meet
403 — your key reaches the restaurant but not with that collection's
scope. The collections endpoint is the cheap way to find out which ones it does
have.
404 — no grant for that restaurant at all, or no such record. The two are
indistinguishable on purpose.
400 — a collection outside the twenty-four above, or a cursor that does
not belong to this restaurant, collection and limit.
All three are problem documents with a requestId.