Connect an app
Send a connect request, let the venue owner approve it in FloorPlanCraft, then exchange the code with PKCE for a connection id and secret.
Updated · View as Markdown
Connecting takes one request from your server, one visit by the venue owner and one more request from your server. At the end, your server holds a connection id and a secret.
Before you start
- Give each install an
installId: a UUID v4 that you generate once and keep. - For each attempt, make a PKCE code verifier of 43 to 128 characters from
A-Z,a-z,0-9and- . _ ~, and its challenge: the base64url SHA-256 of the verifier without padding, 43 characters. - Make a random
stateof 16 to 128 visible ASCII characters and keep it with the attempt. - Know where the owner's browser should come back to (
returnUrl) and which origins will frame the embeds (embedOrigins).
To test your PKCE code, use the example from RFC 7636: the verifier dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk has the challenge E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM. Never use it for a real connection.
Step 1: send a connect request
Your server sends POST https://floorplancraft.com/api/connect/requests. It needs no signature: the request only describes the venue and opens nothing by itself.
{
"app": "restaurant",
"installId": "6f1c2a9e-3b7d-4c55-9a1e-2f8b7c6d5e40",
"appVersion": "2.3.0",
"venueName": "Harbour Café",
"timeZone": "Europe/London",
"returnUrl": "http://192.168.1.20:3000/settings/floorplancraft/callback",
"state": "c4V9qL2xT8mN1pR6wZ0yB3",
"codeChallenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"codeChallengeMethod": "S256",
"embedOrigins": ["http://192.168.1.20:3000", "https://pos.harbourcafe.example"],
"tables": [
{ "label": "T1", "capacity": 4, "room": "Main dining" },
{ "label": "T2", "capacity": 2, "room": "Main dining" },
{ "label": "T10", "capacity": 6, "room": "Terrace" }
],
"stations": [{ "name": "Grill" }, { "name": "Bar" }]
}The answer is 201 Created. Send the owner to authorizeUrl before expiresAt, which is 10 minutes away:
{
"requestId": "cr_q1W2e3R4t5Y6u7I8o9P0aS",
"authorizeUrl": "https://floorplancraft.com/connect?request=cr_q1W2e3R4t5Y6u7I8o9P0aS",
"expiresAt": "2026-11-07T12:10:00.000Z"
}Request fields
| Field | Rule |
|---|---|
app | "restaurant", "booking" or "play" |
installId | A UUID v4, the same for every connect from this install. Compared ignoring case. |
appVersion | Your app's version in semver, such as 2.3.0. At most 32 characters. |
venueName | 1 to 80 characters, no control characters. The approval page quotes it as what the app says the venue is. |
timeZone | The venue's IANA time zone, such as Europe/London. Service days and the times in the live view use it. |
returnUrl | Where the owner's browser comes back: http or https, at most 2,000 characters, no fragment, no user name or password. Kept byte for byte: send the same string to the code exchange. |
state | 16 to 128 visible ASCII characters (! to ~). It comes back unchanged. |
codeChallenge | The PKCE challenge, 43 characters |
codeChallengeMethod | "S256" |
embedOrigins | Optional. Up to 10 origins your app frames the embeds from, each written as scheme, host and optional port: no path, no trailing slash, no wildcard, no repeats. |
tables | Optional. Up to 500 rows, described below. |
stations | Optional, restaurant only. Up to 50 kitchen or bar stations, such as { "name": "Grill" }. A station's name follows the label rule below and may not repeat a table's label. |
returnUrl and every embed origin may use http: only for a host on the venue's own network, listed below. Everything else must use https:.
Tables, areas and zones
tables lists what your pushes will report, so FloorPlanCraft can match it with the plan. Fields other than these are ignored, so you can send your feed rows as they are.
| Field | Rule |
|---|---|
label | The name your pushes will use. At most 24 characters, starting with a letter or digit; letters, digits, spaces and . _ # & ' / + - only. Unique, ignoring case. |
capacity | A whole number from 1 to 99. A play venue sends the zone's kid capacity, from 1 to 999. |
room | Optional. The room's name, with the same rule as label. Without it, "Dining room". |
- Restaurant: one row per table, plus optional
stations. - Booking: one row per area, such as a room, a chair or basin, or a staff member's station.
capacityis 1 for an appointment, or a class's size. Leavestationsout or send[]. - Play venue: one row per play zone, its
labelas the plan names the zone. Leavestationsout or send[].
Addresses on the venue's network
http: is allowed only when the host is one of these:
localhost,127.0.0.0/8or[::1]- A private IPv4 address in
10.0.0.0/8,172.16.0.0/12or192.168.0.0/16 - An IPv6 unique local address,
fc00::/7 - A single-label name such as
pos, or a name ending in.local
Write each origin as a browser does: a lower-case host, no default port and no trailing slash, such as http://192.168.1.20:3000. Anything else is refused rather than corrected.
Connect request errors
| Status | Body | Meaning |
|---|---|---|
| 400 | { "error": "invalid_request", "issues": [{ "path": "tables.3.label", "message": "…" }] } | A field breaks a rule. Up to 50 issues, each naming its field. A body that is not JSON or is over 256 KB is a 400 too. |
| 429 | { "error": "rate_limited", "retryAfterMs": 60000 } | Too many requests: wait retryAfterMs milliseconds. |
| 503 | { "error": "unavailable" } | Try again shortly. |
Limits: 20 requests at once, then 60 an hour from one address (an IPv6 address counts by its /64), and 10 at once, then 30 an hour for one install from one address.
Step 2: the venue owner approves
Check that authorizeUrl starts with https://floorplancraft.com/connect?request=, then send the owner's browser to it.
On that page the owner signs in or creates an account, chooses a workspace and a plan, checks the venue name, the return origin and the embed origins, and presses Approve and connect or Deny. The page warns the owner when the return origin is not on the venue's own network. Connect your venue app shows this page from the owner's side.
- Restaurant: the owner picks an existing plan or Create a plan from your N tables. That plan has one zone per table, each with a table and chairs for its capacity, and a Kitchen room with one zone per station.
- Booking and play venue: the owner picks an existing plan. The owner draws the venue in the planner first and names each zone as your
labelvalues.
When the owner approves, FloorPlanCraft publishes and shares the plan, then sends the browser back to your returnUrl with a GET:
Approve: <returnUrl>?code=<43 characters>&state=<your state>
Deny: <returnUrl>?error=access_denied&state=<your state>Query parameters already on returnUrl are kept. Check that state matches the attempt before you go on.
Note: An expired or already answered request sends the browser nowhere. If the owner has not come back within 10 minutes, treat the attempt as expired and start again.
Step 3: exchange the code
Within 5 minutes, your server sends POST https://floorplancraft.com/api/connect/token:
{
"grantType": "authorization_code",
"code": "<the code from the return address>",
"codeVerifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
"installId": "6f1c2a9e-3b7d-4c55-9a1e-2f8b7c6d5e40",
"returnUrl": "http://192.168.1.20:3000/settings/floorplancraft/callback"
}The answer is 200 OK:
{
"connectionId": "lc_8sKq2mXvT0bN4yR7wE1uZa",
"connectionSecret": "<43 base64url characters, returned once>",
"planId": "…",
"shareToken": "…",
"planEmbedUrl": "https://floorplancraft.com/embed/<shareToken>",
"ingestUrl": "https://floorplancraft.com/api/live/ingest",
"zoneLabels": ["T1", "T2", "T10", "Grill", "Bar"],
"liveView": { "entitled": true, "policy": "entitled" },
"planVersionId": "…"
}| Field | Meaning |
|---|---|
connectionId | lc_ and 22 base64url characters. Send it as X-FPC-Connection. |
connectionSecret | Your signing key, 43 base64url characters. It is only in this answer: store it on your server, encrypted. |
planId | The connected plan |
shareToken | The plan's public share |
planEmbedUrl | The view-only plan, for an iframe |
ingestUrl | Where to push status |
zoneLabels | The plan's zone names, in plan order. Compare them with your labels and show the owner any that differ. |
liveView | entitled is false while the workspace does not have Live 3D: pushes answer 402 until it does. policy is "free" or "entitled". |
planVersionId | The plan's published version. Every push answer carries the current one. |
The exchange also adds your embedOrigins to the origins that may frame planEmbedUrl.
If this install already had an active connection in the same workspace, the new one replaces it and the old secret stops working. One install may be connected in several workspaces at once.
Code exchange errors
| Status | Body | What to do |
|---|---|---|
| 400 | { "error": "invalid_grant" } | The code is used, expired or wrong, or the verifier, installId or returnUrl does not match. An exchange that reaches the code uses it up, even when it fails: start again. |
| 400 | { "error": "invalid_request" } | A wrong grantType, a missing field, or a body that is not JSON or is over 4 KB. The code is not used. |
| 429 | { "error": "rate_limited", "retryAfterMs": 1000 } | Too many failed requests from your address: wait retryAfterMs milliseconds. |
| 503 | { "error": "unavailable" } | The code is still unused: retry the same exchange within the 5 minutes. |
Add tables later
A restaurant app can add tables the plan lacks with a signed POST https://floorplancraft.com/api/connections/self/tables. The body follows the connect request's rules for tables, which is required, and stations, which is optional:
{
"tables": [
{ "label": "T1", "capacity": 4, "room": "Main dining" },
{ "label": "T11", "capacity": 4, "room": "Terrace" }
],
"stations": [{ "name": "Pastry" }]
}Each table the plan lacks, compared ignoring case, becomes a zone with a table and chairs in a new room, New tables, set against the building with its own entrance. Missing stations go into the plan's kitchen when they fit, or else into a new kitchen room. Nothing in the plan moves, is renamed or is deleted; the owner moves the new tables into place in the planner.
{
"added": ["T11"],
"existing": ["T1"],
"addedStations": ["Pastry"],
"existingStations": [],
"stationsRoom": "Kitchen",
"stationsRoomCreated": false,
"planVersionId": "…"
}stationsRoomandstationsRoomCreatedare there only when stations were added.conflicts, when present, lists stations whose name is already a table's zone: rename those in your app.- The change is published at once, together with any changes the owner has saved but not yet published.
- A call that adds nothing makes no new version and returns the current
planVersionId. - A plan made or grown this way holds at most 1,000 zones, 6,000 items (tables and chairs included) and 4 MB. A sync past a limit adds nothing and answers
422with"error": "plan_too_large"and thelimit,maxandneededvalues. - Limits: a 256 KB body; 6 at once, then 6 a minute per connection. It does not need Live 3D.
- Booking and play venue apps get
422with"error": "unsupported_app": the owner adds or renames zones in the planner instead.