Facilities & people
The site, its staff, its zones and its machines — the fixed map every temperature reading hangs off.
These are the things that do not move: the restaurant itself, the people who work in it, the zones it is divided into, and the machines that hold food at a temperature. None of it is a check.
It still decides what the checks mean. A temperature is a number with no verdict attached until you read the thresholds off the equipment it was taken on, and a reading belongs to a part of the building only because the equipment says which area it stands in. Get this page wrong and every figure downstream is wrong quietly.
| Collection | Scope | What a record is |
|---|---|---|
restaurants | restaurants:read | The site itself: name, address, closing days, settings. |
users | users:read | A staff account that records checks. |
areas | areas:read | A zone the site is divided into — kitchen, cold room, bar. |
equipment | equipment:read | A fridge, a freezer, a cold-chain unit, with its thresholds. |
sensors | sensors:read | A wireless probe, by MAC address. |
cooking-equipment | cooking-equipment:read | An oven or other cooking unit, with its thresholds. |
All six share the snapshot envelope: id, deleted,
capturedAt, receivedAt, sequence and the rest sit beside the data
described below.
restaurants
The site your key is scoped to, as a record. One per restaurant, so this collection is usually a single page.
| Field | Type | Meaning |
|---|---|---|
name | string, required | The site's name, as it is displayed in the app. |
address | string, required | The postal address, as one string. |
closingDays | string[], required | The days the site is shut. Plain strings, not constrained by the schema. |
exceptionalClosures | object[] | One-off closures, each an object with from and to. |
detailedAddress | object | null | The address broken into parts, when the app holds it that way. |
deliveryAddress | string | null | Where goods are delivered, when it differs from address. |
deliveryDetailedAddress | object | null | The same, in parts. |
preferredLanguage | string | null | The language the restaurant works in. |
multiArea | boolean | null | Whether the site is split into more than one area. |
settings | object | null | App settings. The shape is the app's and it moves. |
{
"id": "e5a1d3c9-7b40-4a28-9df6-18c0b2e46a75",
"collection": "restaurants",
"deleted": false,
"capturedAt": "1789300481002",
"receivedAt": "1789300489517",
"sequence": "7",
"data": {
"name": "Le Comptoir de Nanterre",
"address": "12 rue des Anciennes Mairies, 92000 Nanterre",
"closingDays": ["sunday"],
"preferredLanguage": "fr",
"multiArea": true
}
}
The example shows the operational half only. Alongside it the record carries
the commercial one: subscription, subscriptions, companyName,
billingAddress, billingEmail, tvaNumber, trialEndDate, customerId,
discount. They exist, they are in the OpenAPI document, and this page will
not talk you through them.
Read the operational fields and leave the commercial ones alone. They describe the restaurant's contract with us, not its kitchen. They are not a billing API, they change for reasons that have nothing to do with food safety, and an integration that renders a customer's subscription state back at them is a support ticket waiting to happen.
closingDays is a list of plain strings. The schema puts no shape on them,
so read them and show them; do not switch on them and do not assume a
particular spelling or case.
users
The staff accounts that record checks. This is who a
temperature record's userId points at.
| Field | Type | Meaning |
|---|---|---|
firstName | string, required | Given name, as entered. |
lastName | string, required | Family name, as entered. |
email | string, required | The account's email address. |
phoneNumber | string | null | A contact number, when one was given. |
modules | string[] | Which parts of the app this person uses. Free strings. |
preferredNotificationTime | string | null | When this person asked to be reminded. |
isChildren | boolean | null | An internal flag. The schema gives it no meaning; do not build on it. |
isDeleted | boolean | null | The app's own retirement flag — and nullable here, unlike elsewhere. |
{
"id": "2774953d-8d9b-4a68-8ec4-1209edd90777",
"collection": "users",
"deleted": false,
"capturedAt": "1789315002664",
"receivedAt": "1789315010218",
"sequence": "31",
"data": {
"firstName": "Amina",
"lastName": "Berthier",
"email": "amina.berthier@example.test",
"phoneNumber": null,
"modules": ["temperatures", "cleaning"],
"isDeleted": false
}
}
This is personal data. email, phoneNumber and the two name fields
identify a real person, and they are in this API for one reason: a food-safety
record has to say who performed the check. That is the only job they have here.
So store the id and resolve a display name when you need one. Do not copy
contact details into your own tables, your logs, your analytics events or a
third-party tool because they happened to be in the payload. The restaurant is
the data controller for their staff, and every copy you make is one they now
have to account for.
isDeleted is nullable on this collection where it is required elsewhere, so
treat a missing flag as "not retired" rather than letting it fall through as
false-ish by accident. The difference between it and the envelope's deleted
is explained on the catalogue page.
areas
A zone of the restaurant. Two fields, and far more important than two fields suggest.
| Field | Type | Meaning |
|---|---|---|
name | string, required | The zone as the restaurant names it — kitchen, cold room, bar. |
isDeleted | boolean, required | The app's retirement flag. See the catalogue page. |
{
"id": "9c2b17ae-4d80-4f55-b3e1-0a6c8f4d2b19",
"collection": "areas",
"deleted": false,
"capturedAt": "1789301120440",
"receivedAt": "1789301126871",
"sequence": "12",
"data": {
"name": "Chambre froide",
"isDeleted": false
}
}
areaId is how everything gets grouped by zone. Equipment, cooking
equipment, fryers and cleaning tasks all carry one, and it is the only link
between a reading and a part of the building. There is no
areaId on the records themselves: you get there through the equipment.
areaId is nullable everywhere it appears. A fridge with no area is normal —
it means nobody assigned it, usually on a single-room site where multiArea is
false. Group those under "unassigned" rather than dropping them.
equipment
Fridges, freezers and other cold-chain units. The record that decides whether a temperature was acceptable.
| Field | Type | Meaning |
|---|---|---|
index | integer, required | The position the restaurant gave it in its own list. Display order, not an identifier. |
type | string, required | What kind of unit it is. An unconstrained string — read it, do not switch on it. |
name | string, required | The unit as the kitchen calls it. |
min | number | null | The lowest acceptable temperature. |
max | number | null | The highest acceptable temperature. |
areaId | string | null | The area it stands in. |
equipmentSensorId | string | null | The sensor fitted to it, when there is one. |
state | string | null | The unit's current state, as an unconstrained string. |
outOfRangeCount | number | null | A count of out-of-range readings that the app maintains. |
isDeleted | boolean, required | The app's retirement flag. |
{
"id": "84ce5c13-3237-40d4-901a-cbba59a6406f",
"collection": "equipment",
"deleted": false,
"capturedAt": "1789302455901",
"receivedAt": "1789302461330",
"sequence": "58",
"data": {
"index": 2,
"type": "freezer",
"name": "Congélateur bas",
"min": -22,
"max": -18,
"areaId": "9c2b17ae-4d80-4f55-b3e1-0a6c8f4d2b19",
"equipmentSensorId": "b31f7d64-0e29-42ca-9a57-51d3c8e07f28",
"state": "ok",
"outOfRangeCount": 0,
"isDeleted": false
}
}
min and max are the only verdict you get. A
temperature record carries a value, a time and the
equipment it was taken on — nothing that says whether it was fine. -14.4 °C
is a crisis in a fridge and a normal Tuesday in a freezer, and this record is
what tells the two apart. Every compliance figure you build joins here.
They are also nullable. When min or max is absent you have no threshold,
and the honest answer for that reading is "unknown", not "in range". Say so in
your interface rather than defaulting to green.
Thresholds are the current ones, not the ones that applied. The snapshot
gives you today's min and max; a reading from March was judged against
whatever was set in March, and the restaurant can change them at any time. If
you re-evaluate old readings against today's numbers — which is all this API
lets you do — label the result as your recalculation, not as what the kitchen
saw.
type and state are unconstrained strings. Both are single free-text
fields in the schema with no list behind them. Display them, group on them if
you must, and never let an unrecognised value fall through a switch into
silence.
outOfRangeCount is the app's number, not yours. The schema does not say
what window it covers, and you cannot rebuild it from the snapshots here.
Treat it as a signal from the app, and if you need a count you can defend,
compute your own from the readings and thresholds and label it as yours.
equipmentSensorId points from the equipment to its sensor. The sensor record
points back the other way. Both sides are nullable, and neither is guaranteed
to be filled in just because the other is.
sensors
The wireless probes. Two fields, both optional.
| Field | Type | Meaning |
|---|---|---|
macAddress | string | null | The probe's hardware address — how you tell one physical device from another. |
sensorEquipmentId | string | null | The equipment this sensor watches. |
{
"id": "b31f7d64-0e29-42ca-9a57-51d3c8e07f28",
"collection": "sensors",
"deleted": false,
"capturedAt": "1789302501764",
"receivedAt": "1789302509002",
"sequence": "61",
"data": {
"macAddress": "F4:12:9D:3A:77:0B",
"sensorEquipmentId": "84ce5c13-3237-40d4-901a-cbba59a6406f"
}
}
Note the two field names, which are mirror images and easy to transpose:
equipment carries equipmentSensorId, a sensor carries sensorEquipmentId.
Read the wrong one and you get undefined with no error.
There is no name here, no battery level and no last-seen time. What a sensor
reported is on temperature-readings, and those are keyed
by equipmentId rather than by the sensor — so a probe that has gone quiet
looks exactly like one that had nothing to report.
cooking-equipment
Ovens, holding cabinets and the rest of the hot side. Same idea as equipment,
with the cold-chain machinery stripped out.
| Field | Type | Meaning |
|---|---|---|
index | integer, required | Display order in the restaurant's own list. |
type | string, required | What kind of unit it is. Unconstrained string. |
name | string, required | The unit as the kitchen calls it. |
min | number | null | The lowest acceptable temperature. |
max | number | null | The highest acceptable temperature. |
areaId | string | null | The area it stands in. |
isDeleted | boolean, required | The app's retirement flag. |
{
"id": "3d90f4c8-62b7-4e13-8a05-ff71d6b9c204",
"collection": "cooking-equipment",
"deleted": false,
"capturedAt": "1789303880115",
"receivedAt": "1789303887640",
"sequence": "64",
"data": {
"index": 1,
"type": "oven",
"name": "Four à sole",
"min": 63,
"max": 260,
"areaId": "9c2b17ae-4d80-4f55-b3e1-0a6c8f4d2b19",
"isDeleted": false
}
}
No state, no outOfRangeCount, no sensor: cooking units are read by a
person, so there is nothing watching them between checks. Their readings are
cooking-temperature-records, which point here with
cookingEquipmentId — a different field name from the cold-chain one, and the
usual place a client silently reads nothing.
Reading the map
Everything on this page is small, slow-moving and needed before any other collection makes sense.
- Load it first and hold it. Walk
areas,equipment,cooking-equipmentandsensorsbefore you walk any record collection, and resolve names and thresholds from your own copy rather than per record. - Refresh it anyway. Equipment gets renamed, thresholds get corrected, a fridge moves to another area. A copy taken once and never walked again drifts without ever failing.
- Keep ids, not copies — especially for people. Store
userIdand resolve a name at render time. See the personal-data note above. - Both delete flags, everywhere. The envelope's
deletedanddata'sisDeletedare different things, and the catalogue page explains which is which.