Skip to content

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.

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.

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

Routes

OperationMethodPathBody
createPOST/{namespace}/{nouns}The properties
listGET/{namespace}/{nouns}Query: limit, after
getGET/{namespace}/{nouns}/{id}None.
updatePATCH/{namespace}/{nouns}/{id}The properties to change
deleteDELETE/{namespace}/{nouns}/{id}None.
A Verb on one recordPOST/{namespace}/{nouns}/{noun}/{verb}The other inputs
A Verb on the collectionPOST/{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.

VerbNames its recordRoute
get, update, deleteid/{nouns}/{id}
Every other VerbIts 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

ModeMeaningAsk for it
tryThe caller waits for the result and nothing is stored.?mode=try
sendThe call returns a pending Action at once and runs durably.?mode=send
doThe 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.

KeyHolds
apiThe namespace URL.
$typeThe Noun of the record or list.
$idThe canonical URL of the record.
<noun>, <nouns>The record, or one page of records, under its Noun name.
actionThe Action a write ran, with its status.
metaThe page: limit and the after cursor.
linksNavigation: self, up, next.
actionsThe 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 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. Each Startup also serves its own at https://api.sb/acme.com/openapi.json.