Guide
6 min read
Describe an API
This guide walks through writing a complete stateanchor.yaml for a realistic API. By the end you will have a working spec that generates TypeScript and Python SDKs, an MCP server, and enables the full gate engine on every push.
The example API
We will describe a user management API with authentication, five endpoints, and two models. This is representative of a real internal service -- simple enough to follow but complex enough to hit the patterns you will encounter in production.
Base URL: https://api.acme.io/v1
Auth: Bearer token in Authorization header
Endpoints:
POST /auth/login → { token, user }
GET /users → User[] (query: page, per_page)
GET /users/{id} → User
POST /users → User (body: name, email, role)
PATCH /users/{id} → User (body: name?, email?, role?)
Models:
User: { id: uuid, name: string, email: string, role: string, created_at: datetime }
LoginResponse: { token: string, user: User }Writing the spec
Create stateanchor.yaml at the root of your repository. Here is the complete spec for the API described above:
service: "acme-user-api"
version: "1.0.0"
server:
base_url: "https://api.acme.io/v1"
auth:
type: bearer
endpoints:
- name: login
method: POST
path: /auth/login
description: "Authenticate a user and return a JWT token"
request_body:
email: string
password: string
returns: LoginResponse
- name: list_users
method: GET
path: /users
description: "List all users with pagination"
parameters:
- name: page
in: query
type: integer
required: false
- name: per_page
in: query
type: integer
required: false
returns: User[]
- name: get_user
method: GET
path: /users/{id}
description: "Get a single user by ID"
parameters:
- name: id
in: path
type: uuid
required: true
returns: User
- name: create_user
method: POST
path: /users
description: "Create a new user"
request_body:
name: string
email: string
role: string
returns: User
- name: update_user
method: PATCH
path: /users/{id}
description: "Update an existing user"
parameters:
- name: id
in: path
type: uuid
required: true
request_body:
name: string
email: string
role: string
returns: User
models:
User:
id:
type: uuid
required: true
name:
type: string
required: true
email:
type: string
required: true
role:
type: string
required: true
created_at:
type: datetime
required: true
LoginResponse:
token:
type: string
required: true
user:
type: User
required: true
outputs:
languages:
- typescript
- python
mcp: trueWalkthrough: every decision explained
Service name and version
The service field is a short identifier used in generated code (class names, package names). Use lowercase with hyphens. The version tracks your API version -- it appears in generated docs and is recorded in every sync run for auditability.
Server and auth
base_url is the production root URL. Generated SDKs use this as the default but allow overriding at initialization. The auth.type determines how the SDK handles authentication -- bearer generates a constructor that accepts a token and attaches it to every request as an Authorization: Bearer ... header.
Supported auth types: bearer, api_key, basic, oauth2, none.
Endpoints
Each endpoint needs at minimum: name, method, and path. The name becomes the method name in the generated SDK (client.listUsers()) so use snake_case and make it descriptive.
Path parameters use {param} syntax and must have a corresponding entry in parameters with in: path and required: true. Query parameters should set required: false unless they are genuinely mandatory.
Request bodies
For POST, PUT, and PATCH endpoints, define the request body as a flat object where each key is a field name and each value is the type. The generated SDK will create a typed interface for the request body. For PATCH endpoints, all fields are treated as optional in the generated code.
Models
Models define reusable data shapes. Every model referenced in returns orrequest_body must be defined here. Field types can reference other models (e.g. type: User for nested objects).
Supported types: string, number, integer,boolean, uuid, datetime, object,array, or another model name. For arrays, use User[] in the returns field.
Outputs
The outputs section controls what StateAnchor generates on each sync. Supported languages: typescript, python, go,rust, php. Set mcp: true to also generate an MCP (Model Context Protocol) server that exposes your API to AI agents.
Common patterns
Pagination
Add page and per_page (or limit and offset) as optional query parameters on list endpoints. The generated SDK will accept them as optional method arguments.
Nested resources
- name: list_project_tasks
method: GET
path: /projects/{project_id}/tasks
parameters:
- name: project_id
in: path
type: uuid
required: true
returns: Task[]File uploads
StateAnchor currently generates JSON-body endpoints. For file upload endpoints, describe them with a note in the description field -- the generated SDK will include a placeholder method with instructions.
Anti-patterns to avoid
Do not include credentials
Never put actual API keys, tokens, or passwords in stateanchor.yaml. Use YOUR_API_KEY placeholders if you want to show examples. The spec file is sent to the generation model -- treat it as semi-public.
Do not duplicate endpoints
Each endpoint must have a unique name. If you have two endpoints at the same path with different methods (GET and POST /users), give them distinct names like list_users and create_user.
Do not omit path parameters
Every {param} in the path must have a matching entry inparameters. Missing path parameters cause validation failures.
Do not use generic names
Avoid names like get, create, update. Include the resource: get_user, create_project, update_task. These names become method names in the generated SDK.
What happens when you push
After committing stateanchor.yaml, every push triggers the sync pipeline:
- Parse -- the spec is validated against the schema. Invalid specs fail immediately.
- IR compile -- the spec is compiled into a canonical Intermediate Representation (IR).
- Gate check -- the IR is diffed against the previous version. Breaking changes are scored.
- Generate -- SDKs and MCP server are generated from the IR.
- Store -- artifacts are SHA-256 hashed and stored with full provenance.
The first push creates the baseline. Subsequent pushes compare against that baseline -- the gate engine only fires on changes, not on the initial spec.
Auto-inference alternative
If your API is already running and serves an OpenAPI document, StateAnchor can generate the spec automatically. Navigate to your project and click Infer Spec -- StateAnchor will probe your API and produce a draft stateanchor.yamlyou can review and commit. See the Getting Started guide for details.