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
| 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 |
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.
ownerIdis 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.participantIdsare additional people allowed to open the door. - Status (
status):active→done(returned) orcancelled. Only anactivereservation can be changed. - Type (
type):once(one hold with a start/stop),multi,period,permanent(no stop),series,session, anddelivery(parcel hand-over on a deposit/pickup pair — yields one-time codes). Useonceunless your Keyspace contact configured something else for the property. - Time window.
startDateTime/stopDateTime(ISO-8601 with offset). OptionalearlyStartDateTime/lateStartDateTime/earlyStopDateTime/lateStopDateTimewiden 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 asassetStatus. - Envelope.
{ "code": "KS000", "message": "...", "data": ... }(+metaon lists).
3. The reservation lifecycle
status | Locker is | You may call |
|---|---|---|
active, locker reserved | held, unused | PATCH (edit), POST …/cancel, POST /lockers/{id} use / open |
active, locker in-used | occupied | POST /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 |
done | returned | read only |
cancelled | freed | read 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:
| Check | Refusal |
|---|---|
| Holder has an active blacklist | 409 KS019 "You are in the blacklist" |
| Holder has an active violation | 409 KS019 "You have violated the policy" |
Holder's active locker count ≥ maxReservation for their role | 409 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=50Filters: 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
durationquota, Keyspace computes the days left. One day before the limit (configurable)LOCKER_VIOLATION_WARNINGfires; at or past the limit a violation is recorded andLOCKER_VIOLATION_CREATEfires. The reservation itself staysactive— 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_REMINDERbefore 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
| HTTP | code | When | Do |
|---|---|---|---|
| 400 | KS001 | validation (missing assetId, bad ISO date, status in a PATCH) | fix the request; details in message |
| 401 | KS002 | token missing / expired / revoked | re-authenticate |
| 503 | KS234 | credential check temporarily unavailable | retry with backoff; keep your tokens |
| 404 | KS005 | reservation not found, or not active when you tried to change it | re-read; it was already finished |
| 404 | KS013 | assetId does not exist | refresh the locker list |
| 409 | KS012 | time-slot conflict on the asset | pick another window or locker |
| 409 | KS019 | holder blacklisted / has a violation / quota full; or a transition the locker's state does not allow | branch on message; see §3 |
| 409 | KS020 | locker not available right now | offer another locker |
| 409 | KS022 | cancel / done while the locker is in-used | return via POST /lockers/{id} action: "done" |
| 409 | KS063 / KS064 | locker not reservable / paired locker busy | offer another locker |
| 422 | KS046 | consigneeRoomId is not in the locker's project | fix the id |
| 502 | KS228 | useLocker: true and the device did not confirm; reservation auto-cancelled | tell 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/200withstatus: "active"; the locker showsreserved -
GET /reservations/{id}andGET /reservations?ownerId=…&status=activeboth return it -
PATCH /reservations/{id}extendsstopDateTime; a PATCH carryingstatusis refused with400 -
POST /reservations/{id}/cancel→cancelled; lockeractive;LOCKER_BOOK_CANCELreceived - Reserve again,
usethe locker, then try…/cancel→409KS022; return withPOST /lockers/{id}done→ reservationdone - Reserve for the same holder beyond
maxReservation→409KS019"Quota is full" - (if you use
act_as) the delegated member can read and cancel their own reservation withoutRESERVATION_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 withact_as), so quotas and violations land on the right person - Your UI distinguishes the three
KS019messages and explains them in the user's words -
KS022is handled by offering return, not by retrying cancel -
depositCode/pickupCodeare shown once and never logged or stored in plain text - Production webhook registered and verified;
reservationIdused 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.
Locker Quotas
Version 1.15.0 · The narrative guide to the limits a property places on locker use — how many lockers a member may hold at once, for how many days, and the per-role rules that turn an overdue…
Locker Violations & Blacklist
Version 1.15.0 · The narrative guide for third-party apps whose users hold Keyspace lockers: what a violation is, how repeated violations become a blacklist, how both lift, how to read and (for…