# Backfill and exit times

> Re-send a past range of a service day after an error, and send when each party leaves so the live view can estimate the floor while your app is offline.

Two additions in contract v1.7, for every app type. A **backfill** re-sends the history of a time range, so entries an error left out still reach the stored day and its replay. An **exit time** on a check-in says when that party leaves, so the live view can estimate the floor while your app is silent.

## Backfill

`POST https://floorplancraft.com/api/live/backfill`, signed exactly as a push (see [Signing requests](https://floorplancraft.com/docs/signing-requests.md)) but with this path, so a push's signature never passes here. It shares the push's refusals: signature failures, `402`, `403`, `404`, `413`, `429` and `503`.

```json
{
  "restaurant": "Example Play Centre",
  "serviceDay": { "start": "2026-11-06T00:00:00+05:00", "end": "2026-11-07T00:00:00+05:00" },
  "from": "2026-11-06T09:00:00+05:00",
  "to": "2026-11-06T15:30:00+05:00",
  "states": [
    { "at": "2026-11-06T09:00:00+05:00", "tables": [ { "label": "Soft play", "room": "Play hall", "capacity": 40, "state": "seated", "covers": 12, "openMinutes": null, "server": null } ], "stations": [] },
    { "at": "2026-11-06T09:07:12+05:00", "tables": [ { "label": "Soft play", "room": "Play hall", "capacity": 40, "state": "seated", "covers": 15, "openMinutes": null, "server": null } ], "stations": [] }
  ],
  "events": [
    { "id": "checkin:T-58213", "at": "2026-11-06T09:03:41+05:00", "type": "seated", "label": "Soft play", "detail": "3 covers", "exitAt": "2026-11-06T10:03:41+05:00" }
  ]
}
```

| Field | Rule |
| --- | --- |
| `serviceDay` | As in a push: start before end, at most 36 hours. Its date in the venue's time zone is the day being replaced. |
| `from`, `to` | The range to replace, inside the service day: `start <= from < to <= end`. A whole day is `from` = start, `to` = end. `to` must be at least 10 minutes in the past, and `from` within the last 30 days. |
| `states` | 1 to 2,000 floors in increasing `at`, each inside the range, the first at `from` exactly. Each floor is checked as a push's floor is: `tables`, `stations` and the optional `longSeatedMinutes`. |
| `events` | 0 to 2,000, each `id` once, each inside the range. `exitAt` as on a push. A label that names no table or station of the request's floors is blanked, not refused. |

- The body may be up to 2 MB. Each connection may send 6 backfills a minute, with bursts of up to 10; backfills and pushes have separate allowances.
- Send only the floors at changes: a floor is the whole state from its `at` until the next one.
- Everything stored for the range, live pushes included, is replaced by the request. Sending the same request twice gives the same result.
- A backfill is not a sign of life: it does not change the app's **Last seen** or its sequence.
- A day your app never pushed, within the last 30 days, is created and appears in the live view's **Service day** menu.

```json
{ "accepted": true, "serviceDay": "2026-11-06", "states": { "stored": 41, "replaced": 37 }, "events": { "stored": 12, "replaced": 0 }, "unmatchedLabels": [] }
```

| Answer | Meaning |
| --- | --- |
| `202` as above | Stored. A replay of that day can take up to a minute to show it. |
| `422` `{ "error": "invalid_backfill", "issues": [...] }` | A rule was broken; up to 20 issues. Nothing was stored. |
| `422` `{ "error": "backfill_too_large", "limit": "states", "max": 2000, "needed": 2104 }` | The day would pass one of its limits on stored floors, bytes or events (5,000 a day). Send fewer floors, or a day of more than 2,000 events as several ranges. Nothing was stored. |

## When to send one

- Every night, the previous service day: it corrects anything an error lost without anyone noticing.
- After a known outage or a correction in your system, the range it affected.

## Exit times

A check-in (`seated` or `reservation_seated`) may carry `exitAt`, in pushes and in backfills: when the party leaves, such as the end of the ticket's paid time. If they leave early, send the same event again, with the same `id`, and the real time.

- While your app is heard from (a push or heartbeat in the last 90 seconds), the live view shows the floors you send.
- Once it has been silent for 90 seconds, and the day has at least one check-in with `exitAt`, the live view estimates each zone: your last count, less the people of earlier check-ins whose `exitAt` has passed since. It never goes below 0; a zone being reset and a zone without a count are left as they were.
- The people in a check-in are its `detail`'s number (`"3 covers"` or `"3 guests"`), else 1.
- The viewer then reads “Estimated hh:mm from exit times” where it usually says “Refreshes every 10 s”. Replays of past days are never estimated.
- When your app is back, the heartbeat that ends the silence answers `"needFullState": true`: send a feed push with the day's events.

## Related

- [Live updates](https://floorplancraft.com/docs/live-updates.md)
- [Signing requests](https://floorplancraft.com/docs/signing-requests.md)
- [Disconnect and errors](https://floorplancraft.com/docs/disconnect-and-errors.md)

---

Source: https://floorplancraft.com/docs/backfill-and-exit-times · Updated 2026-11-07
