# API spec

URL: /code/api/spec · Id: `page:code/api/spec`

> Every rule a client of api.sb can rely on.

[api.sb](/code/platform/api-sb) serves every [Startup](/business/nouns/startup) under its namespace, one route per operation, with one JSON envelope on every response.

### Base URL
Every Startup has a namespace, its domain. Every route sits under it.

```text
https://api.sb/{namespace}
https://api.sb/acme.com
```

### Authentication
Send a key as a bearer token. Test keys start with `sk_test_`, live keys with `sk_live_`. A key acts in its own namespace and scopes only.

```bash
curl https://api.sb/acme.com/offers \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"
```

### Routes
| Operation | Method | Path | Body |
| --- | --- | --- | --- |
| create | POST | `/{namespace}/{nouns}` | The properties |
| list | GET | `/{namespace}/{nouns}` | Query: `limit`, `after` |
| get | GET | `/{namespace}/{nouns}/{id}` | None. |
| update | PATCH | `/{namespace}/{nouns}/{id}` | The properties to change |
| delete | DELETE | `/{namespace}/{nouns}/{id}` | None. |
| A Verb on one record | POST | `/{namespace}/{nouns}/{noun}/{verb}` | The other inputs |
| A Verb on the collection | POST | `/{namespace}/{nouns}/{verb}` | The inputs |

`{nouns}` is the plural in kebab case. The operationId of every route is its `Noun.verb` name.

A body is JSON, sent with `Content-Type: application/json`. Responses are `application/json`; errors are `application/problem+json`.

### Ids
A record's `$id` is its URL. Its id is the last segment: `offer_1` in `https://api.sb/acme.com/offers/offer_1`. Every input that names a record takes either form.

| Verb | Names its record | Route |
| --- | --- | --- |
| `get`, `update`, `delete` | `id` | `/{nouns}/{id}` |
| Every other Verb | Its Noun in camelCase: `offer`, `payRun` | `/{nouns}/{noun}/{verb}` |

When a call uses a record an earlier call returned, it passes the returned id on: `offer.$id` in the SDK and MCP, `jq -r '.offer["$id"] | split("/") | last'` in a shell.

### Deleting
`delete` is the one Verb that removes a record, and its history is kept. No Noun has an `archive` Verb. To stop something without removing it, call the Verb that ends it: `Offer.withdraw` closes an Offer to new Customers and keeps every Subscription.

### Modes
| Mode | Meaning | Ask for it |
| --- | --- | --- |
| `try` | The caller waits for the result and nothing is stored. | `?mode=try` |
| `send` | The call returns a `pending` Action at once and runs durably. | `?mode=send` |
| `do` | The call runs durably and returns when the Action finishes. | `?mode=do` |

A `GET` on a Verb route returns its preview, the same as `?mode=try`. Writes default to `do`. A `send` call answers `202` with the `pending` Action.

### Envelope
Every response is one JSON object: the result under its own key, plus navigation.

| Key | Holds |
| --- | --- |
| `api` | The namespace URL. |
| `$type` | The Noun of the record or list. |
| `$id` | The canonical URL of the record. |
| `<noun>`, `<nouns>` | The record, or one page of records, under its Noun name. |
| `action` | The Action a write ran, with its status. |
| `meta` | The page: `limit` and the `after` cursor. |
| `links` | Navigation: `self`, `up`, `next`. |
| `actions` | The Verbs the caller can run on the record, by URL. |

```json
{
  "api": "https://api.sb/acme.com",
  "$type": "Offer",
  "$id": "https://api.sb/acme.com/offers/offer_1",
  "offer": {
    "$id": "https://api.sb/acme.com/offers/offer_1",
    "$type": "Offer",
    "price": {
      "amount": 49,
      "currency": "USD"
    }
  },
  "links": {
    "self": "https://api.sb/acme.com/offers/offer_1",
    "up": "https://api.sb/acme.com/offers"
  },
  "actions": {
    "accept": "https://api.sb/acme.com/offers/offer_1/accept",
    "grant": "https://api.sb/acme.com/offers/offer_1/grant",
    "publish": "https://api.sb/acme.com/offers/offer_1/publish"
  }
}
```

A write returns its Action beside the result:

```json
{
  "api": "https://api.sb/acme.com",
  "offer": {
    "$id": "https://api.sb/acme.com/offers/offer_1",
    "$type": "Offer",
    "price": {
      "amount": 49,
      "currency": "USD"
    }
  },
  "action": {
    "$id": "https://api.sb/acme.com/actions/action_1",
    "operation": "Offer.publish",
    "status": "done"
  },
  "links": {
    "self": "https://api.sb/acme.com/actions/action_1"
  }
}
```

### Pagination
A list returns at most `limit` records, newest first. Pass `links.next`, or its `after` cursor, for the next page.

### Idempotency
Send an `Idempotency-Key` header on any write. A repeat with the same key returns the first Action and runs nothing.

### Errors
Every error is an RFC 9457 problem served as `application/problem+json`. Its `type` is the URL of the error page; `fix` and `next` say what to do. The [error catalog](/code/api/error-catalog) lists them all.

```json
{
  "type": "https://startupbuilder.dev/errors/not-found",
  "title": "Not found",
  "status": 404,
  "detail": "No record, Noun, or Verb exists at the path, or the caller cannot see it.",
  "fix": "Check the namespace and the id, or search for the record first.",
  "instance": "https://api.sb/acme.com/actions/action_1"
}
```

### Webhooks
Every write emits an Event named `Noun.pastTense`. A webhook receives each delivery as a signed `POST`. The `StartupBuilder-Signature` header holds a timestamp `t` and one or more `v1` signatures, each a hex HMAC-SHA256 of `t`, a dot and the raw body: `t=1792670400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd`. The `webhooks` section of openapi.json types every delivery.

```json
{
  "$id": "https://api.sb/acme.com/events/event_1",
  "$type": "Event",
  "name": "Offer.published",
  "at": "2026-10-22T12:00:00Z",
  "seq": 1,
  "data": {
    "offer": {
      "$id": "https://api.sb/acme.com/offers/offer_1",
      "$type": "Offer",
      "price": {
        "amount": 49,
        "currency": "USD"
      }
    }
  }
}
```

### OpenAPI
The whole API as one OpenAPI 3.1 document: [openapi.json](https://startupbuilder.dev/openapi.json). Each Startup also serves its own at `https://api.sb/acme.com/openapi.json`.