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

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 a roleId (or the role-less default row) it sets maxReservation (how many lockers a member may hold at once) and duration (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. maxReservation is enforced at create (POST /reservations). duration is 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}/config

data 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 }
    ]
  }
}
FieldMeaningDefault when absent
quotas[].roleIdproject role the row applies to; null/absent = default row—
quotas[].maxReservationmax active locker reservations per holder; 0 = unlimitedunconfigured = unlimited
quotas[].durationdays one locker may be held, counted from the day its use startedno deadline
quotas[].warningBeforeInDaysdays before the deadline to send LOCKER_VIOLATION_WARNING1
violations[].violationsThresholdactive violation logs that trigger a blacklist3
violations[].violationsResetInDaysage after which a violation log stops counting30
violations[].blacklistDurationInDayslength of an automatic (temporary) blacklist30
defaultTimeoutInSecondlifetime of a one-time locker code for ordinary reservations900
deliveryTimeoutInSecondlifetime of depositCode / pickupCode on delivery reservations3600
availableThresholdInPercentfree-locker percentage under which LOCKER_LOW_AVAILABILITY fires—
dailyNotificationTimelocal time at which scheduled locker notices are sentproperty 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: 3 means 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":

SituationResult
no quota row for the holder's role and no default rowallowed (unconfigured)
row with maxReservation: 0allowed (unlimited)
active count < maxReservationallowed
active count >= maxReservation409 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=1

and 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

HTTPcodeWhenDo
401KS002token missing / expired / revokedre-authenticate
503KS234credential check temporarily unavailableretry with backoff
404KS005unknown projectId on GET …/configcheck the id
409KS019"Quota is full" on POST /reservationstell the user to return a locker first; show their active holds
409KS211the property has no locker configurationask your Keyspace contact
409KS213the property has no quota configurationask 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}/config returns locker.quotas[] and locker.violations[]
  • First locker reservation for a test member succeeds; a second → 409 KS019 "Quota is full"
  • Returning the first locker lets the second succeed
  • use the locker, wait for the next 00:45 check → LOCKER_VIOLATION_WARNING (or LOCKER_VIOLATION_CREATE with duration: 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.

On this page