One Lease, every Interface
One grammar reaches the Lease on the API, SDK, CLI, MCP and Docs. Every Verb returns one Action, and every refusal names its fix.
- Reads
Noun.Lease- Generates
- 16 outputs
The Lease
Leaseclock's Lease file declares four fields and two Verbs. Every Interface reaches them with one grammar.
| Operation | API | SDK ($) | CLI | MCP |
|---|---|---|---|---|
| List and find | GET …/leases?status=active&sort=endsOn | $.Lease.list() · $.Lease.find({ status: 'active' }) | show /<your-domain>/leases | do runs the $ line |
| Retrieve | GET …/leases/4b | $.Lease.get('4b') | show /<your-domain>/leases/4b | fetch, or do |
| Create | POST …/leases | $.Lease.create({ unit, renter, endsOn }) | do Lease.create | do |
| Update | PATCH …/leases/4b | $.Lease.update('4b', { endsOn }) | do Lease.update 4b | do |
| Replace | PUT …/leases/4b | — | — | — |
| Delete, kept as a Version | DELETE …/leases/4b | — | — | — |
| Revert | GET and POST …/leases/4b/revert | $.Lease.revert('4b', { to }) | revert Lease 4b | do |
| renew | POST …/leases/4b/renew | $.Lease.renew('4b', { endsOn }) | do Lease.renew 4b | do |
| end | POST …/leases/4b/end | $.Lease.end('4b') | do Lease.end 4b | do |
| In all | 18 operations: Lease 10, Unit 8 | 14 methods: Lease 8, Unit 6 |
Two rows are yours, renew and end; every Noun gets the other seven. … stands for https://api.sb/<your-domain>, and the CLI reads your Startup from SB_STARTUP, which login exports.
Leaseclock's Docs render each operation as a row like the two below, generated from the same file. The request changes with the Interface; the answer does not.
Find the Leases that match a filter, sorted by any indexed field.
- Input
- A filter on any field, and
sortonendsOn - Output
- The matching Leases
- Refuses
401 Unauthorized
curl -G "https://api.sb/leaseclock.com/leases" -H "authorization: Bearer $SB_TOKEN" \
--data-urlencode status=active --data-urlencode sort=endsOn \
--data-urlencode 'endsOn[lte]=2026-11-30'{
"api": "https://api.sb/leaseclock.com",
"leases": [
{
"$id": "https://api.sb/leaseclock.com/leases/4b",
"$type": "Lease",
"unit": "4b",
"renter": "maya.ortiz@example.com",
"endsOn": "2026-11-15",
"status": "active"
}
]
}Move the end date forward. Runs from active, in try or do.
- Input
endsOn, a date, required: the new end date- Output
- One Action
- Refuses
422 Refused·401 Unauthorized
curl -X POST "https://api.sb/leaseclock.com/leases/4b/renew" -H 'Prefer: mode=try' \
-H "authorization: Bearer $SB_TOKEN" -H 'content-type: application/json' \
-d '{"endsOn":"2027-11-15"}'{
"name": "you renew Lease 4B",
"verb": "renew",
"object": "https://api.sb/leaseclock.com/leases/4b",
"by": "owner@leaseclock.com",
"in": "leaseclock.com",
"mode": "try",
"status": "Done",
"input": { "endsOn": "2027-11-15" },
"event": "Lease.renewed",
"closes": [],
"stored": false,
"events": []
}The Interfaces
Five Interfaces read the one definition. The hub's Reference documents each once; below is what your files add.
API
Every Noun becomes a Collection at https://api.sb/<your-domain>/<collection>, described by an OpenAPI document generated from your files. An indexed: true field adds its sort and its range filter, ?sort=endsOn&endsOn[lte]=2026-11-30. API reference ↗
SDK
$ is typed from the definition: $.Lease with 8 methods and $.Unit with 6. What you write as says: becomes the doc comment your editor and your agent read. TypeScript, Zod and JSON Schema come from the same compile, published under your name. SDK reference ↗
CLI
show reads any record by its path, and do runs any Verb. A do previews in try and commits with --do. CLI reference ↗
MCP
Three tools, search, fetch and do, never one tool per Verb. do runs the same $ code, in try unless it passes { mode: 'do' }, and a script commits all or nothing. Your Customers' agents get the same three tools on your domain. MCP reference ↗
Docs
A page per Noun and per Verb, built from the rows above, with a Markdown twin of every page for agents. Every example runs on every Interface at build. Interface reference ↗
What ran
Every Verb returns one Action, on every Interface:
curl -s 'https://api.sb/$.d.ts' | grep -A7 '^interface Action'
# interface Action {
# $id?: string; id?: string; name: string; verb: string; object: string; by: string; in: string
# mode: Mode; status: 'Pending' | 'Done' | 'Refused' | 'Failed' | 'Canceled'; reason?: string; says?: string
# input: { [k: string]: unknown }; event: string; closes: string[]; stored: boolean
# events: { n: number; name: string; at: string; subject: string; data?: unknown }[]
# /** a committed Action's undo, as its Startup's Actions list names it ($.<Noun>.revert(<its $id>) runs it) */
# revert?: string
# }Three sentences say what ran, and each one reads a field:
| The answer | It says |
|---|---|
| A GET on a Verb's URL, which returns a preview and no Action | Nothing ran. |
"stored": false | Ran once. Stored nothing. |
revert present | Done. Revert with …/revert. |
A Verb call outside try carries an Idempotency-Key, and a replay with the same key returns the same Action. The App says the same three sentences: the Renew dialog's preview reads "Nothing ran.", a commit shows "Done." with Undo, and on an ended Lease both buttons are disabled with the reason from from.
Refusals
Each condition has one code on every Interface. Each code links its entry in the hub's refusal catalog, which names the fix.
| Call | Code | Message, first clause |
|---|---|---|
| Any write with no credential | 401 Unauthorized | "sign in, or send a key" |
POST …/leases/2a/renew, an ended Lease | 422 Refused | "renew runs from active; this Lease is ended" |
The 201st POST …/units on Leaseclock Pro | 403 Limit exceeded | "Leaseclock Pro holds up to 200 units" |
A Noun file with properties: | 400 Invalid input | "properties" is not part of a definition |
Access
Sign-in
npx startup-builder login opens your browser, then exports SB_TOKEN and SB_STARTUP. The API, SDK, CLI and MCP take the same token, and your agent acts at your Access level: owner, admin, member or viewer. The API enforces it once; a screen only reflects it. Your Customers sign in on your domain (Customer sign-in).
Environments
Every Startup runs in Live, Test and Dev. Test and Dev are full copies whose books never post, reached by adding ~test or ~dev to your domain: https://api.sb/<your-domain>~test/leases. An Access level is set in Live and applies in every environment.
Export
One export call returns your records as JSON, CSV or Parquet, scoped to a Tenant when a Customer asks for theirs. You keep your four files, your records and your Stripe account.