Cleaning

The cleaning plan, the tasks actually done and the photographs proving it — three collections, and what a missing record does not prove.

Cleaning in the app is a plan and its proof. The restaurant writes down what has to be cleaned, where and how often; staff tick the tasks off as they do them; some of those tasks demand a photograph. Three collections, one per layer, and the join between them is where most of the work is.

CollectionScopeWhat a record is
cleaning-taskscleaning-tasks:readA line of the plan: what must be cleaned, where, how often, whether it needs a photograph.
cleaning-task-recordscleaning-task-records:readA task actually done — when, and by whom.
cleaning-task-picturescleaning-task-pictures:readThe photograph proving it was. Carries a file.

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

cleaning-tasks

The plan itself. These change rarely — a restaurant writes its plan once and edits it when the kitchen changes — so this is the collection you walk least often and cache longest.

FieldTypeMeaning
indexinteger, requiredThe task's position in the plan, as the app orders it.
namestring, requiredWhat has to be cleaned, in the restaurant's words.
descriptionstring | nullLonger instructions, when someone wrote them.
areaIdstring | nullThe area it belongs to. null on a restaurant that does not split itself into areas.
recurrenceinteger, requiredHow often the task comes round, as a plain integer.
requirePhotoProofboolean | nulltrue when the task demands a photograph. null means the app never set it.
isDeletedboolean, requiredThe app's own soft delete. Read the note below.
{
  "id": "c9a4f7e2-1b83-4d05-a6f1-90e3b7c4d218",
  "collection": "cleaning-tasks",
  "deleted": false,
  "capturedAt": "1789601204773",
  "receivedAt": "1789601399042",
  "sequence": "1",
  "data": {
    "index": 3,
    "name": "Nettoyage de la trancheuse",
    "description": "Démonter la lame, dégraisser, désinfecter.",
    "areaId": "7d2c0b61-4e8a-49f3-9c27-5a1b8e60d3f4",
    "recurrence": 1,
    "requirePhotoProof": true,
    "isDeleted": false
  }
}

recurrence carries no unit. It is an integer and the record does not say what it counts. Show it as the app shows it rather than rendering "every 1 days" from a guess.

isDeleted is not the envelope's deleted. A task the restaurant removed from its plan comes back with isDeleted: true and deleted: false: it is still a live record, describing a line that no longer applies. Filter on both, and keep the deleted ones around — old records still point at them.

cleaning-task-records

A task done. Small on purpose: which task, when, who.

FieldTypeMeaning
timestampstring, requiredWhen the task was done, epoch milliseconds as a string.
cleaningTaskIdstring, requiredThe cleaning-tasks record it satisfies.
userIdstring | nullThe staff member who did it, when the app recorded one.
{
  "id": "5f8b3d16-2c90-4a77-b4e8-31d0a9c65f7b",
  "collection": "cleaning-task-records",
  "deleted": false,
  "capturedAt": "1789775012558",
  "receivedAt": "1789775204910",
  "sequence": "2",
  "data": {
    "timestamp": "1789774800000",
    "cleaningTaskId": "c9a4f7e2-1b83-4d05-a6f1-90e3b7c4d218",
    "userId": "2774953d-8d9b-4a68-8ec4-1209edd90777"
  }
}

cleaningTaskId is required, so every record has a task. The reverse does not hold: a task can sit in the plan with no record against it for weeks, and that is the normal state of a weekly task on a Tuesday.

cleaning-task-pictures

The photograph. It carries a file, which makes it the one collection here you cannot consume from the records endpoint alone.

FieldTypeMeaning
timestampstring, requiredWhen the picture was taken, epoch milliseconds as a string.
cleaningTaskRecordIdstring | nullThe cleaning-task-records record it proves. Nullable — see below.
partnumber | nullUndocumented, and a decimal rather than a counter. Carry it through rather than interpreting it — the same caveat as on traceability labels.
groupIdstring | nullTies those pictures together. Records sharing a groupId belong to the same proof.
assetobject, requiredNames the file: objectKey and status always, plus contentType, byteLength and sha256 once it is uploaded.
{
  "id": "2d71c8a9-6e34-4b0f-8517-ac93e25d0b46",
  "collection": "cleaning-task-pictures",
  "deleted": false,
  "capturedAt": "1789775118330",
  "receivedAt": "1789775301774",
  "sequence": "3",
  "data": {
    "timestamp": "1789774860000",
    "cleaningTaskRecordId": "5f8b3d16-2c90-4a77-b4e8-31d0a9c65f7b",
    "part": 1,
    "groupId": "a03e5c88-71bd-4f92-b6d4-2e8f10c73a5b",
    "asset": {
      "objectKey": "cleaning-task-pictures/2d71c8a9/1.jpg",
      "status": "uploaded",
      "contentType": "image/jpeg",
      "byteLength": 812043,
      "sha256": "9f2a…"
    }
  }
}

The bytes are a second call, against the record's own id:

GET /v1/restaurants/{restaurantId}/collections/cleaning-task-pictures/records/{recordId}/assets

It answers with a list of signed URLs, each valid for fifteen minutes, each carrying its own authorisation so the download needs no header. Fetch the bytes now, store the bytes rather than the link, and call the endpoint again when you need a fresh URL. The rules — what appears, what sha256 is for, why contentType is worth reading — are on the collections page, and they are the same here.

asset.status tells you whether there are bytes to fetch. It is pending or uploaded. A pending asset is a photograph the device has announced and not yet finished sending: the record is real, the file is not, and the assets endpoint answers without it. Read the status before you treat an empty asset list as a photograph that was never taken.

cleaningTaskRecordId is nullable. A picture can exist without pointing at a record, so a join that assumes it is always there drops photographs on the floor. Count them separately rather than discarding them silently.

Joining the three

The shape you almost certainly want is one row per task done, with its plan entry and its photographs:

  • cleaning-task-records.cleaningTaskIdcleaning-tasks.id
  • cleaning-task-pictures.cleaningTaskRecordIdcleaning-task-records.id

Three walks, joined on your side. There is no endpoint that does it for you, and there is no filter by task or by date — you walk each collection whole and reconcile, as the envelope page describes.

Order on capturedAt, not receivedAt: a tablet in a cold room uploads when it finds signal, and a picture taken at 09:00 can arrive at 14:00 — after the record it belongs to, or before it.

What a missing record does not mean

A plan entry with no matching record is not proof the task was skipped. It may have been done on a device that has not uploaded yet, or done before capture was switched on for that restaurant. The same goes for a task marked requirePhotoProof: true with no picture against it: the photograph may be mid-upload, and an asset that has not finished uploading is absent rather than broken.

So do not print a cleaning completion rate from this API and call it compliance. What you can say honestly is what arrived, and when — see the completeness note before you put a number in front of anyone.

Last updated 2026-09-20.