# Live updates

> Push the venue's floor to FloorPlanCraft: feed pushes, heartbeats, service days, sequence numbers, events and what each answer tells your app.

Your server sends the venue's state to `POST https://floorplancraft.com/api/live/ingest`, signed as in [Signing requests](https://floorplancraft.com/docs/signing-requests.md). A push is either a feed push, with the whole floor, or a small heartbeat.

## When to push

- A feed push when something changes, debounced to 5 seconds.
- A heartbeat every 60 seconds.
- A feed push with the day's events on the first push of each service day, and whenever an answer says `"needFullState": true`.
- After a failed push or a restart, a feed push with the day's events first. A heartbeat that ends a silence of 90 seconds or more also answers `"needFullState": true`.
- Entries an error left out of a past range are re-sent with a backfill: see [Backfill and exit times](https://floorplancraft.com/docs/backfill-and-exit-times.md).

The live view shows **Live** while the last push is under 90 seconds old, and **Last seen hh:mm** after that. Each connection may push 12 times a minute, with bursts of up to 24.

## A feed push

A feed push always describes the whole floor: every table and station, not only the ones that changed.

```json
{
  "restaurant": "Harbour Café",
  "at": "2026-11-07T12:30:00.000Z",
  "serviceDay": { "start": "2026-11-07T06:00:00.000Z", "end": "2026-11-08T02:00:00.000Z" },
  "sequence": 1287,
  "tables": [
    { "label": "T1", "room": "Main dining", "capacity": 4, "state": "seated", "covers": 3, "openMinutes": 25, "server": null },
    { "label": "T2", "room": "Main dining", "capacity": 2, "state": "free", "covers": null, "openMinutes": null, "server": null }
  ],
  "stations": [
    { "name": "Grill", "queue": 2, "oldestMinutes": 6 }
  ],
  "events": [
    { "id": "seated:T1:1", "at": "2026-11-07T12:05:00.000Z", "type": "seated", "label": "T1", "detail": "3 guests" },
    { "id": "ticket:1042:fired", "at": "2026-11-07T12:24:00.000Z", "type": "ticket_fired", "label": "T1", "detail": "Grill" }
  ],
  "longSeatedMinutes": 60
}
```

## Feed fields

| Field | Rule |
| --- | --- |
| `restaurant` | The venue's name, 1 to 80 characters. Booking and play venue apps use this field name too. |
| `at` | When this state was true: an ISO 8601 time with an offset (`Z` or `+01:00`), within 10 minutes of FloorPlanCraft's clock and inside the service day. |
| `serviceDay` | The trading day as `start` and `end` times with offsets: longer than zero and at most 36 hours. |
| `tables` | Every table, area or zone, up to 500 rows. Required; may be empty. |
| `stations` | Kitchen or bar stations, up to 100. Required: send `[]` when there are none. |
| `events` | Up to 2,000 events, each with its own `id`. Required; may be empty. |
| `sequence` | Optional. A whole number that grows with each push. |
| `longSeatedMinutes` | Optional. A whole number from 1 to 600: a seated table open longer than this counts as long-seated. Without it, 45. |

- Send `longSeatedMinutes` on every feed push it should apply to: a feed push without it goes back to 45. A heartbeat does not change it.
- Never add a `heartbeat` field to a feed push: it is refused.

## Rows in tables

| Field | Rule |
| --- | --- |
| `label` | The zone's name in the plan, matched ignoring case. At most 24 characters, starting with a letter or digit; letters, digits, spaces and `. _ # & ' / + -` only. |
| `room` | The room's name, with the same rule. Required. |
| `capacity` | Seats or places, a whole number of 1 or more |
| `state` | `"free"`, `"seated"` or `"reset"` (being cleared) |
| `covers` | How many people are there now, or `null` |
| `openMinutes` | Minutes since the table was seated, or `null` |
| `server` | Optional. FloorPlanCraft removes staff names before it stores a push: send `null`. |

`covers` and `openMinutes` must always be present, as a number or `null`. A row whose `label` or `room` breaks the label rule is not shown and comes back in `unmatchedLabels`.

## Stations

A restaurant app can report kitchen or bar stations. Each row has a `name` that follows the label rule, a `queue` of tickets waiting (a whole number) and `oldestMinutes`, the age of the oldest ticket in minutes or `null`. Booking and play venue apps send `"stations": []`.

## Events

| Field | Rule |
| --- | --- |
| `id` | 1 to 64 printable characters, unique within the push and stable for the service day, such as `seated:T7:2` |
| `at` | When it happened, inside the service day |
| `type` | `seated`, `paid`, `voided`, `ticket_fired`, `ticket_ready`, `reservation_seated` or `no_show`. Any other type is shown as other activity. |
| `label` | Optional. The table or station it concerns, as named in this push |
| `detail` | Optional. Only a party size (`"4 guests"` or `"4 covers"`) or, for ticket events, a station's name is kept. |
| `exitAt` | Optional, on `seated` and `reservation_seated` only: when the party leaves, an ISO 8601 time with an offset, after `at` and at most 24 hours later. See [Backfill and exit times](https://floorplancraft.com/docs/backfill-and-exit-times.md#exit-times). |

- Events are stored by `id`, so you may send only new and changed events, or the whole day each time.
- A stored event is replaced only by a push whose `at` is not earlier than the push that stored it.
- A service day keeps at most 5,000 different ids. New ids past that are counted in `droppedEvents`; known ids still update.
- The live view lists the last 2,000 events of a day.
- An event outside the service day makes the whole push fail.

## A heartbeat

```json
{ "at": "2026-11-07T12:31:00.000Z", "heartbeat": true, "sequence": 1288 }
```

A heartbeat has only these fields, and `sequence` is optional. It tells FloorPlanCraft the venue is still there; nothing else is stored.

## The answer

An accepted push answers `202 Accepted`:

```json
{
  "accepted": true,
  "stored": "state",
  "planVersionId": "…",
  "unmatchedLabels": ["T99"],
  "needFullState": false,
  "capped": false,
  "droppedEvents": 0
}
```

| Field | Meaning |
| --- | --- |
| `stored` | `state`: the floor changed and was stored. `keyframe`: nothing changed, but 5 minutes had passed, so it was stored. `heartbeat`: only the time the venue was last heard from moved. `none`: the push was dropped. |
| `ignored` | Only on a dropped push: `sequence` (lower than the highest seen) or `service_day` (a day before the current one) |
| `planVersionId` | The plan's published version |
| `unmatchedLabels` | Labels and station names in this push that no zone of the plan carries, or that cannot be shown; at most 100. Rename the zone in the plan or the label in your app. |
| `needFullState` | `true`: send a feed push with the day's events next. |
| `capped` | `true`: the service day reached its storage limit, 2,000 stored floors or 50 MB, and now keeps one floor every 5 minutes. |
| `droppedEvents` | New events not stored because the day already holds 5,000 |

## Push errors

| Status | Body | Meaning |
| --- | --- | --- |
| 402 | `{ "error": "entitlement_required", "entitlement": "live_view", "upgradeUrl": "https://floorplancraft.com/checkout/live-3d?connection=lc_…" }` | The workspace does not have Live 3D. Open `upgradeUrl` for the venue owner in a browser tab. |
| 422 | `{ "error": "invalid_feed", "issues": [{ "path": "tables.3.state", "message": "…" }] }` | The body breaks a rule; up to 20 issues. Nothing was stored. |
| 422 | `{ "error": "invalid_sequence" }` | `sequence` jumped too far. Nothing was stored. |

Signing failures, `403 connection_revoked`, `413` for a body over 512 KB and `429` are in [Disconnect and errors](https://floorplancraft.com/docs/disconnect-and-errors.md).

## Service days

- A push belongs to the date, in the venue's `timeZone`, on which its `serviceDay.start` falls.
- Service days only move forward. A push for a day before the current one is dropped with `"ignored": "service_day"`.
- Once the stored day has ended, a heartbeat answers `"needFullState": true`: send a feed push for the new day.

## Sequence numbers

- `sequence` is optional. If you send it, make it grow with every push, feed or heartbeat.
- A push with a lower `sequence` than the highest seen is dropped with `"ignored": "sequence"`. The same value again, a retry, is applied.
- A jump of more than 100 times the last value and more than 10,000,000 above it is refused with `422` and `"error": "invalid_sequence"`, and nothing is stored. One wrong value, such as milliseconds sent instead of seconds, cannot lock out every later push.

## When the plan changes

Every answer carries `planVersionId`. It changes when a new version of the plan is published. If your app keeps a copy of the plan, such as its zone names, read `GET https://floorplancraft.com/api/shares/<shareToken>` again only then: its `publishedVersionId` is the same identifier, and `content.zones` lists the zones with their `name`.

## Stored history

FloorPlanCraft keeps 30 days of pushed history for the live view's replay. A floor is stored when it changes, plus a keyframe every 5 minutes; the growing ages in `openMinutes` and `oldestMinutes` alone are not a change. Replay reads this history and never calls your app.

## Booking apps

A booking app for a salon, clinic or gym sends the same body. Its areas travel as rows in `tables`:

| Field | Booking meaning |
| --- | --- |
| `restaurant` | The venue's name |
| `tables` | One row per area: a room, a chair or basin, a staff member's station |
| `state` | `seated`: in use. `free`: free. `reset`: cleaning time after a booking. |
| `covers` | 1 for an appointment, or the booked places for a class |
| `openMinutes` | Minutes since the booking started |
| `server` | `null` |
| `stations` | `[]` |
| `events[].type` | `seated` (a booking starts or the client arrives), `paid`, `no_show` |
| `longSeatedMinutes` | One value for the whole venue, such as `90`. There is no value per row. |

The live view uses booking words: **In use**, **Cleaning**, clients and places.

## Play venue apps

A play venue's ticketing app sends the same body. Each play zone is a row in `tables`, and `covers` counts the kids in it:

```json
{
  "restaurant": "Example Play Centre",
  "at": "2026-11-07T14:00:00.000Z",
  "serviceDay": { "start": "2026-11-07T09:00:00.000Z", "end": "2026-11-07T19:00:00.000Z" },
  "sequence": 512,
  "tables": [
    { "label": "Soft play", "room": "Play hall", "capacity": 40, "state": "seated", "covers": 12, "openMinutes": 95, "server": null },
    { "label": "Bounce zone", "room": "Play hall", "capacity": 28, "state": "reset", "covers": 0, "openMinutes": null, "server": null }
  ],
  "stations": [],
  "events": [
    { "id": "checkin:4411", "at": "2026-11-07T13:52:00.000Z", "type": "seated", "label": "Soft play", "detail": "3 guests" }
  ]
}
```

| Field | Play venue meaning |
| --- | --- |
| `restaurant` | The venue's name |
| `tables` | One row per play zone, `label` exactly the plan's zone name. Zones nobody plays in, such as parents' seating or the counter, may be left out. |
| `state` | `seated`: one or more kids are in the zone. `free`: none. `reset`: the zone is closed, for cleaning or a break. |
| `covers` | The number of kids in the zone now; `0` when it is free or closed |
| `capacity` | The zone's kid capacity |
| `openMinutes` | Minutes since the zone got busy, or `null` |
| `server` | `null` |
| `stations` | `[]` |
| `events[].type` | `seated`: kids checked in to a zone. `reservation_seated`: a booked party checked in. `no_show`: a booked party did not come. |
| `events[].detail` | How many kids checked in, as `"3 guests"` or `"3 covers"` |
| `longSeatedMinutes` | Still checked, 1 to 600, but it changes nothing: a busy zone is never long-seated. |

- Count each kid in one zone only. Kids with a ticket for several zones can have a zone of their own in the plan, such as a walkway zone named **Combo**.
- The live view says **Playing**, **Empty** and **Closed**, and counts kids.
- The 3D view draws one kid per kid counted, up to 40 in a zone and fewer in a small one. The panel and labels always show the true count.

## Related

- [Signing requests](https://floorplancraft.com/docs/signing-requests.md)
- [Embedding the live view](https://floorplancraft.com/docs/embed-the-live-view.md)
- [Disconnect and errors](https://floorplancraft.com/docs/disconnect-and-errors.md)

---

Source: https://floorplancraft.com/docs/live-updates · Updated 2026-11-07
