API
How api.sb works, traced through one data business.
The StartupBuilder API serves every Startup at api.sb as plain HTTPS and JSON. Every route is compiled from a Verb, so learning one route teaches them all.
One namespace per Startup
A Startup's namespace is its Domain. openhours.dev sells verified opening hours for four million storefronts to developers and the coding agents that work for them, at $0.002 per lookup. Every record it owns lives under https:.
The namespace holds the platform's Nouns, such as Offers and Orders, and the Startup's own. openhours.dev declares a Storefront Noun, so /openhours.dev/ serves its data. https: describes both, and its Customers generate clients from it.
Routes are compiled from Verbs
Nobody writes a route by hand. Each Noun gets a collection named by its plural, and each Verb gets a POST route named by itself:
| Call | Route | Name |
|---|---|---|
| List storefronts | GET / | Storefront.list |
| Read one Offer | GET / | Offer.get |
| Place an Order | POST / | Order.place |
| Subscribe to an Offer | POST / | Offer.subscribe |
A Verb that acts on an existing record takes its id in the path. A Verb that brings a record into being sits on the collection. GET on any Verb route previews it and stores nothing.
The operation name is the contract. Offer.subscribe is the OpenAPI operationId, the SDK method, the CLI command, the MCP call, and the rate-limit key. Adding a Verb to a Noun adds all five, its Event, and its docs row in one write, so no Interface lags another.
Keys
Every request carries a key as a bearer token. The key names the namespace, and the Permissions of its Role decide which Verbs it may run. Limits bound how far each one goes.
curl "https: //api.sb/ openhours.dev/ storefronts? city= Lisbon& limit= 20" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"A key that starts sk_test_ reaches the test environment, where no money moves and no message leaves. A sk_live_ key reaches live. Code moves from one to the other unchanged, as authentication and environments explain.
Reads return an envelope
Every response is one JSON object with the same keys. Reading an Offer returns the record under its Noun's name, plus links to read and Verbs to run:
{
"api": "https: //api.sb/ openhours.dev",
"$type": "Offer",
"$id": "https: //api.sb/ openhours.dev/ offers/ offer_1",
"offer": {
"$id": "https: //api.sb/ openhours.dev/ offers/ offer_1",
"$type": "Offer",
"price": { "amount": 0.002, "currency": "USD" },
"interval": "month",
"terms": "Verified opening hours for any storefront, billed per lookup."
},
"links": {
"self": "https: //api.sb/ openhours.dev/ offers/ offer_1",
"up": "https: //api.sb/ openhours.dev/ offers"
},
"actions": { "subscribe": "https: //api.sb/ openhours.dev/ offers/ offer_1/ subscribe" }
}actions lists only the Verbs this caller may run on this record in its current state. A client that follows actions never calls a Verb the record refuses, and an agent never needs the state rules in its prompt. The envelope covers lists, cursors, and every key.
Writes return an Action
Every write is an Action: a durable record of one Verb, its input, its status, and who ran it for whom. The response carries the Action beside the record it changed:
curl -X POST https: //api.sb/ openhours.dev/ offers/ offer_1/ subscribe \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Idempotency-Key: signup-91c2" \
-H "Content-Type: application/ json" \
-d '{"quantity": 1}'{
"api": "https: //api.sb/ openhours.dev",
"action": {
"$id": "https: //api.sb/ openhours.dev/ actions/ action_1",
"operation": "Offer.subscribe",
"status": "done"
},
"subscription": {
"$id": "https: //api.sb/ openhours.dev/ subscriptions/ subscription_1",
"$type": "Subscription",
"status": "active"
},
"links": { "self": "https: //api.sb/ openhours.dev/ actions/ action_1" }
}The Mode decides when the caller hears back. try runs and stores nothing, send returns a pending Action at once, and do waits for the finish. A pending Action names its reason, such as awaitingInput or awaitingTime, so the caller knows whether to wait or act.
An Action over a Limit stays pending until its Approval is decided. A retry with the same Idempotency-Key returns the first Action and runs nothing. Modes covers each case.
Errors name the fix
Every error is an RFC 9457 problem whose type is a page on this site. Its fix and next fields carry the remedy, so an agent recovers without a search. A test subscription to an Offer that is still a draft is refused:
{
"type": "https: //startupbuilder.dev/ code/ api/ errors/ refused",
"title": "Refused",
"status": 422,
"detail": "Offer offer_2 is a draft, so nobody can subscribe to it.",
"fix": "Publish the Offer, then subscribe again.",
"next": "https: //api.sb/ openhours.dev/ offers/ offer_2/ publish",
"instance": "https: //api.sb/ openhours.dev/ actions/ action_2"
}A refused Action keeps its record forever. A retry runs a new Action linked to the old one, and history is never rewritten. Errors and testing lists every problem type and how to reproduce it in test.
Events
Every write emits one Event, named by its Noun and the past tense of its Verb: Offer.subscribed, Order.paid. A Watch delivers the Events a selector matches to a URL:
curl -X POST https: //api.sb/ openhours.dev/ watches \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/ json" \
-d '{"selector": "Offer.subscribed", "url": "https: //openhours.dev/ hooks/ signups"}'Each delivery is a signed POST, retried until acknowledged and replayable from any point. Webhooks shows how to verify the signature.
The SDK makes the same calls as typed TypeScript.