Keyspace Reservations — Integration Guide

Version 1.15.0 · The narrative guide for third-party apps that hold Keyspace lockers for their users: create the hold, hand over the door, finish or cancel it. Platform basics (credentials, token, envelope, webhooks) are on the getting-started page.

A reservation is the hold on an asset. For partners the asset is a locker — meeting rooms and desks are booked through POST /meetings (meeting guide), not through this endpoint. What a locker is and how its door behaves is in the lockers guide; how long a locker may be held, and what happens when it is held too long, is in the quota and violations guides.


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

Permissions. Creating needs RESERVATION_CREATE on the project. Reading a reservation is allowed for its owner, any participant, or a role with RESERVATION_VIEW; changing or cancelling it for its owner or a role with RESERVATION_UPDATE. The cleanest model for a resident/employee app is act_as: the delegated token is the member, so the member owns what they reserve and your app needs no blanket update rights (getting started §4).


2. The five-minute mental model

  • Holder vs. requester. ownerId is the member who holds the locker — their quota, violations and blacklist are the ones checked. It defaults to the caller; staff and apps with their own role may set it to book on someone's behalf. participantIds are additional people allowed to open the door.
  • Status (status): active → done (returned) or cancelled. Only an active reservation can be changed.
  • Type (type): once (one hold with a start/stop), multi, period, permanent (no stop), series, session, and delivery (parcel hand-over on a deposit/pickup pair — yields one-time codes). Use once unless your Keyspace contact configured something else for the property.
  • Time window. startDateTime / stopDateTime (ISO-8601 with offset). Optional earlyStartDateTime / lateStartDateTime / earlyStopDateTime / lateStopDateTime widen the window in which the door may be used.
  • The locker follows the reservation. Create → locker reserved; use → in-used; done / cancel → active. The reservation records the locker's status at each step as assetStatus.
  • Envelope. { "code": "KS000", "message": "...", "data": ... } (+ meta on lists).

3. The reservation lifecycle

statusLocker isYou may call
active, locker reservedheld, unusedPATCH (edit), POST …/cancel, POST /lockers/{id} use / open
active, locker in-usedoccupiedPOST /lockers/{id} open / done. Cancel and nextStatus: "done" on the reservation are refused (409 KS022) — the locker must be returned through the locker endpoint so the door state and the hold stay in step
donereturnedread only
cancelledfreedread only

What happens on cancel / done. The locker goes back to active, its holder fields are cleared, a paired locker is freed too, the reservation's one-time codes are deleted, any violation recorded for this reservation is lifted, and LOCKER_BOOK_CANCEL (cancel) or LOCKER_RETURN (return) fires.

Gates at create. Before a reservation is minted Keyspace checks the holder:

CheckRefusal
Holder has an active blacklist409 KS019 "You are in the blacklist"
Holder has an active violation409 KS019 "You have violated the policy"
Holder's active locker count ≥ maxReservation for their role409 KS019 "Quota is full"

Read message to tell the three apart. How each arises: violations guide, quota guide.


4. Step by step

Step 1 — Authenticate

POST /auth/accessToken — client_credentials, or act_as to hold a locker as a member. Details: getting started §3.

Step 2 — Pick a locker

GET /lockers?projectId=…&lockerStatuses=active — lockers guide, Step 2.

Step 3 — Reserve a locker

POST /reservations
Authorization: Bearer <token>
Content-Type: application/json

{
  "assetId": "<lockerId>",
  "assetType": "locker",
  "type": "once",
  "startDateTime": "2026-10-20T09:00:00+07:00",
  "stopDateTime": "2026-10-21T09:00:00+07:00",
  "participantIds": [],
  "info": { "topic": "Gym bag", "place": "", "host": "", "guest": "" },
  "description": "Created from <your app>"
}

Optional fields: ownerId (holder — defaults to the caller), passcode (a code the holder may type at the kiosk), useLocker: true (reserve and open now; sendCommand defaults to true in that case), consigneeRoomId + courierTrackingCodes[] (delivery flows).

Response data is the reservation: id, status: "active", assetId, assetType, assetStatus, type, ownerId, participantIds, startDateTime, stopDateTime, info, timestamps — plus depositCode / pickupCode when the locker is a deposit/pickup pair (delivery). Store id.

With useLocker: true a device that does not confirm the door makes Keyspace cancel the reservation and answer 502 KS228 — nothing was stored (lockers guide §3.2).

Step 4 — Read it back

GET /reservations/{reservationId}

Returns the reservation (and the one-time codes while they are valid). List with filters:

GET /reservations?assetType=locker&status=active&ownerId={memberId}&projectId={projectId}&page=1&limit=50

Filters: assetId / assetIds[], assetTitle, ids[], type, status[], ownerId, participantIds[], zoneId, floorId, locationIds[], lockerTypes[], notLockerTypes[], updatedFrom, to, sort. You see what your role is scoped to plus every reservation you participate in; a delegated member therefore sees their own.

Step 5 — Use the locker

POST /lockers/{lockerId} with use / open — lockers guide, Step 4.

Step 6 — Cancel or finish

POST /reservations/{reservationId}/cancel

(equivalent to POST /reservations/{reservationId} with { "nextStatus": "cancelled" }). Allowed only while the reservation is active and the locker is not in-used. To finish a reservation whose locker is occupied, return the locker: POST /lockers/{lockerId} { "action": "done", "sendCommand": true } — the reservation becomes done as part of that call.

POST /reservations/{reservationId} with { "nextStatus": "done" } completes an active reservation whose locker was never used (for example a hold that expired unused on your side).

Step 7 — Edit (optional)

PATCH /reservations/{reservationId}
{ "stopDateTime": "2026-10-22T09:00:00+07:00" }

Editable: type, ownerId, participantIds, the four early/late window fields, startDateTime / stopDateTime, passcode, info, consigneeRoomId. status / nextStatus are not accepted here — use Step 6.

POST /reservations/{id}/change-asset (move the hold to another locker) and POST /reservations/{id}/compensate (issue a goodwill compensation; emits LOCKER_COMPENSATION_ISSUED) are staff operations requiring RESERVATION_UPDATE / RESERVATION_COMPENSATE; partner apps normally do not call them.


5. Timed transitions — what changes without a call

  • Daily check, 00:45 property-local time. For every active locker reservation with a duration quota, Keyspace computes the days left. One day before the limit (configurable) LOCKER_VIOLATION_WARNING fires; at or past the limit a violation is recorded and LOCKER_VIOLATION_CREATE fires. The reservation itself stays active — the locker is still occupied — but the holder can no longer create new reservations until it is returned. Details: quota guide §4, violations guide §3.
  • Reminders. LOCKER_BOOK_REMINDER before the hold's expiry.
  • System cancels. When a meeting-room booking that hands over a locker is cancelled or re-planned, its locker reservation is cancelled with cancelReason: "Auto-cancelled".

Re-read the reservation (GET /reservations/{id}) whenever you need to show its state after a long gap, or consume the events.


6. Error handling strategy

HTTPcodeWhenDo
400KS001validation (missing assetId, bad ISO date, status in a PATCH)fix the request; details in message
401KS002token missing / expired / revokedre-authenticate
503KS234credential check temporarily unavailableretry with backoff; keep your tokens
404KS005reservation not found, or not active when you tried to change itre-read; it was already finished
404KS013assetId does not existrefresh the locker list
409KS012time-slot conflict on the assetpick another window or locker
409KS019holder blacklisted / has a violation / quota full; or a transition the locker's state does not allowbranch on message; see §3
409KS020locker not available right nowoffer another locker
409KS022cancel / done while the locker is in-usedreturn via POST /lockers/{id} action: "done"
409KS063 / KS064locker not reservable / paired locker busyoffer another locker
422KS046consigneeRoomId is not in the locker's projectfix the id
502KS228useLocker: true and the device did not confirm; reservation auto-cancelledtell the user nothing was stored

7. Timezones

Send ISO-8601 with an explicit offset (+07:00). The daily check and notification times use the property's local clock (Asia/Bangkok for today's properties).


8. Receiving events on your server (webhooks)

Reservation changes surface as locker events — LOCKER_BOOK_CREATE, LOCKER_BOOK_REMINDER, LOCKER_BOOK_CANCEL, LOCKER_OPEN, LOCKER_RETURN — with the payload fields listed in the lockers guide §7. reservationId is present on LOCKER_BOOK_REMINDER, LOCKER_BOOK_CANCEL and LOCKER_RETURN, optional on LOCKER_OPEN, and absent on LOCKER_BOOK_CREATE — key your reconciliation on it where present and fall back to lockerTitle + userName + timestamp otherwise. Delivery rules: getting started §5.


9. Sandbox testing checklist

  • POST /reservations (assetType: "locker", type: "once") → 201/200 with status: "active"; the locker shows reserved
  • GET /reservations/{id} and GET /reservations?ownerId=…&status=active both return it
  • PATCH /reservations/{id} extends stopDateTime; a PATCH carrying status is refused with 400
  • POST /reservations/{id}/cancel → cancelled; locker active; LOCKER_BOOK_CANCEL received
  • Reserve again, use the locker, then try …/cancel → 409 KS022; return with POST /lockers/{id} done → reservation done
  • Reserve for the same holder beyond maxReservation → 409 KS019 "Quota is full"
  • (if you use act_as) the delegated member can read and cancel their own reservation without RESERVATION_UPDATE
  • Clean up: no reservation you created is left active

10. Go-live checklist

  • Every reservation you create carries the real holder as ownerId (or is created with act_as), so quotas and violations land on the right person
  • Your UI distinguishes the three KS019 messages and explains them in the user's words
  • KS022 is handled by offering return, not by retrying cancel
  • depositCode / pickupCode are shown once and never logged or stored in plain text
  • Production webhook registered and verified; reservationId used for reconciliation
  • You re-read reservations after the property's 00:45 daily check before showing "still holding" state

11. Getting help & reporting discrepancies

Send your Keyspace contact the environment, the request (method, path, body with secrets removed), the full response envelope and the reservationId. The machine-readable contract is /docs/openapi.json; the platform reference is /llms-full.txt.

On this page