# Routing

URL: /code/routing · Id: `page:code/routing`

> How each file in a Startup project becomes URLs, paths and names on every Interface, with no route table to maintain.

Every route in a [Startup](/business/nouns/startup) derives from its files. A [Noun](/business/nouns/noun) gets API paths, a [Site](/business/nouns/site) folder gets URLs, an [App](/business/nouns/app) gets screens, and a [Workflow](/business/nouns/workflow) gets a name on every [Interface](/business/nouns/interface).

## API
| Call | Route |
| --- | --- |
| List a [Collection](/business/nouns/collection) | `GET /{ns}/{plural}` |
| Read a record | `GET /{ns}/{plural}/{id}` |
| Create | `POST /{ns}/{plural}` |
| Run a [Verb](/business/nouns/verb) | `POST /{ns}/{plural}/{id}/{verb}` |
| Run a Collection Verb | `POST /{ns}/{plural}/{verb}` |
| A record as Markdown | `GET /{ns}/{plural}/{id}.md` |
| A record at a past moment | `GET /{ns}/{plural}/{id}@2026-10-01` |

A [Verb](/business/nouns/verb) is a path segment, never `:verb` or `.verb`. The dot appears only in names: `$.Offer.publish`, `do Offer.publish`, the MCP program and the OpenAPI `operationId`. A record or Collection named like a declared Verb is refused at write time.

```bash
curl -X POST https://api.sb/leaseline.app/offers/offer_1/publish \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"
```

## Sites
| [File](/business/nouns/file) or folder | URL |
| --- | --- |
| `sites/www/page.mdx` | `/` |
| `sites/www/pricing/page.mdx` | `/pricing` |
| `sites/www/(legal)/security/page.mdx` | `/security` |
| `sites/www/blog/[post]/page.mdx` | `/blog/:post`, one [Page](/business/nouns/page) per member |
| `sites/www/pricing/@annual/page.mdx` | `/pricing`, as an [Experiment](/business/nouns/experiment) [Variant](/business/nouns/variant) |
| `sites/www/_drafts/` | none |
| `sites/www/not-found.mdx` | every unmatched path below |

Every Site also serves a Markdown twin of each Page, `/llms.txt`, `/agents.json`, `/sitemap.xml` and `/openapi.json`. `/terms` and `/privacy` are generated and locked.

## Apps and Interfaces
| Path | Serves |
| --- | --- |
| `/app/{plural}` | An App's list screen |
| `/app/{plural}/{id}` | One record |
| `/app/{plural}/{id}/{verb}` | The form for one Verb |
| `/api` | The API on the Startup's own [Domain](/business/nouns/domain) |
| `/docs`, `/blog`, `/dashboard`, `/admin` | Docs, the blog Collection, [Dashboards](/business/nouns/dashboard), the admin App |
| `https://api.sb/mcp` | MCP, scoped by namespace |

The same App URL returns HTML, JSON or Markdown, by reader. The CLI and the SDK have no route.

## Workflows
A Workflow's path in `workflows/` is its name everywhere. `workflows/renewals/notice-at-60-days.mdx` is `$.renewals.noticeAt60Days` in the SDK and `renewals.noticeAt60Days` in MCP.

## Custom routes
Bespoke routes are the exception. A need that seems to call for one first changes the generic parts: records, Verbs, saved queries or MCP. When none serves, `startup.ts` routes a path to a program in `code/` and states why:

```ts
routes: { '/webhooks/partner': { to: 'code/partner-webhook', reason: 'The partner posts form-encoded webhooks that no Verb accepts.' } }
```

The check refuses a route with no reason. There is no `routes/` folder and no route file.