Skip to content
FloorPlanCraftSign in
Explore the planner
Browse the docs

Live updates

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

Updated · View as Markdown

Your server sends the venue's state to POST https://floorplancraft.com/api/live/ingest, signed as in Signing requests. 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.

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.

{
  "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

FieldRule
restaurantThe venue's name, 1 to 80 characters. Booking and play venue apps use this field name too.
atWhen 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.
serviceDayThe trading day as start and end times with offsets: longer than zero and at most 36 hours.
tablesEvery table, area or zone, up to 500 rows. Required; may be empty.
stationsKitchen or bar stations, up to 100. Required: send [] when there are none.
eventsUp to 2,000 events, each with its own id. Required; may be empty.
sequenceOptional. A whole number that grows with each push.
longSeatedMinutesOptional. 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

FieldRule
labelThe zone's name in the plan, matched ignoring case. At most 24 characters, starting with a letter or digit; letters, digits, spaces and . _ # & ' / + - only.
roomThe room's name, with the same rule. Required.
capacitySeats or places, a whole number of 1 or more
state"free", "seated" or "reset" (being cleared)
coversHow many people are there now, or null
openMinutesMinutes since the table was seated, or null
serverOptional. 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

FieldRule
id1 to 64 printable characters, unique within the push and stable for the service day, such as seated:T7:2
atWhen it happened, inside the service day
typeseated, paid, voided, ticket_fired, ticket_ready, reservation_seated or no_show. Any other type is shown as other activity.
labelOptional. The table or station it concerns, as named in this push
detailOptional. Only a party size ("4 guests" or "4 covers") or, for ticket events, a station's name is kept.
exitAtOptional, 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.
  • 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

{ "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:

{
  "accepted": true,
  "stored": "state",
  "planVersionId": "…",
  "unmatchedLabels": ["T99"],
  "needFullState": false,
  "capped": false,
  "droppedEvents": 0
}
FieldMeaning
storedstate: 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.
ignoredOnly on a dropped push: sequence (lower than the highest seen) or service_day (a day before the current one)
planVersionIdThe plan's published version
unmatchedLabelsLabels 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.
needFullStatetrue: send a feed push with the day's events next.
cappedtrue: the service day reached its storage limit, 2,000 stored floors or 50 MB, and now keeps one floor every 5 minutes.
droppedEventsNew events not stored because the day already holds 5,000

Push errors

StatusBodyMeaning
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.

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:

FieldBooking meaning
restaurantThe venue's name
tablesOne row per area: a room, a chair or basin, a staff member's station
stateseated: in use. free: free. reset: cleaning time after a booking.
covers1 for an appointment, or the booked places for a class
openMinutesMinutes since the booking started
servernull
stations[]
events[].typeseated (a booking starts or the client arrives), paid, no_show
longSeatedMinutesOne 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:

{
  "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" }
  ]
}
FieldPlay venue meaning
restaurantThe venue's name
tablesOne 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.
stateseated: one or more kids are in the zone. free: none. reset: the zone is closed, for cleaning or a break.
coversThe number of kids in the zone now; 0 when it is free or closed
capacityThe zone's kid capacity
openMinutesMinutes since the zone got busy, or null
servernull
stations[]
events[].typeseated: kids checked in to a zone. reservation_seated: a booked party checked in. no_show: a booked party did not come.
events[].detailHow many kids checked in, as "3 guests" or "3 covers"
longSeatedMinutesStill 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.