Cooling & food processes

Cooling, freezing, reheating and transport — four collections recording a temperature transition, and what an unfinished one looks like.

A hot dish that cools slowly spends hours in the range where bacteria multiply, so the rule is to bring it down fast and to be able to show you did. Four collections record that kind of transition. Three of them are a transition in time — down, down further, back up — and the fourth is the same idea in space.

CollectionScopeWhat a record is
coolingcooling:readA hot product brought down to cold-storage temperature.
freezingfreezing:readA product taken down to frozen.
reheatingreheating:readA cold product brought back up to service temperature.
transporttransport:readA product moved from one place to another, with the temperature at each end.

All four share the snapshot envelope: id, deleted, capturedAt, receivedAt, sequence and the rest sit beside the data described below.

cooling

One cycle: a product, when it started and how hot it was, when it finished and how cold it was.

FieldTypeMeaning
timestampstring, requiredThe record's own stamp, epoch milliseconds as a string. Not the cycle's clock — that is the two dates below.
productstring, requiredWhat was cooled, as text.
beginDatestring, requiredWhen the cycle started, epoch milliseconds as a string.
beginTemperaturenumber, requiredThe temperature it started at.
endDatestring | nullWhen it finished. null while it has not.
endTemperaturenumber | nullThe temperature it finished at. null alongside endDate.
reminderIdstring | nullThe reminder the app attached to the cycle, when there was one.
userIdstring | nullThe staff member who recorded it, when the app recorded one.
{
  "id": "0e5d1a7c-9f24-4b13-8d60-c1a7f0b25e94",
  "collection": "cooling",
  "deleted": false,
  "capturedAt": "1789779612441",
  "receivedAt": "1789779910228",
  "sequence": "1",
  "data": {
    "timestamp": "1789772400000",
    "product": "Blanquette de veau",
    "beginDate": "1789772400000",
    "beginTemperature": 63.5,
    "endDate": "1789779600000",
    "endTemperature": 8.2,
    "userId": "2774953d-8d9b-4a68-8ec4-1209edd90777"
  }
}

product is a name, not a reference. There is no productId on these records, so you cannot join them to products without matching strings — and the strings are typed during service, in the language of the restaurant.

reminderId is opaque. No collection in this API resolves it. Carry it if it helps you group records; do not expect to look it up.

freezing

The same shape, for a product taken down to frozen rather than to fridge temperature. Field for field, it is cooling.

FieldTypeMeaning
timestampstring, requiredThe record's own stamp.
productstring, requiredWhat was frozen.
beginDatestring, requiredWhen the operation started.
beginTemperaturenumber, requiredThe temperature it started at.
endDatestring | nullWhen it finished, or null.
endTemperaturenumber | nullThe temperature it finished at, or null.
reminderIdstring | nullAs above.
userIdstring | nullWho recorded it.

Nothing in the record says which of the three it is: that is the collection it came from, and the envelope's collection field echoes it. A pipeline that merges all three into one table needs to keep that column.

reheating

A cold product brought back up. Same shape again, read in the other direction: beginTemperature is the low one and endTemperature the high one.

FieldTypeMeaning
timestampstring, requiredThe record's own stamp.
productstring, requiredWhat was reheated.
beginDatestring, requiredWhen it went in.
beginTemperaturenumber, requiredThe temperature it started at.
endDatestring | nullWhen it came out, or null.
endTemperaturenumber | nullThe temperature it reached, or null.
reminderIdstring | nullAs above.
userIdstring | nullWho recorded it.

An operation with no end looks like this:

{
  "id": "b4c8e21d-3a76-4f58-9c02-77ad5e1b8f30",
  "collection": "reheating",
  "deleted": false,
  "capturedAt": "1789783221907",
  "receivedAt": "1789783402115",
  "sequence": "2",
  "data": {
    "timestamp": "1789783200000",
    "product": "Gratin dauphinois",
    "beginDate": "1789783200000",
    "beginTemperature": 4.1,
    "endDate": null,
    "endTemperature": null,
    "userId": "2774953d-8d9b-4a68-8ec4-1209edd90777"
  }
}

transport

The same transition, in space rather than in time: where the product left from and where it arrived, with a temperature at each end.

FieldTypeMeaning
timestampstring, requiredThe record's own stamp.
productstring, requiredWhat was transported, as text.
departureLocationstring, requiredWhere it left from, as text.
departureTimestring, requiredWhen it left, epoch milliseconds as a string.
departureTemperaturenumber, requiredThe temperature on departure.
arrivalLocationstring | nullWhere it arrived. null while it has not.
arrivalTimestring | nullWhen it arrived, or null.
arrivalTemperaturenumber | nullThe temperature on arrival, or null.
reminderIdstring | nullAs above.
userIdstring | nullWho recorded it.
{
  "id": "f31b6a08-5c47-4de9-b1a5-6e0c9d247b82",
  "collection": "transport",
  "deleted": false,
  "capturedAt": "1789786944610",
  "receivedAt": "1789787101883",
  "sequence": "3",
  "data": {
    "timestamp": "1789785000000",
    "product": "Plats préparés — service traiteur",
    "departureLocation": "Cuisine centrale",
    "departureTime": "1789785000000",
    "departureTemperature": 3.4,
    "arrivalLocation": "Salle Montparnasse",
    "arrivalTime": "1789786800000",
    "arrivalTemperature": 5.9,
    "userId": "2774953d-8d9b-4a68-8ec4-1209edd90777"
  }
}

The two locations are free text, not areas and not addresses. They are whatever the person typed, and the arrival end is exactly as optional as the arrival temperature: one record can carry a departure and nothing else.

Working with these

A null end is two different things. endDate and endTemperature — and arrivalTime, arrivalTemperature and arrivalLocation on transport — are nullable, and the record does not tell you why. It may be an operation still running as you read it, or one nobody ever closed. Both look identical.

That matters the moment you compute a duration. The dates are strings, so endDate - beginDate subtracts fine on a closed record and, on an open one, coerces null to zero and hands you a large negative number instead of an error. Nothing throws; your average is simply wrong. Filter to closed records first, count the open ones separately, and decide for yourself at what age an open record stops being in progress and starts being abandoned — nothing in this API makes that call for you.

Compute on beginDate and endDate, not on timestamp. The dates are the operation's clock. timestamp is the record's own, and it is not the thing a duration is made of.

Order on capturedAt, not receivedAt. A tablet in a cold room uploads when it finds signal, so a cycle started at 09:00 can arrive at 14:00. The envelope page has the full story.

Absence proves nothing. A product with no cooling record may never have been cooled, or may be sitting on a device that has not uploaded yet. Do not turn a count of these into a compliance figure — see the caveat.

Last updated 2026-09-20.