# Collections & records

> The twenty-six record collections, the snapshot envelope they share, and how to walk one.

Source: https://developers.backresto.com/en/docs/collections

A **collection** is one record type: `temperature-records`, `cooling`,
`deliveries`, and twenty-three others. Every one is read the same way,
through the same four endpoints, and every record comes back in the same
envelope: the fields around `data` that say when it was captured and whether
it still exists.

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

```http
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:

```json
{
  "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](https://developers.backresto.com/en/docs/access).

## 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. [Deliveries](https://developers.backresto.com/en/docs/deliveries) are the one exception: those two
  collections carry the history the app still held when they opened.

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](https://developers.backresto.com/en/docs/support) 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.

## How long records stay

Operational records stay here for **90 days**, counted from when they happened
in the kitchen (`data.timestamp`), not from when we received them. After that, a
nightly job moves them to a locked cold archive, kept for five years, and they
stop appearing in every read: lists, single records and their assets.

| Kept 90 days | Kept for the life of the account |
| --- | --- |
| `temperature-readings`, `temperature-records`, `cooking-temperature-records`, `cleaning-task-records`, `cleaning-task-pictures`, `cooling`, `freezing`, `reheating`, `transport`, `fryer-checks`, `surface-analyses`, `traceability-labels`, `deliveries`, `delivery-pictures` | `restaurants`, `users`, `areas`, `equipment`, `sensors`, `cooking-equipment`, `suppliers`, `products`, `preparations`, `cleaning-tasks`, `fryer-equipment`, `drive-files` |

**An archived record is not a tombstone**, and it is the one exception to the tombstone rule above. It leaves without a `deleted: true`,
because nobody deleted it: it moved. If you keep a copy, keep what you already
hold, and never treat a record missing from a walk as a delete. For anything
older than 90 days, your copy is the source; an inspection that needs our
archive goes through [support](https://developers.backresto.com/en/docs/support).

## The twenty-six collections

Every one takes `<collection>:read` as its scope, so `cooling` needs
`cooling:read`, and so on. They are documented by domain, field by field, on
the pages below.

| Page | Collections |
| --- | --- |
| [Temperatures](https://developers.backresto.com/en/docs/temperatures) | `temperature-records`, `temperature-readings`, `cooking-temperature-records` |
| [Deliveries](https://developers.backresto.com/en/docs/deliveries) | `deliveries`, `delivery-pictures` |
| [Traceability labels](https://developers.backresto.com/en/docs/traceability-labels) | `traceability-labels` |
| [Cleaning](https://developers.backresto.com/en/docs/cleaning) | `cleaning-tasks`, `cleaning-task-records`, `cleaning-task-pictures` |
| [Fryers](https://developers.backresto.com/en/docs/fryers) | `fryer-equipment`, `fryer-checks` |
| [Cooling & food processes](https://developers.backresto.com/en/docs/food-processes) | `cooling`, `freezing`, `reheating`, `transport` |
| [Surface analyses](https://developers.backresto.com/en/docs/surface-analyses) | `surface-analyses` |
| [Documents](https://developers.backresto.com/en/docs/drive-files) | `drive-files` |
| [Catalogue](https://developers.backresto.com/en/docs/catalogue) | `suppliers`, `products`, `preparations` |
| [Facilities & people](https://developers.backresto.com/en/docs/facilities) | `restaurants`, `users`, `areas`, `equipment`, `sensors`, `cooking-equipment` |

Temperatures inside a delivery are always Celsius: a reading taken in
Fahrenheit is converted when it arrives, and every `temperatureRecords[].unit`
says `"C"`.

Start with **Facilities & people** if you are reading any of the others:
equipment thresholds and areas are what turn a bare measurement into something
you can judge.

The [OpenAPI document](https://api.backresto.com/openapi.json) carries the same
shapes, generated from the running service. When a page here and that document
disagree, the document is right and we have a page to fix.

## Walking a collection

```http
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: you walk a collection, not a window of it. To read a
week of deliveries, walk `deliveries` and keep the records whose
`data.timestamp` falls in that week.

**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.

```js
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](https://developers.backresto.com/en/docs/rate-limits) 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

Four collections carry a file: `cleaning-task-pictures`,
`delivery-pictures`, `traceability-labels` and `drive-files`. The record names it in `data.asset`;
the bytes come from the assets endpoint.

```http
GET /v1/restaurants/{restaurantId}/collections/{collection}/records/{recordId}/assets
```

```json
{
  "data": [
    {
      "id": "a17c93be4f02",
      "contentType": "application/pdf",
      "byteLength": 184320,
      "sha256": "9f2a…",
      "uploadedAt": "1789000000000",
      "url": "https://…?X-Amz-Signature=…",
      "urlExpiresAt": "1789000900000"
    }
  ]
}
```

It answers with a list of signed URLs, each valid for **fifteen minutes**, and
every URL in one response shares the same expiry. Each one carries its own
authorisation, so the download needs no `Authorization` header, which also
means the link is a bearer credential in itself: for fifteen minutes anybody
holding it can read that file. Two things are worth knowing.

**These are files, not only pictures.** A cleaning proof or a delivery
photograph is a picture, 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 worth repeating because the failure is quiet: fetch
the bytes now, store the bytes rather than the link, never put the URL
somewhere it outlives its purpose (an email, a ticket, a long-lived cache), and
call the endpoint again when you need a fresh URL. It is cheap, and it re-checks
the grant.

## 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-six above, or a cursor that does
not belong to this restaurant, collection and limit.

All three are [problem documents](https://developers.backresto.com/en/docs/errors) with a `requestId`.
