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.

FieldTypeMeaning
idstringThe record's identifier, stable and unique within the collection.
restaurantIdstringThe restaurant it belongs to. Echoes the path.
collectionstringWhich collection it came from. Echoes the path.
dataobject | absentThe record itself. A tombstone is the only case where it can be missing, so treat it as present everywhere else.
deletedbooleantrue means a tombstone: the record was deleted in the app.
capturedAtstringWhen the device recorded the change, epoch milliseconds as a string.
receivedAtstringWhen this API received it. Later than capturedAt, sometimes by hours.
sequencestringA monotonically increasing position in the stream of changes. Compare two of them; do not do arithmetic on one.
mutationIdstringThe identifier of the change that produced this state. Idempotency key on our side; a deduplication key on yours.
sourceVersionstring | nullThe version stamp of the originating record, when it has one.
payloadVersionnumberCurrently always 1. It bumps if the meaning of data ever changes.
stateKindstringAlways 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.

CollectionWhat a record is
restaurantsThe site itself: name, address, closing days, subscription and settings.
usersThe staff accounts that record checks, and which modules they use.
areasThe zones a restaurant is divided into — kitchen, cold room, bar.
equipmentFridges, freezers and cold-chain units, with their min/max thresholds.
sensorsThe wireless probes, by MAC address, and the equipment each one watches.
temperature-recordsA temperature a person took on a piece of equipment, with the shift and any corrective action.
temperature-readingsA temperature a sensor reported on its own, unattended.
suppliersWho delivers, with contact methods and account number.
productsThe products received and used.
preparationsIn-house preparations, with shelf life and allergens.
cleaning-tasksThe cleaning plan: each task, its area, its recurrence, whether it demands a photograph.
cleaning-task-recordsA cleaning task actually done — when, and by whom.
cleaning-task-picturesThe photograph proving it was. Carries a file; see the assets section below.
coolingA cooling cycle: product, start and end temperature, start and end time.
freezingA freezing operation, same shape as cooling.
reheatingA reheating operation, same shape as cooling.
transportA transported product, with departure and arrival location, time and temperature.
fryer-equipmentThe fryers.
fryer-checksAn oil quality check and what was decided — filtered, changed, left.
cooking-equipmentOvens and cooking units, with their thresholds.
cooking-temperature-recordsA cooking temperature, taken by a person, on a cooking unit.
surface-analysesA surface swab: what was tested, whether it passed, the action plan if not.
traceability-labelsA printed traceability label. Carries a file; see the assets section below.
drive-filesA 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
ParameterRequiredRules
limitno1 to 100. Defaults to 50.
cursornoThe 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 id within a collection, and overwrite when the sequence you receive is higher than the one you stored.
  • Honour tombstones. deleted: true is the delete instruction; applying it is the only way your copy stops diverging.
  • Walk at a sane hour. A full collection at limit=100 is 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.

Last updated 2026-09-19.