Self-storage management software
Self-storage management software for customers, units, rentals, billing, arrears and many sites, kept as one operational record instead of four that disagree.
Developer API
A storage management API: 13 endpoints, a scope for each one, key rotation without an outage, one log row per request, and webhooks with per-delivery tracking.
Available · Covered by two test files, with the reference generated from the endpoint list
Direct answer
A self-storage software API lets another system read the inventory, customers, tenancies, bookings, access events and invoices of a site. Codemind Storage publishes 13 endpoints, 10 read and 3 write, each demanding its own scope, each bound to one tenant, and each writing one audit row for every request.
Operator question
Ten endpoints read and three write. The reads cover sites, unit types, units, availability, customers as a list and one at a time, rentals, reservations, access events and invoices. Every list endpoint takes a site filter and a cursor, pages 50 rows by default and 200 at most, and refuses a larger page size rather than quietly clamping it.
The three writes are narrow on purpose: record a payment already received, raise a fee already in the catalogue of that site, and end a tenancy or book notice for a future date. The fee endpoint has no amount field at all. The caller names a fee and the engine reads the price and the tax from the catalogue row, so the blast radius of a compromised integration is bounded by what the operator has already priced.
Operator question
A scope narrows a permission the platform already has; it never invents one. Each scope names the capability a member of staff would need for the same screen, and the endpoint checks both, so granting a partner the rentals feed does not silently hand them the customer directory because the two stand behind one door.
A key holds no role, and several refusals follow from that alone. No scope can write off a balance, issue a partial refund, move a locked-out debtor out or decide a held deposit. Nothing under the API emits a gate code or a tenant access credential, so there is no scope that could grant one. And naming a site the key is not bound to is a 404, never a silently ignored filter that would answer with the other sites of that key.
Operator question
Yes. Rotation mints a successor carrying the binding, scopes and rate limit of its predecessor, and gives the old key a short bounded life, so both work while the consumer deploys the new value. Three rules keep that from becoming a way round the controls.
The plaintext exists for one function call and is shown once. Everything printed afterwards is the public prefix, which reveals nothing about the secret. Revocation is not cached, so a killed key stops working on the next request rather than at the end of a cache lifetime.
Operator question
Every request writes exactly one log row, whatever happened, including a throw. The row carries the public prefix, the route template rather than the request URL, the status, the outcome, the scope the endpoint demanded and how long the handler took. That log sits on the key management screen rather than in a file somebody would have to ask for, because what did this key do is the question that gets asked at the worst possible moment.
The rate limit is a column on the row of the key itself, set per key by the operator, and nothing in the request can influence it.
Operator question
From the endpoint list itself. The reference screen renders the same table the routes are ratcheted against, in both directions, so an endpoint cannot exist without appearing in the reference and cannot appear there without existing. A hand-written reference is correct on the day it is written, is published to people who build against it, and nothing anywhere fails when it stops being true.
The built-in console sends GETs only. The write scopes move real money and end real tenancies with no undo, and a console is a place people click to see what happens; the writes are documented in full, with the idempotency header each one requires, because documenting a thing and firing it from a form are different acts. The console never posts the key back to the application either: the browser holds it and calls the API directly.
Operator question
Outbound webhooks are tracked per delivery, and a refused delivery is a different kind of fact from a slow one. When the hostname of an endpoint resolves to a loopback or private address at send time, the delivery is refused, the refusal is recorded as a security event, and the site is alerted on the first one.
That record is append-only, so a timeout on the next attempt cannot overwrite it: a security record a later network hiccup deletes is not a record. The subscription is never silently auto-disabled either, because turning it off is a decision for a person taken with the refusal in front of them.
At a glance
13 endpoints: 10 read and 3 write. Reviewed 10 September 2026.
| Endpoint | Method | Scope | What it returns or does |
|---|---|---|---|
| /api/v1/sites | GET | sites:read | The sites this key is bound to: code, name, currency, locale and time zone |
| /api/v1/unit-types | GET | unit-types:read | Unit types and street rates, using the size labels the inventory board prints |
| /api/v1/units | GET | units:read | Individual units and their status. No gate key or access credential, under any scope |
| /api/v1/availability | GET | availability:read | How many units of each type could be let now, the count the booking journey uses |
| /api/v1/customers | GET | customers:read | Customers at those sites with their account balances. No national identifiers |
| /api/v1/customers/{id} | GET | customers:read | One customer, with both money figures: account balance, and what is owed on open documents |
| /api/v1/rentals | GET | rentals:read | Live and ended tenancies: which customer has which unit, from when, at what rent |
| /api/v1/reservations | GET | reservations:read | Bookings and where each one got to, from the same live-booking set the staff screens read |
| /api/v1/access | GET | access:read | What the doors did: entries, exits and refusals, as delivered by an access system |
| /api/v1/invoices | GET | invoices:read | Invoices, what is still owed on each, and its bucket on the one ageing ladder |
| /api/v1/payments | POST | payments:record | Record a cash, cheque or bank-transfer payment already received. Takes no card |
| /api/v1/fees | POST | fees:charge | Raise a fee already in the catalogue of that site. The caller names a fee, never an amount |
| /api/v1/rentals/{id}/move-out | POST | rentals:move-out | End a tenancy today, or book notice for a future date |
What this does not do
There is no OpenAPI document to hand a client generator, and there is no sandbox tenant. You build against a real site, with a key you have scoped narrowly and can rotate. That is workable, and it is not the same thing as being given a generated client and a disposable dataset to break.
The API also cannot charge a card. It moves no money at a gateway, and the three writes stay deliberately small: record a payment already received, raise a fee already in the catalogue, end a tenancy. An integration that needs to take money from a tenant needs the booking journey or the payment screens, not this.
Continue through the workflow
Self-storage management software for customers, units, rentals, billing, arrears and many sites, kept as one operational record instead of four that disagree.
Invoice storage tenants, allocate partial payments, prorate mid-period changes, take Stripe card payments and run autopay over a ledger that cannot be edited.
A useful first step
We are building this from the daily work of a real multi-site operation. If your process disagrees with ours, that is the useful conversation.