Appearance
Keyspace Meeting Rooms & Desks — Integration Guide
Version 1.11.1 · The narrative guide for third-party apps that book Keyspace meeting rooms and desks. Platform basics (credentials, token, envelope, webhooks) are on the getting-started page.
This guide is the narrative — how the pieces fit and the journey a booking travels. For field-by-field endpoint detail see the full API reference; for a machine-readable contract see the OpenAPI spec. To scaffold a client quickly, hand your coding agent the integration prompt and this environment's /docs/openapi.json.
1. Before you start
Keyspace issues you, per environment:
app_id+app_secret— your OAuth client credentials (server-side only)- a default
projectId— the property whose rooms/desks you may book
| Environment | Base URL | Use for |
|---|---|---|
| Development | https://api.keyspace-dev.com | Early build + smoke tests |
| UAT | https://api.keyspace-qat.com | Pre-production sign-off |
| Production | https://api.keyspace.tech | Live |
Credentials are not shared across environments. Each environment also serves its own copy of this guide, the reference, and the OpenAPI spec — always read the docs from the environment you are integrating against.
2. The five-minute mental model
- Resources. A
projectId(property) contains meeting rooms and desks. Both are booked through the samePOST /meetingsendpoint — a room booking passesmeetingRoomId, a desk booking passesdeskId. - Envelope. Every response except the token endpoint is wrapped:
{ code, success, message, data, meta? }.code: "KS000"means success; any otherKSxxxis an error. Unwrapdata; readmetaon paginated lists. - Lifecycle. A booking moves
unpaid → active → checked-in → done, withcancelledreachable fromunpaidandactiveonly — achecked-inbooking cannot be cancelled; it ends via check-out (organizer orpinCode) or automatically at the booking's end time.rejectedis a terminal decline. Free rooms skipunpaidand land onactiveimmediately. - Bookings change state on their own. A scheduler runs against every booking: an
unpaidone auto-cancels when its ~5-minute payment window lapses, and anactiveone that nobody checks in to auto-cancels ~15 minutes after start (per-assetconfig.meetingConfig;postStartExpireMinutes: -1disables the no-show cancel). At the end time the booking closes todoneautomatically. - Webhooks push, polling reconciles. Register a listener server (see section 6) and Keyspace POSTs signed lifecycle events to your backend — bookings, changes, cancels (including the automatic ones), reminders. Keep
GET /meetings/:idpolling as the reconciliation fallback (payment confirmation, missed deliveries). - No availability endpoint per se. You search rooms/desks for a time window and get back the ones that are free — see step 3.
3. Step by step
Step 1 — Authenticate
POST /auth/accessToken with { app_id, app_secret, grant_type: "app_credentials" }. You get back access_token, refresh_token, and their lifetimes in seconds.
- Send the token on every call as
Authorization: Bearer <access_token>. - Refresh proactively: when the access token is within ~5 minutes of expiry, call the same endpoint with
{ refresh_token, grant_type: "refresh_token" }and store the new pair it returns. - Recover once: if a call still returns
401(KS002), refresh → retry once → full re-auth → then surface the error. - Do not hardcode token lifetimes — they are environment configuration; read
expires_infrom the response.
Authenticate once per server process, not per request.
Advanced — act on behalf of a user (act_as). The app token above books everything as your app; a person on a booking is just an attendee. If your users should instead reuse Keyspace's per-user scope (member booking quotas, member-owned resources, per-member roles), exchange your app credentials for a member-scoped token: same endpoint, grant_type: "act_as", plus subject: "<your-user-id>" — the member is created and linked to your app on first use, no Keyspace password or login involved. The delegated token is short-lived, has no refresh token (re-run the grant), and can never exceed your app's own scope. This is a coordinated feature — agree it with your Keyspace contact first. Full detail + errors (KS221/KS223): reference §act_as.
Your app owns its users? If you hold your users' profiles, access-card numbers or face photos and want Keyspace to enrol them on the doors, that is a separate surface — the Members & credentials guide. The act_as subject above is the same id as that guide's externalUserId, so a member you provision and a member you book as are one person.
Step 2 — Find something to book
Rooms — GET /meeting-rooms?projectId=…&status=active. Add a window to filter to what's free:
GET /meeting-rooms?projectId=<id>&status=active
&startDateTime=2026-07-15T14:00:00+07:00
&endDateTime=2026-07-15T15:00:00+07:00Rooms with a conflicting booking are dropped from the result. A room that is free but not bookable for that window carries an unavailableReason (OPERATING_HOURS or BREAK_TIME); skip those. No unavailableReason ⇒ bookable.
Desks — GET /desks?projectId=…. For availability, pass startDateTime plus exactly one of timePeriod (morning/afternoon/allDay) or endDateTime (explicit window).
Read the asset's config before you build the booking form:
config.dayConfig— the operating hours that matter, per weekday, each withavailableTime {start,end}and optionalbreakTimes[]. A missing weekday means closed that day. (config.start/config.endare legacy overall hours — preferdayConfig.)config.minimumLeadTime— how far ahead a booking must start.config.maxAdvanceBookingDays/maxDurationMinutes— booking-horizon and length limits.
Times inside config are property-local wall-clock (HH:mm:ss).
Step 3 — Create the booking
POST /meetings. Rules to enforce client-side (the server enforces them too, but failing fast is a better UX):
- Exactly one of
meetingRoomId/deskId. - Rooms use
startDateTime+endDateTime. Desk bookings usestartDateTime+ exactly one ofendDateTime/timePeriod. - The window must sit inside the day's operating hours, avoid
breakTimes, meetminimumLeadTime, and stay withinmaxAdvanceBookingDays/maxDurationMinutes.
Preview price before committing with draft: true (nothing is created); use draftEndDateTimes to price several durations in one call.
The success response carries the booking _id and a pinCode — store both. The pinCode authorizes later update/cancel/check-in/check-out when the caller is not the organizer. Treat it as a secret; never log it.
Step 4 — Handle payment (paid rooms only)
If the create (or an extending update) returns status: "unpaid", payment is required. It is a human web flow — do not try to automate it.
- Present
payment.webPaymentUrl(or renderpayment.webPaymentQr) to the user. - The window is ~5 minutes (
billingSummaries[].expiredAt); past it the booking auto-cancels. - Pass
frontendRedirectUrlon create so the payer returns into your app after paying. - Confirm by polling
GET /meetings/:iduntilstatusleavesunpaid.
Step 5 — Manage the booking
- Update / extend —
PATCH /meetings/:id(onlyactive/checked-in; start time is frozen after check-in; extending a paid room can return a newunpaidpayment step). - Cancel —
POST /meetings/:id/cancel(fromunpaid/activeonly — achecked-inbooking cannot be cancelled, check out instead; irreversible; voids pending payment). - Check-in —
POST /meetings/:id/check-in, opens ~15 min before start;activeonly. Do not skip this: anactivebooking with no check-in is auto-cancelled ~15 minutes after start (unless the asset disables it). - Check-out —
POST /meetings/:id/check-out,checked-inonly, irreversible — and the only way to end achecked-inbooking early.
Include pinCode in the body when the caller is not the organizer.
4. Error handling strategy
- Branch on HTTP status + the body
code; treatmessageas human text, not a stable contract. On400(KS001),messageis an array of constraint strings — surface them. 409 KS012(time-slot conflict) is a normal, recoverable outcome of two users racing for the same slot — catch it and offer another slot, don't treat it as a crash.- Retry only network failures and
5xx(and429), exponential backoff, max 3. Never auto-retry other4xx. - No idempotency key on create. If
POST /meetingstimes out, do not resend blindly — you may double-book. Reconcile withGET /meetings?from=…&to=…&meetingRoomId=…to see whether the booking landed, then decide. _idis not unique in afrom/tolist response. That endpoint expands a recurring series into one row per occurrence, all sharing the series master's_id. Key and de-duplicate onoccurrenceEventIdwhen it is present; keep using_idfor check-in/cancel/detail. NoteGET /meetings/:idreturns the master, whose window rolls forward across the series, so it will not match the occurrence row you started from.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | KS001 | Validation failed (message[]) |
| 401 | KS002 | Token missing/expired |
| 403 | KS003 | No access to the resource |
| 404 | KS005 | Not found |
| 409 / 410 | KS006 | State conflict / target already gone |
| 409 | KS012 | Time-slot conflict (recoverable) |
| 429 | KS036 | Too many requests |
| 500 / 503 | KS004 | Server / upstream error (retryable) |
| 403 | KS221 | act_as / partner members: not your linked member (or not a single-project app) |
| 401 | KS223 | act_as / partner members: subject not resolved |
A KSxxx code is not 1:1 with an HTTP status (KS006 spans 409/410, KS004 spans 500/503) — branch on the status, log the code. Full reference: error codes.
5. Timezones
Send every date-time in ISO 8601 with an explicit offset (2026-07-15T14:00:00+07:00). Render times back to users in the property's local timezone. config hours are property-local HH:mm:ss.
6. Receiving events on your server (webhooks)
Give your Keyspace contact an HTTPS callback URL, a 32+ character secret you generate, and the event types you want. Keyspace then POSTs each event to your URL with these headers:
| Header | Meaning |
|---|---|
X-Keyspace-Event | event type, e.g. ROOM_CANCEL |
X-Keyspace-Delivery | unique delivery id — de-duplicate retries on it |
X-Keyspace-Timestamp | epoch ms — reject if older than ~5 min (replay guard) |
X-Keyspace-Signature | sha256=HEX(HMAC-SHA256(secret, timestamp + "." + rawBody)) |
Rules of the road:
- Verify the signature over the raw body bytes before trusting anything (code sample in the full reference).
- Ack fast: respond 2xx within 10 s; process async. Failures retry up to 5× with exponential backoff; 20 consecutive dead deliveries auto-disable your webhook until Keyspace re-enables it.
- The payload is a signal, not the record — re-fetch
GET /meetings/:idfor authoritative state; the payload never containspinCode. - Branch cancels on
data.cancelReason: absent = user/admin cancel,"No-show — auto-cancelled by system", or"Payment window lapsed — auto-cancelled by system".
Event types: ROOM_/DESK_BOOK, _MODIFY, _CANCEL, _REMINDER, _NO_SHOW_WARNING, _ENDING_SOON, plus visitor events VMS_INVITE_SENT, VMS_VISITOR_CHECKED_IN, VMS_HOST_CANCELLED — full catalog + payload fields in the reference. Visitor events ride the same channel; note VMS_INVITE_SENT carries the visitor's access QR (qrCardCode) for you to relay to the invitee — the one payload field you should treat as a credential.
If your endpoint needs its own auth (beyond verifying our signature), Keyspace can present a bearer token, a custom-header API key, or a short-lived jwt (HS256 over the shared secret) — pick the scheme at registration. Default is signature-only.
7. Sandbox testing checklist
Run against dev/UAT before you ship:
- [ ] Authenticate, then let the token expire and confirm your refresh path.
- [ ] List rooms; list again with a time window and confirm busy rooms drop out.
- [ ]
draft: truecreate returns a price and creates nothing. - [ ] Real create returns
active(free room) orunpaid+webPaymentUrl(paid room). - [ ] Second overlapping create fails
409 KS012and your UI offers another slot. - [ ] Update changes a field; cancel flips to
cancelled. - [ ] Your smoke test cancels everything it created (try/finally) — never leave live bookings behind.
- [ ] Confirm you never log
pinCode, tokens, orapp_secret. - [ ] If using webhooks: your endpoint verifies the signature, acks within 10 s, and de-duplicates on
X-Keyspace-Delivery.
8. Go-live checklist
- [ ] Production
app_id/app_secretstored in a secrets manager, not in code. - [ ] Token cached per process; proactive refresh + one-shot 401 recovery in place.
- [ ]
409 KS012handled as a user-facing "pick another slot", not an error page. - [ ] Timed-out creates reconcile via
GET /meetings, never blind-retry. - [ ] Paid-room flow polls to
activebefore showing success; honours the 5-min window andfrontendRedirectUrl. - [ ] All date-times sent with an offset; times rendered in the property timezone.
9. Getting help & reporting discrepancies
If the API's observed behavior differs from these docs, note the exact endpoint, what you expected, and what you saw, and send it to your Keyspace contact — the docs are served from the API itself and we correct drift at the source.