Keyspace Locker Quotas — Integration Guide
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 into a violation. Platform basics are on the getting-started page.
Quotas are enforced when a reservation is created and checked daily while it is held. What happens after the limit is passed is in 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 configuration you read
| 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. GET /projects/{projectId}/config is readable by any
authenticated caller of the property. Writing the configuration
(POST /projects/{projectId}/config) is a property-admin operation — partner
apps read it and ask their Keyspace contact for changes.
2. The five-minute mental model
- Quota row — one entry of
config.locker.quotas[]: for aroleId(or the role-less default row) it setsmaxReservation(how many lockers a member may hold at once) andduration(how many days one locker may be held). The row is matched by the holder's project role; if their role has no row the default row applies; if there is none, the member is unconfigured = no limit. - Violation row — one entry of
config.locker.violations[], matched the same way:violationsThreshold,violationsResetInDays,blacklistDurationInDays. - Two moments.
maxReservationis enforced at create (POST /reservations).durationis enforced by the daily check: a warning the day before, a violation on the day the limit is reached. - The holder, not the caller. Everything is counted against the
reservation's
ownerId. - Envelope.
{ "code": "KS000", "message": "...", "data": ... }.
3. Reading the configuration
GET /projects/{projectId}/configdata is the property's configuration. The locker part:
{
"locker": {
"defaultTimeoutInSecond": 900,
"deliveryTimeoutInSecond": 3600,
"availableThresholdInPercent": 10,
"dailyNotificationTime": "08:00",
"notifyEmailBeforeExpireInMinutes": 60,
"quotas": [
{ "roleId": null, "maxReservation": 1, "duration": 3, "warningBeforeInDays": 1 },
{ "roleId": "66f1…", "maxReservation": 2, "duration": 7, "warningBeforeInDays": 2 }
],
"violations": [
{ "roleId": null, "violationsThreshold": 3, "violationsResetInDays": 30, "blacklistDurationInDays": 30 }
]
}
}| Field | Meaning | Default when absent |
|---|---|---|
quotas[].roleId | project role the row applies to; null/absent = default row | — |
quotas[].maxReservation | max active locker reservations per holder; 0 = unlimited | unconfigured = unlimited |
quotas[].duration | days one locker may be held, counted from the day its use started | no deadline |
quotas[].warningBeforeInDays | days before the deadline to send LOCKER_VIOLATION_WARNING | 1 |
violations[].violationsThreshold | active violation logs that trigger a blacklist | 3 |
violations[].violationsResetInDays | age after which a violation log stops counting | 30 |
violations[].blacklistDurationInDays | length of an automatic (temporary) blacklist | 30 |
defaultTimeoutInSecond | lifetime of a one-time locker code for ordinary reservations | 900 |
deliveryTimeoutInSecond | lifetime of depositCode / pickupCode on delivery reservations | 3600 |
availableThresholdInPercent | free-locker percentage under which LOCKER_LOW_AVAILABILITY fires | — |
dailyNotificationTime | local time at which scheduled locker notices are sent | property default 08:00 |
Role ids come from the member record (GET /partner/members/{externalUserId}
in the members guide) or from your Keyspace contact.
Cache the config for a few minutes at most; staff change it without notice.
4. The daily check
Runs every day at 00:45, property-local time (Asia/Bangkok for today's
properties), one property at a time. For each active locker reservation whose
holder's quota row has a duration:
daysUsed = floor(days since start-of-day of the reservation's startUsedAt)
daysLeft = duration - daysUsed
daysLeft == warningBeforeInDays => LOCKER_VIOLATION_WARNING (data.daysUntilExpiry)
daysLeft <= 0 => one violation log for this reservation
+ LOCKER_VIOLATION_CREATE (data.daysOverdue, currentViolations, ...)Notes that matter for your UI:
- The clock starts when the locker is first used (
startUsedAt), not when it was reserved. A locker reserved but never opened has no deadline. duration: 3means the locker is fine on days 0, 1 and 2 and overdue from the 00:45 check on day 3.- Exactly one violation per reservation, however long it stays overdue.
- The reservation is not cancelled by the check; the locker remains the holder's until they return it. What the violation blocks is new reservations (violations guide §2).
- The same pass resets old logs and lifts expired temporary blacklists.
5. Enforcement at create — maxReservation
When POST /reservations is called for a locker, Keyspace counts the holder's
reservations with status: "active" and assetType: "locker":
| Situation | Result |
|---|---|
| no quota row for the holder's role and no default row | allowed (unconfigured) |
row with maxReservation: 0 | allowed (unlimited) |
active count < maxReservation | allowed |
active count >= maxReservation | 409 KS019 "Quota is full" |
Blacklist and violation gates run before the count
(violations guide); their refusals are also
409 KS019 with their own message.
Pre-checking from your app. To grey out "reserve" instead of surfacing the 409, count yourself:
GET /reservations?assetType=locker&status=active&ownerId={memberId}&projectId={projectId}&limit=1and compare meta.total (or the page length) with the holder's
maxReservation from §3. Treat the API's answer as authoritative — another
device may have reserved in between.
6. Error handling strategy
| HTTP | code | When | Do |
|---|---|---|---|
| 401 | KS002 | token missing / expired / revoked | re-authenticate |
| 503 | KS234 | credential check temporarily unavailable | retry with backoff |
| 404 | KS005 | unknown projectId on GET …/config | check the id |
| 409 | KS019 | "Quota is full" on POST /reservations | tell the user to return a locker first; show their active holds |
| 409 | KS211 | the property has no locker configuration | ask your Keyspace contact |
| 409 | KS213 | the property has no quota configuration | ask your Keyspace contact |
7. Receiving events on your server (webhooks)
The quota check emits LOCKER_VIOLATION_WARNING and LOCKER_VIOLATION_CREATE;
the escalation emits LOCKER_BLACKLIST_WARNING and LOCKER_BLACKLIST_CREATE;
low stock emits LOCKER_LOW_AVAILABILITY. Payload fields:
violations guide §6
and lockers guide §7.
Delivery rules: getting started §5.
8. Sandbox testing checklist
Ask your Keyspace contact to set your development property's default quota row
to maxReservation: 1, duration: 1, warningBeforeInDays: 1 so every rule is
observable within a day.
-
GET /projects/{projectId}/configreturnslocker.quotas[]andlocker.violations[] - First locker reservation for a test member succeeds; a second →
409KS019"Quota is full" - Returning the first locker lets the second succeed
-
usethe locker, wait for the next 00:45 check →LOCKER_VIOLATION_WARNING(orLOCKER_VIOLATION_CREATEwithduration: 1) received with the expected counters - Your pre-check (§5) and the API agree for a member at the limit
- Clean up: return every locker you used; ask your contact to restore the quota row
9. Go-live checklist
- You read quotas from the production property's config (they differ per environment and per property)
- The config is cached briefly (minutes), never baked into your build
- "Quota is full" is explained with the member's actual active holds, fetched from
GET /reservations - Your reminders, if any, are derived from the property's
duration/warningBeforeInDays, not hard-coded - Webhook listener registered in production and verified
10. Getting help & reporting discrepancies
Send your Keyspace contact the environment, projectId, the member's role id,
the config you read and the full response envelope of the refused call. The
machine-readable contract is /docs/openapi.json; the platform reference is
/llms-full.txt.
Members & Credentials
Version 1.12.0 · For third-party apps that are the system of record for their users — a condo / residence app, an office or HR app — and want Keyspace to enrol those people on the building's…
Reservations
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,…