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
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 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 as title + lockerNumber. It belongs to a projectId and sits in a zoneId / floorId / locationId hierarchy.
  • 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). Only active lockers can be reserved.
  • Reservation — the hold. currentReservationId on the locker points at it. Whoever is the reservation's ownerId holds 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; poll GET /locker-operations/{operationId} to learn whether the device confirmed.
  • Envelope. Every response except the token endpoint is wrapped: { "code": "KS000", "message": "...", "data": ... } (+ meta on lists).

3. The locker lifecycle

Locker statusMeansYou may
activeFreeReserve it (POST /reservations)
reservedHeld by a reservation, door not used yetuse or open; cancel the reservation
in-usedDoor was used; contents insideopen again; done to return. Cancelling the reservation is refused (409 KS022) — return with done instead
inactiveOut of service (staff)Nothing — pick another locker
failedHardware faultNothing — 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 }
actionFromToNotes
usereserved / in-usedin-usedMarks the locker occupied. sendCommand: true drives the door; the default false only records the state (kiosk / offline hand-over).
openreserved / in-usedunchangedCommands the door for the current holder. Needs ASSET_OPEN or a delegated member who participates in the active reservation.
donein-usedactiveReturn. Frees the locker, clears the holder, completes the reservation (status: "done").
activate / inactivatestaff—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:

statusMeans
pendingQueued, not yet sent to the device
mqtt-sentSent; waiting for the device to acknowledge
completedDevice confirmed
failedDevice 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=50

Useful 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.

HTTPcodeWhenDo
401KS002token missing / expired / revokedre-authenticate
503KS234credential check temporarily unavailableretry with backoff; keep your tokens
404KS005unknown operationIdlog; the id you stored is wrong
404KS041unknown lockerIdrefresh the list
404KS013reservation's asset not foundrefresh the list
409KS019action not allowed for the locker's status; quota full; holder blacklisted / has an active violationread message; see the quota / violations guides
409KS020locker is not available right nowoffer another locker
409KS022tried to cancel/finish a reservation whose locker is in-usedreturn it with POST /lockers/{id} action: "done"
409KS063this locker cannot be reserved (service locker, not public, wrong type)offer another locker
409KS064the paired locker is busyoffer another pair
409KS080the door could not be opened nowpoll the operation if present; retry once
422KS046the room you referenced is not in the locker's projectfix the ids
502KS228reserve+use: device did not confirm the door; reservation auto-cancelledtell 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.

typeFiresKey data fields
LOCKER_BOOK_CREATEa locker was reservedlockerTitle, zoneName, floorName?, locationName?, expiryDate, userName?
LOCKER_BOOK_REMINDERreminder before the hold expiresreservationId, lockerTitle, zoneName, expiryDate, userName?
LOCKER_BOOK_CANCELreservation cancelled — user, staff or systemreservationId, lockerTitle, zoneName, cancelledAt, cancelReason? ("Auto-cancelled" = system), cancelReasonDetail?, userName?, userEmail?
LOCKER_OPENthe door was commanded openreservationId?, lockerTitle, zoneName, userName? (pinCode is redacted; attached photos are dropped)
LOCKER_RETURNlocker returned (done)reservationId, lockerTitle, zoneName, returnDate, userName
LOCKER_LOW_AVAILABILITYfree lockers in a zone fell under the configured percentagezoneName, 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=active returns at least one locker in your projectId
  • Reserve it (POST /reservations, assetType: "locker") → locker shows status: "reserved" and currentReservationId
  • POST /lockers/{id} { "action": "use", "sendCommand": true } → in-used; the returned operation reaches completed or failed
  • POST /lockers/{id} { "action": "done", "sendCommand": true } → active; reservation is done
  • Reserve again, then cancel the reservation → locker is active, LOCKER_BOOK_CANCEL received
  • Try to cancel a reservation while the locker is in-used → 409 KS022
  • Reserve a locker twice for the same holder past maxReservation → 409 KS019 "Quota is full"
  • Your listener acked LOCKER_BOOK_CREATE, LOCKER_OPEN, LOCKER_RETURN within 10 s and deduped a redelivery
  • Clean up: every reservation you created is done or cancelled

9. Go-live checklist

  • Production app_id / app_secret stored 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
  • KS228 and KS080 paths show the user a clear "nothing stored / try another locker" message
  • KS022 is handled by offering return, not by retrying cancel
  • Production webhook registered; signature verified; pinCode never 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.

On this page