Skip to content
FloorPlanCraftSign in
Explore the planner
Browse the docs

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-9 and - . _ ~, and its challenge: the base64url SHA-256 of the verifier without padding, 43 characters.
  • Make a random state of 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

FieldRule
app"restaurant", "booking" or "play"
installIdA UUID v4, the same for every connect from this install. Compared ignoring case.
appVersionYour app's version in semver, such as 2.3.0. At most 32 characters.
venueName1 to 80 characters, no control characters. The approval page quotes it as what the app says the venue is.
timeZoneThe venue's IANA time zone, such as Europe/London. Service days and the times in the live view use it.
returnUrlWhere 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.
state16 to 128 visible ASCII characters (! to ~). It comes back unchanged.
codeChallengeThe PKCE challenge, 43 characters
codeChallengeMethod"S256"
embedOriginsOptional. 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.
tablesOptional. Up to 500 rows, described below.
stationsOptional, 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.

FieldRule
labelThe name your pushes will use. At most 24 characters, starting with a letter or digit; letters, digits, spaces and . _ # & ' / + - only. Unique, ignoring case.
capacityA whole number from 1 to 99. A play venue sends the zone's kid capacity, from 1 to 999.
roomOptional. 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. capacity is 1 for an appointment, or a class's size. Leave stations out or send [].
  • Play venue: one row per play zone, its label as the plan names the zone. Leave stations out or send [].

Addresses on the venue's network

http: is allowed only when the host is one of these:

  • localhost, 127.0.0.0/8 or [::1]
  • A private IPv4 address in 10.0.0.0/8, 172.16.0.0/12 or 192.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

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

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": "…"
}
FieldMeaning
connectionIdlc_ and 22 base64url characters. Send it as X-FPC-Connection.
connectionSecretYour signing key, 43 base64url characters. It is only in this answer: store it on your server, encrypted.
planIdThe connected plan
shareTokenThe plan's public share
planEmbedUrlThe view-only plan, for an iframe
ingestUrlWhere to push status
zoneLabelsThe plan's zone names, in plan order. Compare them with your labels and show the owner any that differ.
liveViewentitled is false while the workspace does not have Live 3D: pushes answer 402 until it does. policy is "free" or "entitled".
planVersionIdThe 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

StatusBodyWhat 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": "…"
}
  • stationsRoom and stationsRoomCreated are 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 422 with "error": "plan_too_large" and the limit, max and needed values.
  • 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 422 with "error": "unsupported_app": the owner adds or renames zones in the planner instead.