Skip to content

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
EnvironmentBase URLUse for
Developmenthttps://api.keyspace-dev.comEarly build + smoke tests
UAThttps://api.keyspace-qat.comPre-production sign-off
Productionhttps://api.keyspace.techLive

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 same POST /meetings endpoint — a room booking passes meetingRoomId, a desk booking passes deskId.
  • Envelope. Every response except the token endpoint is wrapped: { code, success, message, data, meta? }. code: "KS000" means success; any other KSxxx is an error. Unwrap data; read meta on paginated lists.
  • Lifecycle. A booking moves unpaid → active → checked-in → done, with cancelled reachable from unpaid and active only — a checked-in booking cannot be cancelled; it ends via check-out (organizer or pinCode) or automatically at the booking's end time. rejected is a terminal decline. Free rooms skip unpaid and land on active immediately.
  • Bookings change state on their own. A scheduler runs against every booking: an unpaid one auto-cancels when its ~5-minute payment window lapses, and an active one that nobody checks in to auto-cancels ~15 minutes after start (per-asset config.meetingConfig; postStartExpireMinutes: -1 disables the no-show cancel). At the end time the booking closes to done automatically.
  • 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/:id polling 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_in from 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:00

Rooms 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.dayConfigthe operating hours that matter, per weekday, each with availableTime {start,end} and optional breakTimes[]. A missing weekday means closed that day. (config.start/config.end are legacy overall hours — prefer dayConfig.)
  • 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 use startDateTime + exactly one of endDateTime / timePeriod.
  • The window must sit inside the day's operating hours, avoid breakTimes, meet minimumLeadTime, and stay within maxAdvanceBookingDays / 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 render payment.webPaymentQr) to the user.
  • The window is ~5 minutes (billingSummaries[].expiredAt); past it the booking auto-cancels.
  • Pass frontendRedirectUrl on create so the payer returns into your app after paying.
  • Confirm by polling GET /meetings/:id until status leaves unpaid.

Step 5 — Manage the booking

  • Update / extendPATCH /meetings/:id (only active/checked-in; start time is frozen after check-in; extending a paid room can return a new unpaid payment step).
  • CancelPOST /meetings/:id/cancel (from unpaid/active only — a checked-in booking cannot be cancelled, check out instead; irreversible; voids pending payment).
  • Check-inPOST /meetings/:id/check-in, opens ~15 min before start; active only. Do not skip this: an active booking with no check-in is auto-cancelled ~15 minutes after start (unless the asset disables it).
  • Check-outPOST /meetings/:id/check-out, checked-in only, irreversible — and the only way to end a checked-in booking early.

Include pinCode in the body when the caller is not the organizer.


4. Error handling strategy

  • Branch on HTTP status + the body code; treat message as human text, not a stable contract. On 400 (KS001), message is 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 (and 429), exponential backoff, max 3. Never auto-retry other 4xx.
  • No idempotency key on create. If POST /meetings times out, do not resend blindly — you may double-book. Reconcile with GET /meetings?from=…&to=…&meetingRoomId=… to see whether the booking landed, then decide.
  • _id is not unique in a from/to list response. That endpoint expands a recurring series into one row per occurrence, all sharing the series master's _id. Key and de-duplicate on occurrenceEventId when it is present; keep using _id for check-in/cancel/detail. Note GET /meetings/:id returns the master, whose window rolls forward across the series, so it will not match the occurrence row you started from.
HTTPCodeMeaning
400KS001Validation failed (message[])
401KS002Token missing/expired
403KS003No access to the resource
404KS005Not found
409 / 410KS006State conflict / target already gone
409KS012Time-slot conflict (recoverable)
429KS036Too many requests
500 / 503KS004Server / upstream error (retryable)
403KS221act_as / partner members: not your linked member (or not a single-project app)
401KS223act_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:

HeaderMeaning
X-Keyspace-Eventevent type, e.g. ROOM_CANCEL
X-Keyspace-Deliveryunique delivery id — de-duplicate retries on it
X-Keyspace-Timestampepoch ms — reject if older than ~5 min (replay guard)
X-Keyspace-Signaturesha256=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/:id for authoritative state; the payload never contains pinCode.
  • 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: true create returns a price and creates nothing.
  • [ ] Real create returns active (free room) or unpaid + webPaymentUrl (paid room).
  • [ ] Second overlapping create fails 409 KS012 and 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, or app_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_secret stored in a secrets manager, not in code.
  • [ ] Token cached per process; proactive refresh + one-shot 401 recovery in place.
  • [ ] 409 KS012 handled 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 active before showing success; honours the 5-min window and frontendRedirectUrl.
  • [ ] 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.