---
url: https://saas.dev/docs/interfaces
md: https://saas.dev/docs/interfaces.md
reads: [Noun.Lease]
generates: 16 of 34 outputs
next: https://saas.dev/docs/app.md
---

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

## The Lease

Leaseclock's Lease file declares four fields and two [Verbs](https://startupbuilder.dev/business/nouns/verb). Every [Interface](https://startupbuilder.dev/business/nouns/interface) reaches them with one grammar.

| Operation | API | SDK (`$`) | CLI |
| --- | --- | --- | --- |
| List and find | `GET …/leases?status=active&sort=endsOn` | `$.Lease.list()` · `$.Lease.find({ status: 'active' })` | `show /leaseclock.com/leases` |
| Retrieve | `GET …/leases/4b` | `$.Lease.get('4b')` | `show /leaseclock.com/leases/4b` |
| Create | `POST …/leases` | `$.Lease.create({ unit, renter, endsOn })` | `do Lease.create` |
| Update | `PATCH …/leases/4b` | `$.Lease.update('4b', { endsOn })` | `do Lease.update 4b` |
| Replace | `PUT …/leases/4b` | REST only | REST only |
| Delete, kept as a Version | `DELETE …/leases/4b` | REST only | REST only |
| Revert | `GET` and `POST …/leases/4b/revert` | `$.Lease.revert('4b', { to })` | `revert Lease 4b` |
| **renew** | `POST …/leases/4b/renew` | `$.Lease.renew('4b', { endsOn })` | `do Lease.renew 4b` |
| **end** | `POST …/leases/4b/end` | `$.Lease.end('4b')` | `do Lease.end 4b` |
| **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. `…` is [your base URL](https://saas.dev/docs/interfaces.md#base-urls). On MCP, `do` runs the SDK line, and `fetch` also retrieves by `$id`. Replace and Delete are REST only; on `$` and the CLI, `update` changes a record, and every change keeps a Version.

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.

### `Lease.find`

Find the Leases that match a filter, sorted by any indexed field.

Input:

- `unit` (link to Unit): A Unit, by its slug
- `renter` (email): An address, exact
- `endsOn` (date): A date, or a range: `endsOn[lte]`, `endsOn[gte]`
- `status` (choice): `active` or `ended`, or several with `in`
- `sort` (field): An indexed field: `endsOn`
- `limit` (number): How many, 1 to 200; 25 by default
- `after` (cursor): The next page: the `meta.next` of the page before

- Output: The matching Leases, with `meta.total` and `meta.next`
- Refuses: [`400 Invalid input`](https://startupbuilder.dev/code/api/errors/invalid-input) · [`401 Unauthorized`](https://startupbuilder.dev/code/api/errors/unauthorized)

**API**

```bash title="GET /leaseclock.com/leases"
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'
```

**SDK**

```ts title="TypeScript"
await $.Lease.find({ status: 'active', endsOn: { $lte: '2026-11-30' } }, { sort: 'endsOn' })
```

**CLI**

```bash title="Terminal"
npx startup-builder show '/leaseclock.com/leases?status=active&endsOn[lte]=2026-11-30'
```

**MCP**

```json title="do"
{ "name": "do", "arguments": { "in": "leaseclock.com", "code": "return $.Lease.find({ status: 'active', endsOn: { $lte: '2026-11-30' } })" } }
```

```json title="200 · application/json"
{
  "leases": [
    {
      "$id": "https://api.sb/leaseclock.com/leases/4b",
      "$type": "Lease",
      "unit": "4b",
      "renter": "maya.ortiz@example.com",
      "endsOn": "2026-11-15",
      "status": "active"
    }
  ],
  "meta": { "total": 1 }
}
```

### `Lease.renew`

Move the end date forward. Runs from `active`.

Input:

- `endsOn` (date, required): The new end date
- `mode` (try | do): `try` runs it and stores nothing; `do` commits

- Output: One [Action](https://saas.dev/docs/interfaces.md#what-ran)
- Refuses: [`422 Refused`](https://startupbuilder.dev/code/api/errors/refused) · [`401 Unauthorized`](https://startupbuilder.dev/code/api/errors/unauthorized)

**API**

```bash title="POST /leaseclock.com/leases/4b/renew"
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"}'
```

**SDK**

```ts title="TypeScript"
await $.Lease.renew('4b', { endsOn: '2027-11-15' }, { mode: 'try' })
```

**CLI**

```bash title="Terminal"
npx startup-builder do Lease.renew 4b --json '{"endsOn":"2027-11-15"}'
```

**MCP**

```json title="do"
{ "name": "do", "arguments": { "in": "leaseclock.com", "code": "return $.Lease.renew('4b', { endsOn: '2027-11-15' })" } }
```

```json title="200 · application/json"
{
  "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](https://startupbuilder.dev/business/nouns/collection) at `https://api.sb/leaseclock.com/<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 ↗](https://startupbuilder.dev/code/api)

### SDK

`$` is typed from the definition: `$.Lease` with 8 methods and `$.Unit` with 6, each carrying [its `does:` and `says:` lines](https://saas.dev/docs/definition.md#verbs). TypeScript, Zod and JSON Schema come from the same compile, published under your name. [SDK reference ↗](https://startupbuilder.dev/code/sdk)

### CLI

`show` reads any record by its path, and `do` runs any Verb, committing with `--do`. [CLI reference ↗](https://startupbuilder.dev/code/cli)

### MCP

Three tools, `search`, `fetch` and `do`, never one tool per Verb. `do` runs the same `$` code. A script's writes commit together when it returns, and a script does not read its own writes. [Your Customers' agents](https://saas.dev/docs/customers.md#agents) get the same three tools on your domain. [MCP reference ↗](https://startupbuilder.dev/code/mcp)

```json title="MCP config"
{ "mcpServers": { "api.sb": { "url": "https://api.sb/mcp", "headers": { "authorization": "Bearer ${SB_TOKEN}" } } } }
```

### 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 ↗](https://startupbuilder.dev/code/interfaces)

## What ran

**Try mode** runs a call in full and stores nothing. Each Interface asks for it in its own way:

### Modes

| Interface | Without a mode | Preview | Commit |
| --- | --- | --- | --- |
| API | Commits | `Prefer: mode=try` | No `Prefer` header |
| SDK (`$`) | Previews | `{ mode: 'try' }` | `{ mode: 'do' }` |
| CLI | Previews | `do Lease.renew 4b` | `do Lease.renew 4b --do` |
| MCP | Previews | `do`, as written | `{ mode: 'do' }` inside the script |
| The App | Previews in the dialog | The dialog | The button |

A Verb call that commits carries an `Idempotency-Key`, and a replay with the same key returns the same Action.

Every Verb returns one [Action](https://startupbuilder.dev/business/nouns/action), on every Interface:

```bash title="Terminal"
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
# }
```

One sentence says what ran, and each one reads a field:

| The answer | It says |
| --- | --- |
| A GET on a Verb's URL: the Action it would create, `"status": "Pending"` | **Nothing ran.** |
| `"mode": "try"`, `"stored": false` | **Ran once. Stored nothing.** |
| `"stored": true`, `"status": "Pending"` | **Waiting on approval.** |
| `revert` present | **Done. Revert with `…/revert`.** |

[The App](https://saas.dev/docs/app.md#buttons) shows the same sentences.

## 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 | `SbError.code` | Message, first clause |
| --- | --- | --- | --- |
| Any write with no credential | [`401 Unauthorized`](https://startupbuilder.dev/code/api/errors/unauthorized) | `NoPermission` | "sign in, or send a key" |
| `POST …/leases/2a/renew`, an ended Lease | [`422 Refused`](https://startupbuilder.dev/code/api/errors/refused) | `WrongState` | "renew runs from active; this Lease is ended" |
| The 201st `POST …/units` on Leaseclock Pro | [`403 Limit exceeded`](https://startupbuilder.dev/code/api/errors/limit-exceeded) | `limit` | "Leaseclock Pro holds up to 200 units" |
| A Noun file with `properties:` | [`400 Invalid input`](https://startupbuilder.dev/code/api/errors/invalid-input) | `invalid_input` | `"properties" is not part of a definition` |
| A commit on an older head that overlaps it | [`409 Conflict`](https://startupbuilder.dev/code/api/errors/conflict) | `version_conflict` | "rebased, or refused where the two overlap; send baseVersionId" |

Every refusal is `application/problem+json` with `type`, `status`, `title`, `code` and `detail`; [the agent loop](https://saas.dev/docs/how-it-works.md#constraints) shows a whole one. Inside `do`, a refusal throws `SbError` with its `code`, `status` and the `action`.

## Access

### Base URLs

| Who calls | Base URL | Key | A Lease's `$id` |
| --- | --- | --- | --- |
| You and your agent | `https://api.sb/leaseclock.com` | `SB_TOKEN` | `https://api.sb/leaseclock.com/leases/4b` |
| Your Customers and their agents | `https://leaseclock.com/api` | `LEASECLOCK_KEY` | `https://leaseclock.com/api/leases/4b` |
| You, in Test or Dev | `https://api.sb/leaseclock.com~test`, `~dev` | `SB_TOKEN` | `https://api.sb/leaseclock.com~test/leases/4b` |

One record, two addresses: its `$id` follows the host you call, and both reach the same Lease. Put your domain where these say leaseclock.com.

### Sign-in

`npx startup-builder login` opens your browser, then exports `SB_TOKEN` and `SB_STARTUP`; the CLI reads your Startup from `SB_STARTUP`. The API, SDK, CLI and MCP take the same token, and your agent acts at your [Access level](https://startupbuilder.dev/business/nouns/access-level): owner, admin, member or viewer. Your Customers sign in on your domain ([Customer sign-in](https://saas.dev/docs/customers.md#sign-in)).

### Environments

Every Startup runs in Live, Test and Dev. Test and Dev are full copies whose books never post, at the `~test` and `~dev` [base URLs](https://saas.dev/docs/interfaces.md#base-urls). 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](https://startupbuilder.dev/business/nouns/tenant) when a Customer asks for theirs. You keep your four files, your records and your Stripe account.

Read next: [The App](https://saas.dev/docs/app.md)