Keyspace Lockers — Integration Guide
Version 1.15.0 · The narrative guide for third-party apps that let their users find, hold, open and return Keyspace smart lockers. Platform basics (credentials, token, envelope, webhooks) are on the getting-started page.
A locker is held through a reservation — the booking object shared by every locker flow. This guide covers the locker itself (what it is, its states, the door). The reservation journey (create → use → return / cancel) is in the reservations guide; the hold limits and what happens when a locker is kept too long are in the quota guide and the violations & blacklist guide.
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 use
| 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 serves its own copy of this guide — always read the docs from the environment you are integrating against.
Permissions. Your app's token carries a project role. Locker calls check that
role for ASSET_VIEW (read), ASSET_USE (use / return) and ASSET_OPEN (open
the door); reservation calls check RESERVATION_CREATE / RESERVATION_VIEW /
RESERVATION_UPDATE. Ask your Keyspace contact to grant what your flows need.
When you act as a member (act_as, getting started §4),
the member may also open a locker they hold without any extra role: being a
participant of the locker's active reservation is enough.
2. The five-minute mental model
- Locker — one physical compartment. Identified by
lockerId; shown to people astitle+lockerNumber. It belongs to aprojectIdand sits in azoneId/floorId/locationIdhierarchy. - Type (
type) —locker-normal,locker-cooler,locker-warmer,locker-uv,mailbox,locker-deposit,locker-pickup. Deposit/pickup lockers come in pairs used for parcel hand-over (see §3.3). - Status (
status) —active(free),reserved(held, door not yet used),in-used(something is inside),inactive(taken out of service by staff),failed(hardware fault). Onlyactivelockers can be reserved. - Reservation — the hold.
currentReservationIdon the locker points at it. Whoever is the reservation'sownerIdholds the locker; that member's quota and violation record are the ones that count. - Locker operation — one door command sent to the hardware. When a call
drives the door you get an
operationId; pollGET /locker-operations/{operationId}to learn whether the device confirmed. - Envelope. Every response except the token endpoint is wrapped:
{ "code": "KS000", "message": "...", "data": ... }(+metaon lists).
3. The locker lifecycle
Locker status | Means | You may |
|---|---|---|
active | Free | Reserve it (POST /reservations) |
reserved | Held by a reservation, door not used yet | use or open; cancel the reservation |
in-used | Door was used; contents inside | open again; done to return. Cancelling the reservation is refused (409 KS022) — return with done instead |
inactive | Out of service (staff) | Nothing — pick another locker |
failed | Hardware fault | Nothing — pick another locker |
3.1 Reserve
A locker is never reserved directly — create a reservation with
assetType: "locker" and the locker's id as assetId
(reservations guide, Step 3).
The locker flips active → reserved, currentReservationId is set and
LOCKER_BOOK_CREATE fires.
Before anything is minted Keyspace checks the holder (the reservation's
ownerId): an active blacklist or violation refuses with 409 KS019
("You are in the blacklist" / "You have violated the policy"), and a full
quota refuses with 409 KS019 "Quota is full". Details:
violations guide, quota guide.
3.2 Use, open, return — POST /lockers/{lockerId}
{ "action": "use", "sendCommand": true }action | From | To | Notes |
|---|---|---|---|
use | reserved / in-used | in-used | Marks the locker occupied. sendCommand: true drives the door; the default false only records the state (kiosk / offline hand-over). |
open | reserved / in-used | unchanged | Commands the door for the current holder. Needs ASSET_OPEN or a delegated member who participates in the active reservation. |
done | in-used | active | Return. Frees the locker, clears the holder, completes the reservation (status: "done"). |
activate / inactivate | staff | — | Service actions; not for partner apps. |
Any other action/status pair answers 409 KS019. The response data is the
locker after the transition.
Reserve and use in one call. POST /reservations with "useLocker": true
reserves and immediately runs use with the door command. If the device does
not confirm the door, Keyspace cancels the just-created reservation and
answers 502 KS228 — nothing was stored, tell the user to try another locker.
Its data carries lockerId, lockerStatus, deliveryState, failureReason
and the command timestamps for your logs.
3.3 Deposit / pickup pairs and parcel lockers
A locker-deposit is paired with a locker-pickup. A delivery reservation on
such a pair returns two one-time codes, depositCode (for the courier) and
pickupCode (for the resident), both in the POST /reservations and
GET /reservations/{id} responses. Relay them to the right person and never
log them — the same field is redacted from webhooks. The pair is managed as a
unit: when the reservation ends both lockers return to active. Choosing a
locker whose pair is busy answers 409 KS064.
3.4 Tracking a door command — GET /locker-operations/{operationId}
Flows that command the hardware (meeting-room bookings that hand over a locker,
use / open) return a lockerOperation: { id, status }. Poll it until it
settles:
status | Means |
|---|---|
pending | Queued, not yet sent to the device |
mqtt-sent | Sent; waiting for the device to acknowledge |
completed | Device confirmed |
failed | Device refused or timed out — mqttInfo.errorMessage says why; replacementOperationId is set when Keyspace retried with another locker |
mqttInfo carries successful, sentAt, ackedAt, errorMessage. An unknown
id is 404 KS005. Poll every 1–2 s for up to ~30 s; after that treat the
operation as failed and show the user a retry.
4. Step by step
Step 1 — Authenticate
Same for every module: POST /auth/accessToken with
grant_type: "client_credentials" (or act_as to act for a member) →
Authorization: Bearer <access_token>. Full rules:
getting started §3.
Step 2 — Find a free locker
GET /lockers?projectId={projectId}&zoneId={zoneId}&lockerStatuses=active&lockerTypes=locker-normal&isPublic=true&page=1&limit=50Useful filters: zoneId, floorId, locationIds[], lockerTypes[],
lockerStatuses[], size, isPublic, charged, title, lockerNumber,
ids[]. data is a page of lockers (id, title, lockerNumber, type,
status, size, isPublic, charged, zoneId, floorId, locationId,
currentReservationId, pictureUrl), meta holds the paging.
GET /lockers/list/summary with the same filters returns counts per status —
handy for an availability badge. GET /lockers/{lockerId} reads one locker.
Availability changes without you: between your list and your reserve another
user may take the locker. Reserve the user's pick straight away and handle
409 KS020 / KS063 by offering the next one.
Step 3 — Hold it
POST /reservations — see the
reservations guide. Keep
the returned reservation.id.
Step 4 — Open the door
POST /lockers/{lockerId}
{ "action": "open" }Or { "action": "use", "sendCommand": true } the first time, which also flips
the locker to in-used. Then poll the operation (§3.4) and only show "door
opened" on completed.
Step 5 — Return
POST /lockers/{lockerId}
{ "action": "done", "sendCommand": true }The locker goes back to active, the reservation becomes done, and
LOCKER_RETURN fires. If the user changes their mind before using the locker,
cancel the reservation instead
(reservations guide, Step 6).
5. Error handling strategy
Every error is the same envelope with a non-KS000 code. Branch on code,
show message only as a fallback.
| HTTP | code | When | Do |
|---|---|---|---|
| 401 | KS002 | token missing / expired / revoked | re-authenticate |
| 503 | KS234 | credential check temporarily unavailable | retry with backoff; keep your tokens |
| 404 | KS005 | unknown operationId | log; the id you stored is wrong |
| 404 | KS041 | unknown lockerId | refresh the list |
| 404 | KS013 | reservation's asset not found | refresh the list |
| 409 | KS019 | action not allowed for the locker's status; quota full; holder blacklisted / has an active violation | read message; see the quota / violations guides |
| 409 | KS020 | locker is not available right now | offer another locker |
| 409 | KS022 | tried to cancel/finish a reservation whose locker is in-used | return it with POST /lockers/{id} action: "done" |
| 409 | KS063 | this locker cannot be reserved (service locker, not public, wrong type) | offer another locker |
| 409 | KS064 | the paired locker is busy | offer another pair |
| 409 | KS080 | the door could not be opened now | poll the operation if present; retry once |
| 422 | KS046 | the room you referenced is not in the locker's project | fix the ids |
| 502 | KS228 | reserve+use: device did not confirm the door; reservation auto-cancelled | tell the user nothing was stored; offer another locker |
Idempotency: done on an already-active locker is 409 KS019 — treat it as
success if your own record says you already returned it.
6. Timezones
All timestamps are ISO-8601 with offset. Daily locker checks and reminders run on the property's local clock (Asia/Bangkok for today's properties) — see the quota guide §4.
7. Receiving events on your server (webhooks)
Register a listener with your Keyspace contact and subscribe to the locker
types. Delivery rules (HMAC signature, 10-second ack, 5 retries,
X-Keyspace-Delivery dedupe) are on the
getting-started page §5.
type | Fires | Key data fields |
|---|---|---|
LOCKER_BOOK_CREATE | a locker was reserved | lockerTitle, zoneName, floorName?, locationName?, expiryDate, userName? |
LOCKER_BOOK_REMINDER | reminder before the hold expires | reservationId, lockerTitle, zoneName, expiryDate, userName? |
LOCKER_BOOK_CANCEL | reservation cancelled — user, staff or system | reservationId, lockerTitle, zoneName, cancelledAt, cancelReason? ("Auto-cancelled" = system), cancelReasonDetail?, userName?, userEmail? |
LOCKER_OPEN | the door was commanded open | reservationId?, lockerTitle, zoneName, userName? (pinCode is redacted; attached photos are dropped) |
LOCKER_RETURN | locker returned (done) | reservationId, lockerTitle, zoneName, returnDate, userName |
LOCKER_LOW_AVAILABILITY | free lockers in a zone fell under the configured percentage | zoneName, availableSlots, totalSlots, availablePercent, thresholdPercent, hierarchySummary? |
Violation and blacklist events (LOCKER_VIOLATION_*, LOCKER_BLACKLIST_*,
LOCKER_COMPENSATION_ISSUED) are listed in the
violations guide §6.
Every payload also carries projectId, userId and timestamp.
8. Sandbox testing checklist
Run against https://api.keyspace-dev.com with your development credentials.
-
GET /lockers?lockerStatuses=activereturns at least one locker in yourprojectId - Reserve it (
POST /reservations,assetType: "locker") → locker showsstatus: "reserved"andcurrentReservationId -
POST /lockers/{id}{ "action": "use", "sendCommand": true }→in-used; the returned operation reachescompletedorfailed -
POST /lockers/{id}{ "action": "done", "sendCommand": true }→active; reservation isdone - Reserve again, then cancel the reservation → locker is
active,LOCKER_BOOK_CANCELreceived - Try to cancel a reservation while the locker is
in-used→409KS022 - Reserve a locker twice for the same holder past
maxReservation→409KS019"Quota is full" - Your listener acked
LOCKER_BOOK_CREATE,LOCKER_OPEN,LOCKER_RETURNwithin 10 s and deduped a redelivery - Clean up: every reservation you created is
doneorcancelled
9. Go-live checklist
- Production
app_id/app_secretstored server-side only; never in a client build - Your app's production project role has exactly the
ASSET_*/RESERVATION_*permissions you use — nothing more - Door feedback is driven by the operation status, never by the HTTP 200 of the state call
-
KS228andKS080paths show the user a clear "nothing stored / try another locker" message -
KS022is handled by offering return, not by retrying cancel - Production webhook registered; signature verified;
pinCodenever persisted from events - You re-read the locker (
GET /lockers/{id}) after any failed command before letting the user retry - Read the quota and violations guides so your UI explains refusals in the user's own words
10. Getting help & reporting discrepancies
If the API behaves differently from this guide, send your Keyspace contact the
environment, the request (method, path, body with secrets removed), the full
response envelope and the X-Keyspace-Delivery id of any related event. The
machine-readable contract is /docs/openapi.json; the platform reference is
/llms-full.txt.
Getting started
Version 1.12.0 · The partner "start here" for every Keyspace integration: credentials, authentication, the response envelope, webhooks — and the index of per-module guides. For field-by-field…
Meeting Rooms & Desks
Version 1.15.3 · 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.