API spec
Every rule a client of api.sb can rely on.
api.sb serves every 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.
https: //api.sb/ {namespace}
https: //api.sb/ acme.comAuthentication
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.
curl https: //api.sb/ acme.com/ offers \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"Routes
| Operation | Method | Path | Body |
|---|---|---|---|
| create | POST | / | The properties |
| list | GET | / | Query: limit, after |
| get | GET | / | None. |
| update | PATCH | / | The properties to change |
| delete | DELETE | / | None. |
| A Verb on one record | POST | / | The other inputs |
| A Verb on the collection | POST | / | 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:. Responses are application/; errors are application/.
Ids
A record's $id is its URL. Its id is the last segment: offer_1 in https:. Every input that names a record takes either form.
| Verb | Names its record | Route |
|---|---|---|
get, update, delete | id | / |
| Every other Verb | Its Noun in camelCase: offer, payRun | / |
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("/ 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= |
send | The call returns a pending Action at once and runs durably. | ?mode= |
do | The call runs durably and returns when the Action finishes. | ?mode= |
A GET on a Verb route returns its preview, the same as ?mode=. 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. |
{
"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:
{
"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/. Its type is the URL of the error page; fix and next say what to do. The error catalog lists them all.
{
"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=. The webhooks section of openapi.json types every delivery.
{
"$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. Each Startup also serves its own at https:.