---
url: https://saas.dev/docs/sdk
md: https://saas.dev/docs/sdk.md
---

# SDK

`npm i leaseclock` installs a typed client generated from your definition. `$.Lease.renew('4b', { endsOn })` is checked by your editor before it runs. The package is republished whenever the definition changes.

## In short

[An editor showing the leaseclock client's renew method and its hover type](https://leaseclock.com/docs/sdk)

The client is published to npm as `leaseclock`, under your name. It calls `leaseclock.com/api`, its errors link to `leaseclock.com/docs`, and nothing in it names SaaS.Dev. Northgate Rentals' developers use it to script their own leases, and your team uses the same package, with the same permissions as the app.

Each Noun is a property on the client. `$.Lease` has `list`, `get`, `create` and `update`, plus `renew` and `end` from the Lease file, and a Verb you add appears as a method in the next version. Field types, required fields and choice options become TypeScript types. A Verb's `does:` line is its method's hover text, and each input's `says:` line describes that argument.

## Leaseclock's client

Northgate Rentals renews Dana Whitfield's lease on 4B from a script:

```ts title="renew.ts"
import { Leaseclock } from 'leaseclock'

const $ = new Leaseclock({ token: process.env.LEASECLOCK_TOKEN })

const lease = await $.Lease.renew('4b', { endsOn: '2027-11-30' })

lease.endsOn   // '2027-11-30'
lease.status   // 'active'
```

Hovering `renew` in the editor shows the method as Lease.mdx declares it:

```ts title="Hover on renew"
(method) Lease.renew(
  id: string,
  input: { endsOn: string }
): Promise<Lease>

// Move the end date forward. Runs from: active.
// @param input.endsOn  the new end date
```

A misspelled field is caught as it is typed, before anything reaches the API:

```ts title="renew.ts"
await $.Lease.renew('4b', { endsAt: '2027-11-30' })
//                          ~~~~~~
// Object literal may only specify known properties, and 'endsAt'
// does not exist in type '{ endsOn: string; }'.
```

## How to call it

### Install and sign in

```bash title="Terminal"
npm i leaseclock
```

Each caller creates a key in Leaseclock under **Settings → API keys**, sets it as `LEASECLOCK_TOKEN` and passes it to the client. A key acts with its owner's role, so Priya Shah's key reaches Northgate's records and no one else's. A Test key reaches the Test copy, where Stripe charges nothing.

```ts
const $ = new Leaseclock({ token: process.env.LEASECLOCK_TOKEN })
```

### Read and write records

`list` filters on any field and sorts on any field marked `indexed: true`. It returns up to 200 records a page, and `for await` walks every page. Priya's renewal reminder lists the active leases ending this year:

```ts title="ending-soon.ts"
for await (const lease of $.Lease.list({
  status: 'active',
  endsOn: { before: '2027-01-01' },
  sort: 'endsOn',
})) {
  console.log(lease.unit, lease.renter, lease.endsOn)
}
```

```text title="Output"
4b  dana.whitfield@example.com  2026-11-30
2a  marcus.bell@example.com     2026-12-15
```

`get`, `create` and `update` take the same field names as the Lease file. A missing required field is a type error.

```ts
const lease = await $.Lease.get('4b')

await $.Lease.create({ unit: '5a', renter: 'ines.moreau@example.com', endsOn: '2027-10-31' })

await $.Lease.update('4b', { renter: 'dana.whitfield@northgate.example' })
```

### Run a Verb

A Verb's method takes the record's id and the Verb's input, and returns the record after the change. Each call carries an idempotency key, so a retried `renew` after a dropped connection renews 4B once. `end` takes no input:

```ts
await $.Lease.end('2a')
```

### Preview and undo

Pass `{ preview: true }` to see what a call would change. Nothing is saved, and no customer is emailed or charged.

```ts
const preview = await $.Lease.renew('4b', { endsOn: '2027-11-30' }, { preview: true })

preview.changes // { endsOn: ['2026-11-30', '2027-11-30'] }
```

Every write is kept in the record's history, and `undo` reverses the latest one:

```ts
await $.Lease.undo('4b') // endsOn is 2026-11-30 again
```

### Handle errors

Your editor catches wrong names and wrong types. What depends on the data, such as a lease's state or a plan's limit, comes back as a `LeaseclockError` with a status, a code and the fix:

```ts title="renew.ts"
import { Leaseclock, LeaseclockError } from 'leaseclock'

try {
  await $.Lease.renew('5a', { endsOn: '2027-09-30' })
} catch (err) {
  if (err instanceof LeaseclockError) {
    err.status  // 422
    err.code    // 'wrong-state'
    err.message // 'renew runs from active, and Lease 5A is ended'
    err.docs    // 'https://leaseclock.com/docs/errors#wrong_state'
  }
}
```

Every code is listed with its cause and fix in [Errors](https://saas.dev/docs/errors.md).

## Types and schemas

The package exports a type for every Noun and every Verb's input, so your own functions can take a `Lease`:

```ts
import type { Lease } from 'leaseclock'

function reminder(lease: Lease) {
  return `${lease.renter}: your lease on ${lease.unit.toUpperCase()} ends ${lease.endsOn}.`
}
```

```ts title="leaseclock/index.d.ts"
export interface Lease {
  $id: string
  $type: 'Lease'
  unit: string                  // the Unit's id
  renter: string                // an email address
  endsOn: string                // a date, YYYY-MM-DD
  status?: 'active' | 'ended'
}
```

`leaseclock/schemas` holds the same definition as JSON Schema, for checking a form or a file before it is sent. The `says:` line becomes each property's description:

```ts
import { schemas } from 'leaseclock/schemas'

schemas['Lease.renew']
```

```json title="schemas['Lease.renew']"
{
  "type": "object",
  "properties": {
    "endsOn": { "type": "string", "format": "date", "description": "the new end date" }
  },
  "required": ["endsOn"],
  "additionalProperties": false
}
```

The API's OpenAPI file at `leaseclock.com/api/openapi.json` carries the same schemas, so a client in another language agrees with this one.

## New versions

Each push that changes a Noun, a field or a Verb publishes a new version of `leaseclock`, and the changelog entry for that change names it.

- Adding a field, a Verb or a Noun publishes a minor version. When Lease gained `rent`, the new version added `rent: number` to the `Lease` type, beside the changelog entry "Leases now track rent".
- Renaming or removing one publishes a major version, so a project pinned to `^1` keeps building until its owner upgrades.

## Questions

**Do I write or maintain the SDK?**
No. It is generated from your definition on each push and published for you. Overrides you make to the app's screens or theme leave it untouched.

**Where does it run?**
Node, Bun, Deno, edge workers and the browser. In the browser, it uses the signed-in person's session instead of a key, so no key ships to a page.

**Can my customers' agents use it?**
Their agents connect to [MCP](https://saas.dev/docs/mcp.md), whose `do` tool runs the same calls. Agents that write code install `leaseclock` like any other package.

**Does the SDK follow a renamed Noun?**
Yes. Rename Lease to Tenancy and the next major version has `$.Tenancy`, the `Tenancy` type and `schemas['Tenancy.renew']`. The [CLI](https://saas.dev/docs/cli.md) and [API](https://saas.dev/docs/api.md) change in the same push.
