# APIs.Dev: every page > Build, launch and monetize API products in a few lines of code. The API, CLI, MCP, SDK, docs, portal and Stripe billing are generated for you. ## Contents 1. Skip the plumbing tax. (/): line 17, 837 tokens 2. Start a Dataset API (/quickstart/dataset-api): line 75, 3,419 tokens 3. Start a Headless SaaS (/quickstart/headless-saas): line 409, 7,107 tokens 4. Patterns (/patterns): line 1190, 5,495 tokens 5. Dataset API (/patterns/dataset-api): line 1728, 1,715 tokens 6. Headless SaaS (/patterns/headless-saas): line 1910, 3,115 tokens 7. Workflow (/workflow): line 2272, 2,622 tokens 8. What every product gets (/every-product): line 2534, 4,512 tokens 9. Reference (/reference): line 2964, 15,200 tokens --- title: "Skip the plumbing tax." url: / --- # Skip the plumbing tax. > Build, launch and monetize API products in a few lines of code. The API, CLI, MCP, SDK, docs, portal and Stripe billing are generated for you. ## The plumbing tax. To sell one API, you build ten other products first. Here you write one file, and the ten are generated from it. ```ts title="definition.ts" import { define, secret } from 'apis.dev' /** Any company by its domain, with its headcount and sector. */ export default define({ name: 'companyfacts', domain: 'companyfacts.apis.dev', sources: { firms: { origin: 'https://firms.example.com', key: secret('FIRMS_KEY') }, }, Company: { domain: { type: 'url!#', from: 'firms', map: 'website' }, employees: { type: 'number?', from: 'firms', map: 'headcount' }, sector: { type: 'string?', from: 'firms', map: 'industry' }, }, offer: { 'Company.get': 0.002, 'Company.search': 0.005 }, }) ``` - API keys `Live and Test, in /dashboard` - Rate limits `429 with Retry-After` - Metering `X-Cost on every reply` - Stripe billing `credits and plans, by Checkout` - Docs `/docs` - SDKs `npm i companyfacts` - A CLI `npx companyfacts` - An MCP server `/mcp` - A portal `/dashboard` - A landing page `/` Built by hand, they cost months before your first Customer, an API agents cannot find or call, and revenue left on the table. ## Five things you can sell. *Example prices, in USD.* | Pattern | Instead of | For example | Example price, USD | | --- | --- | --- | --- | | [Dataset API](/patterns/dataset-api.md): Sell the facts about a thing, priced per answer. | Bulk data exports | Any company by its domain: headcount and sector | 0.002 a lookup | | [Headless SaaS](/patterns/headless-saas.md): Sell a home for your Customers' records, priced by plan and seat. | A SaaS app with screens | A pipeline tracker teams log deals in, each team in its own space | Free, or 49 a month for 5 seats | | [Function API](/patterns.md#function-api): Sell an answer, priced per call. | A prompt behind a wrapper app | Two sentences on what any company sells | 0.001 to 5 an answer, by kind | | [Agent API](/patterns.md#agent-api): Sell a worker who does the job, billed by consumption. | An agency retainer | Three meetings a week booked in each Customer's inbox | 0.01 per 1,000 tokens and 0.05 a minute | | [Service API](/patterns.md#service-api): Sell an outcome, charged once, when the work is accepted. | A firm billing by the hour | Each Customer's annual report, filed and accepted | 25 when accepted | ## Run an API business. A live, priced [API product](/every-product.md) under your name, earning from the first call. --- title: "Start a Dataset API" url: /quickstart/dataset-api --- # Start a Dataset API > A live, priced API people and agents pay for in about 30 seconds, written as one file: a Source, a Mapping and a price. You need Node 20 and a GitHub account. The first deploy answers at once on `companyfacts.apis.dev`; your own domain is one line. 1. **Call the sample** ```sh title="Request" curl https://apis.dev/samples/companyfacts/companies/acme.com ``` This is the API you are about to build, answering keyless from this site: the record, its price in `X-Cost`, and a request id. Press Try it, or paste the line into a terminal. 2. **Start the folder** ```sh title="Terminal" $ npx apis.dev init dataset-api ✓ wrote definition.ts: a Dataset API next: npx apis.dev dev ``` `init` writes the file you build here. 3. **Write the file** ```ts title="definition.ts" import { define, secret } from 'apis.dev' /** Any company by its domain, with its headcount and sector. */ export default define({ name: 'companyfacts', domain: 'companyfacts.apis.dev', sources: { // Source: the API you resell firms: { origin: 'https://firms.example.com', key: secret('FIRMS_KEY') }, }, Company: { // Noun: the shape you promise domain: { type: 'url!#', from: 'firms', map: 'website' }, // Mapping employees: { type: 'number?', from: 'firms', map: 'headcount' }, sector: { type: 'string?', from: 'firms', map: 'industry' }, }, offer: { 'Company.get': 0.002, 'Company.search': 0.005 }, // Offer: the price }) ``` Your Source answers in its own field names, and each `map` picks one of them; `!#` makes `domain` the key. The comment above the file is its pitch on every [Interface](/every-product.md). The Source's credential never sits in the file: set it once with `npx apis.dev secrets set FIRMS_KEY`. 4. **Try it locally** ```sh title="Terminal" $ npx apis.dev dev ✓ firms answered in 212 ms; their field → your field: website → domain "acme.com" headcount → employees 240 industry → sector "Software" ✓ every Interface on http://localhost:8787, against your Test twin reloads on every save ``` `dev` asks the Source once and prints the record it maps, then serves every Interface on localhost against your [Test twin](/reference.md#test-twin). 5. **Publish it** ```sh title="Terminal" $ npx apis.dev deploy ✓ signed in with GitHub ✓ definition.ts: companyfacts on companyfacts.apis.dev ✓ Stripe connected: each Customer pays into your own account ✓ 1 Noun, 2 Verbs, 1 Source, 2 prices Company get, search Company.get 0.002 a call Company.search 0.005 a call ✓ published v1 to https://companyfacts.apis.dev: 11 Interfaces REST /companies/{domain}, /companies?sector= CLI npx companyfacts MCP /mcp SDK npm i companyfacts, pip install companyfacts Docs /docs OpenAPI /openapi.json llms.txt /llms.txt agents.json /.well-known/agents.json Pricing /pricing Landing site / Developer portal /dashboard ✓ Test key: companyfacts_test_NNTTj83hyuMNRNk85nzuQNW8h8y3wu33c155 ``` The first run signs you in with GitHub and connects Stripe to your own account. Your product answers at once on `https://companyfacts.apis.dev`, with each Verb, each price and each Interface printed, and a Test key. Set `domain` to a domain of your own and deploy again to move every Interface there; deploy checks it once by a TXT record. 6. **Call it with the Test key** **curl** ```sh title="Request" curl https://companyfacts.apis.dev/companies/acme.com \ -H "Authorization: Bearer $COMPANYFACTS_KEY" ``` **TypeScript** ```ts title="TypeScript" import { Companyfacts } from 'companyfacts' const api = new Companyfacts({ key: process.env.COMPANYFACTS_KEY }) const { data, meta } = await api.getCompany('acme.com') // data: { 'domain': 'acme.com', 'employees': 240, 'sector': 'Software' } // meta.cost: { price: 0.002, currency: 'USD' } ``` **Python** ```python title="Python" import os from companyfacts import Companyfacts api = Companyfacts(key=os.environ["COMPANYFACTS_KEY"]) data, meta = api.get_company("acme.com") # data: { "domain": "acme.com", "employees": 240, "sector": "Software" } # meta.cost: { "price": 0.002, "currency": "USD" } ``` **CLI** ```sh title="Terminal" $ npx companyfacts login --key $COMPANYFACTS_KEY $ npx companyfacts companies get acme.com { "domain": "acme.com", "employees": 240, "sector": "Software" } price 0.002 USD · req_3cd59a9a1ede10c5bf21 ``` **MCP** ```sh title="Request" curl https://companyfacts.apis.dev/mcp \ -H "Authorization: Bearer $COMPANYFACTS_KEY" \ --json '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "getCompany", "arguments": { "domain": "acme.com" } } }' ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0.002, currency="USD" X-Request-Id: req_f9cf5fc65654d907192f { "jsonrpc": "2.0", "id": 1, "result": { "content": [ … ], "structuredContent": { "domain": "acme.com", "employees": 240, "sector": "Software" }, "_meta": { "cost": { "price": 0.002, "currency": "USD" }, "requestId": "req_f9cf5fc65654d907192f" } } } ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0.002, currency="USD" X-Request-Id: req_3cd59a9a1ede10c5bf21 { "domain": "acme.com", "employees": 240, "sector": "Software" } ``` Call with the [Test key](/reference.md#test-key) the first run printed: its calls never bill. A Live [Key](/reference.md#key) answers the same, and bills. 7. **Get paid** ```text title="Stripe receipt · October 2026 · USD" Company.get 12,400 × 0.002 24.80 Company.search 1,300 × 0.005 6.50 ──────────────────────────────────── Total 31.30 Paid from 50.00 of credits 31.30 Balance 18.70 ``` Every Live call your Customers make lands on their Stripe receipt, a line per Verb, and the money lands in your own Stripe account. This is one Customer's month. 8. **See every Interface** *Every Interface, on https://companyfacts.apis.dev* | Interface | Path | | --- | --- | | REST | `/companies/{domain}`, `/companies?sector=` | | CLI | `npx companyfacts` | | MCP | `/mcp` | | SDK | `npm i companyfacts`, `pip install companyfacts` | | Docs | `/docs` | | OpenAPI | `/openapi.json` | | llms.txt | `/llms.txt` | | agents.json | `/.well-known/agents.json` | | Pricing | `/pricing` | | Landing site | `/` | | Developer portal | `/dashboard` | 9. **Change the price** ```ts title="definition.ts" offer: { 'Company.get': 0.002, 'Company.search': 0.005 }, // [!code --] offer: { 'Company.get': 0.003, 'Company.search': 0.005 }, // [!code ++] ``` ```sh title="Terminal" $ npx apis.dev deploy ✓ definition.ts: 1 Noun, 2 Verbs, 1 Source, 2 prices ✓ published v2 to https://companyfacts.apis.dev: every Interface at once ``` *The price on three Interfaces* | Interface | At 0.002 | At 0.003 | | --- | --- | --- | | `price` in `X-Cost` | `price=0.002` | `price=0.003` | | `/pricing` | `"price": 0.002` | `"price": 0.003` | | `/llms.txt` | `($0.002)` | `($0.003)` | Publish again, and all three move at once: the price in your docs is the price on the reply. A higher price is a breaking change: each Key keeps the [Version](/reference.md#version) it first called until it sends `Api-Version`. 10. **Ask for one nobody has** **curl** ```sh title="Request" curl https://companyfacts.apis.dev/companies/nobody.example.com \ -H "Authorization: Bearer $COMPANYFACTS_KEY" ``` **TypeScript** ```ts title="TypeScript" import { Companyfacts } from 'companyfacts' const api = new Companyfacts({ key: process.env.COMPANYFACTS_KEY }) const { error, meta } = await api.getCompany('nobody.example.com') // error: { status: 404, code: 'not_found' } // meta.cost: { price: 0, currency: 'USD' } ``` **Python** ```python title="Python" import os from companyfacts import Companyfacts api = Companyfacts(key=os.environ["COMPANYFACTS_KEY"]) error, meta = api.get_company("nobody.example.com") # error: { "status": 404, "code": "not_found" } # meta.cost: { "price": 0, "currency": "USD" } ``` **CLI** ```sh title="Terminal" $ npx companyfacts login --key $COMPANYFACTS_KEY $ npx companyfacts companies get nobody.example.com error 404 not_found price 0 USD · req_84326ad21bc488454616 ``` **MCP** ```sh title="Request" curl https://companyfacts.apis.dev/mcp \ -H "Authorization: Bearer $COMPANYFACTS_KEY" \ --json '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "getCompany", "arguments": { "domain": "nobody.example.com" } } }' ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_f9cf5fc65654d907192f { "jsonrpc": "2.0", "id": 1, "result": { "isError": true, "content": [ … ], "structuredContent": { "status": 404, "code": "not_found" }, "_meta": { "cost": { "price": 0, "currency": "USD" }, "requestId": "req_f9cf5fc65654d907192f" } } } ``` ```http title="Response" HTTP/1.1 404 Not Found Content-Type: application/problem+json X-Cost: price=0, currency="USD" X-Request-Id: req_84326ad21bc488454616 { "type": "https://companyfacts.apis.dev/docs/errors#not_found", "title": "Not Found", "status": 404, "detail": "No Company has domain nobody.example.com.", "code": "not_found", "instance": "req_84326ad21bc488454616" } ``` A miss answers a [Problem](/reference.md#errors) with its code, and its price is 0. ## Result Your API answers on https://companyfacts.apis.dev: 2 Verbs on 11 Interfaces, each call drawing its posted price from credits your Customers buy through Stripe Checkout, and a miss is free. You wrote no Verb, no docs and no price page. ## Next steps - **[Start a Headless SaaS](/quickstart/headless-saas.md) →** Your Customers write each field. You pick the shape, its lifecycle and the plans. - [Dataset API](/patterns/dataset-api.md): how a field is filled, kept or live. - [What every product gets](/every-product.md): everything else the file gives you. --- title: "Start a Headless SaaS" url: /quickstart/headless-saas --- # Start a Headless SaaS > A live API your Customers keep records in, with plans and seats paid to your Stripe, written as one file of Nouns, moves and plans. You need Node 20 and a GitHub account. The first deploy answers at once on `pipeline.apis.dev`; your own domain is one line. 1. **Start the folder** ```sh title="Terminal" $ npx apis.dev init headless-saas ✓ wrote definition.ts: a Headless SaaS next: npx apis.dev dev ``` 2. **Write the file** ```ts title="definition.ts" import { define } from 'apis.dev' /** A pipeline tracker your Customers log their deals in, each in its own space. */ export default define({ name: 'pipeline', domain: 'pipeline.apis.dev', Company: { // Noun: your Customer writes it name: 'string', domain: 'url!#', deals: '<-Deal.company', // a Relationship }, Deal: { title: 'string', amount: 'decimal(12,2)', company: '->Company.deals', stage: { // Verbs: the lifecycle open: [], qualify: 'opened', win: 'qualified', // []: from no state, so it creates the Deal lose: ['opened', 'qualified'], }, }, offer: { // Offer: plans, and a price past them Free: { month: 0, seats: 1, includes: { 'Deal.open': 25 } }, Team: { month: 49, seats: 5, seat: 9, includes: { 'Deal.open': 1000 } }, 'Deal.open': 0.001, }, }) ``` With no `from`, your Customer writes the field: create, get, list, update, delete and restore. `stage` adds a Verb per move. Free stops at what it `includes`; Team draws the price past it. 3. **Try it locally** ```sh title="Terminal" $ npx apis.dev dev ✓ every Interface on http://localhost:8787, against your Test twin reloads on every save ``` 4. **Publish it** ```sh title="Terminal" $ npx apis.dev deploy ✓ signed in with GitHub ✓ definition.ts: pipeline on pipeline.apis.dev ✓ Stripe connected: each Customer pays into your own account ✓ 2 Nouns, 15 Verbs, 2 plans, 1 price Company create, get, list, update, delete, restore Deal open, get, list, update, delete, restore, qualify, win, lose Free 0 a month, 1 seat, 25 Deal.open included Team 49 a month, 5 seats, 1,000 Deal.open included Deal.open 0.001 a call, past a plan ✓ published v1 to https://pipeline.apis.dev: 11 Interfaces REST /companies, /deals CLI npx pipeline MCP /mcp SDK npm i pipeline, pip install pipeline Docs /docs OpenAPI /openapi.json llms.txt /llms.txt agents.json /.well-known/agents.json Pricing /pricing Landing site / Developer portal /dashboard ✓ Test key: pipeline_test_PUzMCRBrEcNUxGDRDwFcRU42AR8CCc21e958 ``` Deploy counts the Verbs from the file and prints each one by name. Each Customer who signs up gets a space of their own, and every record they write stays in it. 5. **Add a Company** **curl** ```sh title="Request" curl https://pipeline.apis.dev/companies \ -H "Authorization: Bearer $PIPELINE_KEY" \ --json '{"name":"Acme","domain":"acme.com"}' ``` **TypeScript** ```ts title="TypeScript" import { Pipeline } from 'pipeline' const api = new Pipeline({ key: process.env.PIPELINE_KEY }) const { data, meta } = await api.createCompany({ name: 'Acme', domain: 'acme.com' }) // data: { 'id': 'company_1', 'name': 'Acme', 'domain': 'acme.com', … } // meta.cost: { price: 0, currency: 'USD' } ``` **Python** ```python title="Python" import os from pipeline import Pipeline api = Pipeline(key=os.environ["PIPELINE_KEY"]) data, meta = api.create_company(name="Acme", domain="acme.com") # data: { "id": "company_1", "name": "Acme", "domain": "acme.com", … } # meta.cost: { "price": 0, "currency": "USD" } ``` **CLI** ```sh title="Terminal" $ npx pipeline login --key $PIPELINE_KEY $ npx pipeline companies create --name Acme --domain acme.com { "id": "company_1", "name": "Acme", "domain": "acme.com", … } price 0 USD · req_df59a11235e22d4bc20f ``` **MCP** ```sh title="Request" curl https://pipeline.apis.dev/mcp \ -H "Authorization: Bearer $PIPELINE_KEY" \ --json '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "createCompany", "arguments": { "name": "Acme", "domain": "acme.com" } } }' ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_864ae4b67955f0e11948 { "jsonrpc": "2.0", "id": 1, "result": { "content": [ … ], "structuredContent": { "id": "company_1" }, "_meta": { "cost": { "price": 0, "currency": "USD" }, "requestId": "req_864ae4b67955f0e11948" } } } ``` ```http title="Response" HTTP/1.1 201 Created Location: /companies/company_1 X-Action-Id: act_1 X-Cost: price=0, currency="USD" X-Request-Id: req_df59a11235e22d4bc20f { "id": "company_1", "name": "Acme", "domain": "acme.com", "createdAt": "2026-10-24T09:00:00Z", "updatedAt": "2026-10-24T09:00:00Z" } ``` A create answers 201 with the record, where it lives, and the [Action](/workflow.md#the-lifecycle) it ran as. 6. **Open a Deal** **curl** ```sh title="Request" curl https://pipeline.apis.dev/deals \ -H "Authorization: Bearer $PIPELINE_KEY" \ --json '{"title":"Pilot","amount":"5000.00","company":"company_1"}' ``` **TypeScript** ```ts title="TypeScript" import { Pipeline } from 'pipeline' const api = new Pipeline({ key: process.env.PIPELINE_KEY }) const { data, meta } = await api.openDeal({ title: 'Pilot', amount: '5000.00', company: 'company_1' }) // data: { 'id': 'deal_1', 'title': 'Pilot', 'amount': '5000.00', … } // meta.cost: { price: 0, currency: 'USD' } ``` **Python** ```python title="Python" import os from pipeline import Pipeline api = Pipeline(key=os.environ["PIPELINE_KEY"]) data, meta = api.open_deal(title="Pilot", amount="5000.00", company="company_1") # data: { "id": "deal_1", "title": "Pilot", "amount": "5000.00", … } # meta.cost: { "price": 0, "currency": "USD" } ``` **CLI** ```sh title="Terminal" $ npx pipeline login --key $PIPELINE_KEY $ npx pipeline deals open --title Pilot --amount 5000.00 --company company_1 { "id": "deal_1", "title": "Pilot", "amount": "5000.00", … } price 0 USD · req_019e21b5e33a04d5e91e ``` **MCP** ```sh title="Request" curl https://pipeline.apis.dev/mcp \ -H "Authorization: Bearer $PIPELINE_KEY" \ --json '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "openDeal", "arguments": { "title": "Pilot", "amount": "5000.00", "company": "company_1" } } }' ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_cfa756674d6e49366ba7 { "jsonrpc": "2.0", "id": 1, "result": { "content": [ … ], "structuredContent": { "id": "deal_1" }, "_meta": { "cost": { "price": 0, "currency": "USD" }, "requestId": "req_cfa756674d6e49366ba7" } } } ``` ```http title="Response" HTTP/1.1 201 Created Location: /deals/deal_1 X-Action-Id: act_5 X-Cost: price=0, currency="USD" X-Request-Id: req_019e21b5e33a04d5e91e { "id": "deal_1", "title": "Pilot", "amount": "5000.00", "company": "company_1", "stage": "opened", "createdAt": "2026-10-24T09:10:00Z", "updatedAt": "2026-10-24T09:10:00Z" } ``` Opening the Deal wrote `Deal.opened`. Free includes it, so its price is 0. 7. **Get the webhook** **curl** ```sh title="Request" curl https://pipeline.apis.dev/watches \ -H "Authorization: Bearer $PIPELINE_KEY" \ --json '{ "events": ["Deal.opened"], "url": "https://hooks.acme.com/hooks/pipeline" }' ``` **TypeScript** ```ts title="TypeScript" import { Pipeline } from 'pipeline' const api = new Pipeline({ key: process.env.PIPELINE_KEY }) const { data, meta } = await api.createWatch({ events: ['Deal.opened'], url: 'https://hooks.acme.com/hooks/pipeline' }) // data: { 'id': 'watch_1', 'events': ['Deal.opened'], … } // meta.cost: { price: 0, currency: 'USD' } ``` **Python** ```python title="Python" import os from pipeline import Pipeline api = Pipeline(key=os.environ["PIPELINE_KEY"]) data, meta = api.create_watch(events=["Deal.opened"], url="https://hooks.acme.com/hooks/pipeline") # data: { "id": "watch_1", "events": ["Deal.opened"], … } # meta.cost: { "price": 0, "currency": "USD" } ``` **CLI** ```sh title="Terminal" $ npx pipeline login --key $PIPELINE_KEY $ npx pipeline watchs create \ --events '["Deal.opened"]' \ --url https://hooks.acme.com/hooks/pipeline { "id": "watch_1", "events": ["Deal.opened"], … } price 0 USD · req_8db1e5ea781c9709a731 ``` **MCP** ```sh title="Request" curl https://pipeline.apis.dev/mcp \ -H "Authorization: Bearer $PIPELINE_KEY" \ --json '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "createWatch", "arguments": { "events": ["Deal.opened"], "url": "https://hooks.acme.com/hooks/pipeline" } } }' ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_79bcb44af511cb69425f { "jsonrpc": "2.0", "id": 1, "result": { "content": [ … ], "structuredContent": { "id": "watch_1" }, "_meta": { "cost": { "price": 0, "currency": "USD" }, "requestId": "req_79bcb44af511cb69425f" } } } ``` ```http title="Response" HTTP/1.1 201 Created Location: /watches/watch_1 X-Action-Id: act_4 X-Cost: price=0, currency="USD" X-Request-Id: req_8db1e5ea781c9709a731 { "id": "watch_1", "events": ["Deal.opened"], "url": "https://hooks.acme.com/hooks/pipeline", "secret": "whsec_7Kq2vN4xR7mW1pT9", "createdAt": "2026-10-24T09:08:00Z", "updatedAt": "2026-10-24T09:08:00Z" } ``` ```http title="Delivery" POST /hooks/pipeline HTTP/1.1 Host: hooks.acme.com X-Webhook-Attempt: 1 X-Webhook-Signature: t=1792833000,v1=2c067386fcd9006ece95389e2fde364074ad34388decdfc0a31f3edce14d0252 { "id": "evt_2", "type": "Deal.opened", "at": "2026-10-24T09:10:00Z", "seq": 2, "data": { "id": "deal_1", "title": "Pilot", "amount": "5000.00", "company": "company_1", "stage": "opened" } } ``` ```ts title="Receive it" import { createHmac, timingSafeEqual } from 'node:crypto' // verify(request, secret): true when the delivery was signed with your secret in the last 5 minutes export async function verify(request: Request, secret: string) { const header = request.headers.get('X-Webhook-Signature') ?? '' const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('='))) if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false const want = createHmac('sha256', secret).update(`${t}.${await request.text()}`).digest() const got = Buffer.from(v1 ?? '', 'hex') return got.length === want.length && timingSafeEqual(got, want) } // The receiver: verify a copy, then read the Event once by its id. const seen = new Set() export default { async fetch(req: Request) { if (!(await verify(req.clone(), process.env.PIPELINE_WEBHOOK_SECRET!))) return new Response(null, { status: 401 }) const event = await req.json() if (!seen.has(event.id)) seen.add(event.id) // a retry carries the same id return new Response(null, { status: 204 }) }, } ``` Your Customer makes a [Watch](/reference.md#events), and each `Deal.opened` goes out signed, retried until their URL answers. `npx apis.dev events listen --forward localhost:3000/hooks` tests the receiver first. 8. **Win it too early** **curl** ```sh title="Request" curl -X POST https://pipeline.apis.dev/deals/deal_1/win \ -H "Authorization: Bearer $PIPELINE_KEY" ``` **TypeScript** ```ts title="TypeScript" import { Pipeline } from 'pipeline' const api = new Pipeline({ key: process.env.PIPELINE_KEY }) const { error, meta } = await api.winDeal('deal_1') // error: { status: 409, code: 'wrong_state' } // meta.cost: { price: 0, currency: 'USD' } ``` **Python** ```python title="Python" import os from pipeline import Pipeline api = Pipeline(key=os.environ["PIPELINE_KEY"]) error, meta = api.win_deal("deal_1") # error: { "status": 409, "code": "wrong_state" } # meta.cost: { "price": 0, "currency": "USD" } ``` **CLI** ```sh title="Terminal" $ npx pipeline login --key $PIPELINE_KEY $ npx pipeline deals win deal_1 error 409 wrong_state price 0 USD · req_66b1b3cad16f500d936f ``` **MCP** ```sh title="Request" curl https://pipeline.apis.dev/mcp \ -H "Authorization: Bearer $PIPELINE_KEY" \ --json '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "winDeal", "arguments": { "id": "deal_1" } } }' ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_dd2bf757dc2514a44b6c { "jsonrpc": "2.0", "id": 1, "result": { "isError": true, "content": [ … ], "structuredContent": { "status": 409, "code": "wrong_state" }, "_meta": { "cost": { "price": 0, "currency": "USD" }, "requestId": "req_dd2bf757dc2514a44b6c" } } } ``` ```http title="Response" HTTP/1.1 409 Conflict Content-Type: application/problem+json X-Cost: price=0, currency="USD" X-Request-Id: req_66b1b3cad16f500d936f { "type": "https://pipeline.apis.dev/docs/errors#wrong_state", "title": "Conflict", "status": 409, "detail": "A Deal in opened cannot win; win moves from qualified.", "code": "wrong_state", "instance": "req_66b1b3cad16f500d936f", "allowed": ["qualify", "lose"] } ``` A Deal moves only along its lifecycle: `win` needs `qualified` first, and the refusal lists the moves it allows. 9. **Qualify it, then win it** **qualify** ```sh title="Request" curl -X POST https://pipeline.apis.dev/deals/deal_1/qualify \ -H "Authorization: Bearer $PIPELINE_KEY" ``` ```http title="Response" HTTP/1.1 200 OK X-Action-Id: act_6 X-Cost: price=0, currency="USD" X-Request-Id: req_b3219e966dabdd964790 { "id": "deal_1", "title": "Pilot", "amount": "5000.00", "company": "company_1", "stage": "qualified", "createdAt": "2026-10-24T09:10:00Z", "updatedAt": "2026-10-24T09:20:00Z" } ``` **win** ```sh title="Request" curl -X POST https://pipeline.apis.dev/deals/deal_1/win \ -H "Authorization: Bearer $PIPELINE_KEY" ``` ```http title="Response" HTTP/1.1 200 OK X-Action-Id: act_7 X-Cost: price=0, currency="USD" X-Request-Id: req_3baa43ca329668af8de5 { "id": "deal_1", "title": "Pilot", "amount": "5000.00", "company": "company_1", "stage": "won", "createdAt": "2026-10-24T09:10:00Z", "updatedAt": "2026-10-24T09:30:00Z" } ``` Winning wrote `Deal.won`. 10. **Kick off when a Deal is won** ```ts title="definition.ts" import { define, workflow } from 'apis.dev' // [!code ++] Deal: { … kickoff: workflow(async ($, deal) => { // [!code ++] const { plan } = await $.generative('Draft the kickoff plan for {company.name}', deal, // [!code ++] { returns: { plan: 'string' } }) // [!code ++] await $.human('Approve the plan', { plan }, { role: 'manager' }) // [!code ++] const at = await $.step('stamp it', () => new Date().toISOString()) // [!code ++] return { plan, at } // [!code ++] }), // [!code ++] }, on: { 'Deal.won': 'Deal.kickoff' }, // [!code ++] ``` **get** ```sh title="Request" curl https://pipeline.apis.dev/actions/act_8 \ -H "Authorization: Bearer $PIPELINE_KEY" ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_1154ca02bc01e341ac7b { "id": "act_8", "verb": "Deal.kickoff", "status": "pending", "reason": "AwaitingWorker", "poll": "/actions/act_8" } ``` **complete** ```sh title="Request" curl https://pipeline.apis.dev/actions/act_8/complete \ -H "Authorization: Bearer $MANAGER_KEY" \ --json '{"output":{"approved":true}}' ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_5881a5df799b8e82a74b { "id": "act_8", "verb": "Deal.kickoff", "status": "done", "output": { "plan": "Week 1: import. Week 2: first review.", "at": "2026-10-26T15:00:00Z" } } ``` `Deal.won` starts the kickoff once. It waits on a manager, who completes the step with their own [Key](/reference.md#key), and the run ends with its output. See [Workflow](/workflow.md). 11. **Outgrow the Free plan** **curl** ```sh title="Request" curl https://pipeline.apis.dev/deals \ -H "Authorization: Bearer $PIPELINE_KEY" \ --json '{"title":"Renewal","amount":"1200.00","company":"company_1"}' ``` **TypeScript** ```ts title="TypeScript" import { Pipeline } from 'pipeline' const api = new Pipeline({ key: process.env.PIPELINE_KEY }) const { error, meta } = await api.openDeal({ title: 'Renewal', amount: '1200.00', company: 'company_1' }) // error: { status: 402, code: 'plan_limit' } // meta.cost: { price: 0, currency: 'USD' } ``` **Python** ```python title="Python" import os from pipeline import Pipeline api = Pipeline(key=os.environ["PIPELINE_KEY"]) error, meta = api.open_deal(title="Renewal", amount="1200.00", company="company_1") # error: { "status": 402, "code": "plan_limit" } # meta.cost: { "price": 0, "currency": "USD" } ``` **CLI** ```sh title="Terminal" $ npx pipeline login --key $PIPELINE_KEY $ npx pipeline deals open --title Renewal --amount 1200.00 --company company_1 error 402 plan_limit price 0 USD · req_02f4f9b0fd2d3af756ba ``` **MCP** ```sh title="Request" curl https://pipeline.apis.dev/mcp \ -H "Authorization: Bearer $PIPELINE_KEY" \ --json '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "openDeal", "arguments": { "title": "Renewal", "amount": "1200.00", "company": "company_1" } } }' ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_ac0716f42658ea530abb { "jsonrpc": "2.0", "id": 1, "result": { "isError": true, "content": [ … ], "structuredContent": { "status": 402, "code": "plan_limit" }, "_meta": { "cost": { "price": 0, "currency": "USD" }, "requestId": "req_ac0716f42658ea530abb" } } } ``` ```http title="Response" HTTP/1.1 402 Payment Required Content-Type: application/problem+json X-Cost: price=0, currency="USD" X-Request-Id: req_02f4f9b0fd2d3af756ba { "type": "https://pipeline.apis.dev/docs/errors#plan_limit", "title": "Payment Required", "status": 402, "detail": "Free includes 25 Deal.open a month; this is the 26th.", "code": "plan_limit", "instance": "req_02f4f9b0fd2d3af756ba", "offers": [ { "kind": "plan", "name": "Team", "price": 49, "checkout": "https://pipeline.apis.dev/checkout/team" } ] } ``` The call past what Free includes answers 402 with Team in its `offers`; after Checkout, the same call goes through. On Team, a teammate past its seats answers 402 for one more seat. ## Result Your API answers on https://pipeline.apis.dev: 15 Verbs, a wrong move refused with 409, each change sent on a signed webhook, and a Workflow 8 lines long on `Deal.won`. Free includes 25 `Deal.open` a month; the call past them answers 402 with Team on Stripe Checkout. You wrote no CRUD, no state checks, no Events and no webhook code. ```ts title="definition.ts" import { define, workflow } from 'apis.dev' /** A pipeline tracker your Customers log their deals in, each in its own space. */ export default define({ name: 'pipeline', domain: 'pipeline.apis.dev', Company: { // Noun: your Customer writes it name: 'string', domain: 'url!#', deals: '<-Deal.company', // a Relationship }, Deal: { title: 'string', amount: 'decimal(12,2)', company: '->Company.deals', stage: { // Verbs: the lifecycle open: [], qualify: 'opened', win: 'qualified', // []: from no state, so it creates the Deal lose: ['opened', 'qualified'], }, kickoff: workflow(async ($, deal) => { const { plan } = await $.generative('Draft the kickoff plan for {company.name}', deal, { returns: { plan: 'string' } }) await $.human('Approve the plan', { plan }, { role: 'manager' }) const at = await $.step('stamp it', () => new Date().toISOString()) return { plan, at } }), }, on: { 'Deal.won': 'Deal.kickoff' }, offer: { // Offer: plans, and a price past them Free: { month: 0, seats: 1, includes: { 'Deal.open': 25 } }, Team: { month: 49, seats: 5, seat: 9, includes: { 'Deal.open': 1000 } }, 'Deal.open': 0.001, }, }) ``` ## Next steps - **[Start a Dataset API](/quickstart/dataset-api.md) →** Another API writes each field. You pick the shape and the price. - [Headless SaaS](/patterns/headless-saas.md): records, the lifecycle, [Integrations](/patterns/headless-saas.md#integrations), plans and seats. - [Workflow](/workflow.md): what runs when a Deal is won. --- title: "Patterns" url: /patterns --- # Patterns > Five kinds of API product you can sell, each priced its own way, and each one file of the same five blocks. ## The grid | Pattern | Noun: the shape you promise | Verb: what can be done to it | Source: where its data comes from | Trigger: what reacts to a change | Offer: the price | | --- | --- | --- | --- | --- | --- | | [Dataset API](/patterns/dataset-api.md): Sell the facts about a thing, priced per answer. Sources write it. | Company, keyed by its domain | get by key, search by a field | the API you resell, mapped into yours | a refresh on a schedule | a price per answer, from credits; a miss is free | | [Headless SaaS](/patterns/headless-saas.md): Sell a home for your Customers' records, priced by plan and seat. Your Customer writes it. | your Customers' records and their Relationships | create, get, list, update and delete, and a lifecycle | each Customer's own CRM, as an Integration | a Workflow on Deal.won | plans with seats that include calls, and a price past them | | [Function API](/patterns.md#function-api): Sell an answer, priced per call. A Function writes it. | the Noun it acts on | a Function: code, a model, an agent or a person | — | — | a price per call, by kind; a failure is free | | [Agent API](/patterns.md#agent-api): Sell a worker who does the job, billed by consumption. An agent writes it. | your Customer's records, in their space | the job, done by an agent for them | your Customer's inbox and CRM | a schedule: three meetings a week | by consumption: tokens and minutes | | [Service API](/patterns.md#service-api): Sell an outcome, charged once, when the work is accepted. A Workflow writes it. | a work Thing the buyer watches | order, then one Verb per move | the filing office's receipt | Filing.ordered starts the draft | 25 when accepted, under an Outcome contract | ## The five blocks ```ts title="definition.ts" import { define, workflow } from 'apis.dev' /** Deals for each Customer, kicked off when won. */ export default define({ name: 'pipeline', domain: 'pipeline.apis.dev', sources: { firms: { origin: 'https://firms.example.com' } }, // Source: where its data comes from Deal: { // Noun: the shape you promise title: 'string', amount: 'decimal(12,2)', stage: { // Verb: what can be done to it open: [], qualify: 'opened', win: 'qualified', }, kickoff: workflow(async ($, deal) => { … }), }, on: { 'Deal.won': 'Deal.kickoff' }, // Trigger: what reacts to a change offer: { Team: { month: 49, seats: 5 }, 'Deal.open': 0.001 }, // Offer: the price }) ``` Five blocks, four keys: Verbs live inside their Noun. The [Dataset API](/patterns/dataset-api.md) and the [Headless SaaS](/patterns/headless-saas.md) have pages of their own, and each has a Get started page. ## Answers, workers and outcomes The other three are each one file here. ### Function API *The four Function kinds* | Kind | What it does | How long | Verb | Price | | --- | --- | --- | --- | --- | | Code | computes | milliseconds | `Company.score` | 0.001 | | Generative | writes | seconds | `Company.summarize` | 0.02 | | Agentic | researches, with tools | minutes | `Company.research` | 0.5 | | Human | decides | hours or days | `Company.verify` | 5 | A Verb whose value is a [Function](/reference.md#function) sells an answer, and its kind sets how long it takes. `returns` types the output on every [Interface](/every-product.md): the [Action](/workflow.md#the-lifecycle), the MCP `outputSchema`, OpenAPI and the SDK. In a prompt, `{domain}` reads a field and `{company.name}` follows a [Relationship](/patterns/headless-saas.md). ```ts title="definition.ts" import { define, code, generative, agentic, human } from 'apis.dev' /** Answers about any company by its domain, from a score in milliseconds to a person's check in hours. */ export default define({ name: 'summaries', domain: 'summaries.apis.dev', Company: { // Noun: the Noun it acts on domain: 'url!#', /** A score from 0 to 100, from the domain alone. */ score: code((c) => ({ score: c.domain.endsWith('.ai') ? 80 : 50 })), // Verb: a Function summarize: generative('Two sentences on what {domain} sells', { returns: { summary: 'string' } }), research: agentic('Find the head of sales at {domain}', { returns: { headOfSales: 'string' } }), verify: human('Call and confirm the head of sales', { returns: { confirmed: 'boolean' }, role: 'researcher' }), }, Text: { // a Function on no stored Noun translate: generative('Translate {text} into {to}', { input: { text: 'string', to: 'string' }, returns: { text: 'string' } }), }, offer: { // Offer: per call 'Company.score': 0.001, 'Company.summarize': 0.02, 'Company.research': 0.5, 'Company.verify': 5, 'Text.translate': 0.01, }, }) ``` A fast answer comes back done. A slow one answers 202 with where to poll, and the caller polls its Action until it is done. A Function on a Noun with no records, such as `Text.translate`, takes its `input` in the body. **summarize** ```sh title="Request" curl -X POST https://summaries.apis.dev/companies/acme.com/summarize \ -H "Authorization: Bearer $SUMMARIES_KEY" ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0.02, currency="USD" X-Request-Id: req_fa5075382229d67f1ce9 { "id": "act_1", "verb": "Company.summarize", "status": "done", "output": { "summary": "Acme sells pipeline software to B2B sales teams." } } ``` **research** ```sh title="Request" curl -X POST https://summaries.apis.dev/companies/acme.com/research \ -H "Authorization: Bearer $SUMMARIES_KEY" ``` ```http title="Response" HTTP/1.1 202 Accepted Location: /actions/act_2 Retry-After: 60 X-Cost: price=0.5, currency="USD" X-Request-Id: req_0a6b1dbf299819cd7076 { "id": "act_2", "verb": "Company.research", "status": "pending", "reason": "Running", "poll": "/actions/act_2" } ``` **get** ```sh title="Request" curl https://summaries.apis.dev/actions/act_2 \ -H "Authorization: Bearer $SUMMARIES_KEY" ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_6fcd6e915495e75ebee0 { "id": "act_2", "verb": "Company.research", "status": "done", "output": { "headOfSales": "Dana Ito" }, "provenance": "Inferred" } ``` **translate** ```sh title="Request" curl https://summaries.apis.dev/texts/translate \ -H "Authorization: Bearer $SUMMARIES_KEY" \ --json '{"text":"Two sentences on Acme.","to":"fr"}' ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0.01, currency="USD" X-Request-Id: req_0ab351527f78099b8341 { "id": "act_3", "verb": "Text.translate", "status": "done", "output": { "text": "Deux phrases sur Acme." } } ``` Each kind has its price: 0.001 to compute, 0.02 to write, 0.5 to research, 5 for a person. A failed or cancelled Action costs nothing. ```sh title="Terminal" $ npx apis.dev init function-api $ npx apis.dev deploy ✓ published v1 to https://summaries.apis.dev $ curl -X POST https://summaries.apis.dev/companies/acme.com/summarize \ -H "Authorization: Bearer $SUMMARIES_KEY" ``` ### Agent API *One run, metered once · USD* | Meter | Usage × rate | Amount | | --- | --- | --- | | Tokens | 18,400 × 0.01 / 1,000 | 0.184 | | Minutes | 6 × 0.05 | 0.3 | | Reads | get and list | 0 | | The run | `Meeting.book` | 0.484 | An agent does the job for each Customer, in their own space, with their own inbox and CRM as [Sources](/patterns/dataset-api.md#how-a-field-is-filled). Its schedule runs once per Customer, in that Customer's timezone, and a `Max-Cost` on the run stops it at the ceiling. ```ts title="definition.ts" import { define, agentic } from 'apis.dev' /** An agent that books three meetings a week with each Customer's leads, in their own inbox and CRM. */ export default define({ name: 'meetings', domain: 'meetings.apis.dev', sources: { inbox: { system: 'email', scope: 'customer' }, // Source: each Customer's own crm: { system: 'crm', scope: 'customer' } }, Meeting: { // Noun: their records, in their space with: 'string', company: 'string', at: 'date', book: agentic('Book a meeting with the next lead in {crm}', // Verb: the job { tools: ['inbox', 'crm'], returns: { meeting: 'string' } }), }, on: { 'every Mon, Wed, Fri 09:00': 'Meeting.book' }, // Trigger: in each Customer's timezone offer: { // Offer: by use 'Meeting.book': { per1000Tokens: 0.01, perMinute: 0.05 }, 'Meeting.get': 0, 'Meeting.list': 0, }, }) ``` Each run is an Action that carries what it spent. **curl** ```sh title="Request" curl https://meetings.apis.dev/actions/act_31 \ -H "Authorization: Bearer $MEETINGS_KEY" ``` **TypeScript** ```ts title="TypeScript" import { Meetings } from 'meetings' const api = new Meetings({ key: process.env.MEETINGS_KEY }) const { data, meta } = await api.getAction('act_31') // data: { 'id': 'act_31', 'verb': 'Meeting.book', 'status': 'done', … } // meta.cost: { price: 0, currency: 'USD' } ``` **Python** ```python title="Python" import os from meetings import Meetings api = Meetings(key=os.environ["MEETINGS_KEY"]) data, meta = api.get_action("act_31") # data: { "id": "act_31", "verb": "Meeting.book", "status": "done", … } # meta.cost: { "price": 0, "currency": "USD" } ``` **CLI** ```sh title="Terminal" $ npx meetings login --key $MEETINGS_KEY $ npx meetings actions get act_31 { "id": "act_31", "verb": "Meeting.book", "status": "done", … } price 0 USD · req_24fbbad973aa8085d69b ``` **MCP** ```sh title="Request" curl https://meetings.apis.dev/mcp \ -H "Authorization: Bearer $MEETINGS_KEY" \ --json '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "getAction", "arguments": { "id": "act_31" } } }' ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_ed960bb1c8504fb2745b { "jsonrpc": "2.0", "id": 1, "result": { "content": [ … ], "structuredContent": { "id": "act_31" }, "_meta": { "cost": { "price": 0, "currency": "USD" }, "requestId": "req_ed960bb1c8504fb2745b" } } } ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_24fbbad973aa8085d69b { "id": "act_31", "verb": "Meeting.book", "status": "done", "output": { "meeting": "mtg_1" }, "usage": { "tokens": 18400, "minutes": 6 }, "cost": { "price": 0.484, "currency": "USD" } } ``` Each run is billed by consumption: 0.01 per 1,000 tokens and 0.05 a minute, metered once. Reads cost nothing. ```json title="/pricing" { "currency": "USD", "rates": [ { "name": "Meeting.get", "price": 0, "unit": "call" }, { "name": "Meeting.list", "price": 0, "unit": "call" }, { "name": "Meeting.book", "price": 0.01, "unit": "1000 tokens" }, { "name": "Meeting.book", "price": 0.05, "unit": "minute" } ] } ``` ```sh title="Terminal" $ npx apis.dev init agent-api $ npx apis.dev deploy ✓ published v1 to https://meetings.apis.dev $ curl https://meetings.apis.dev/meetings?week=2026-W44 \ -H "Authorization: Bearer $MEETINGS_KEY" ``` ### Service API ```mermaid title="Filing: its lifecycle" stateDiagram-v2 [*] --> ordered: order ordered --> drafting: draft drafting --> drafted: done drafted --> signed: sign signed --> filing: file filing --> filed: done filed --> accepted: accept · 25 filed --> rejected: reject rejected --> ordered: redo, free ``` A Service API sells an outcome: a work Thing the buyer watches move through its lifecycle, under an [Outcome contract](/reference.md#outcome-contract). It is charged once, on the move the contract names, and a rejected one gets its remedy. ```ts title="definition.ts" import { define, workflow, agentic } from 'apis.dev' /** Each Customer's annual report, drafted, signed and filed, charged once when the Customer accepts it. */ export default define({ name: 'filings', domain: 'filings.apis.dev', Filing: { // Noun: the work your buyer watches company: 'string', year: 'number', status: { // Verbs: one per move order: [], draft: { in: 'ordered', run: workflow(async ($, filing) => { const { form } = await $.generative('Draft the annual report for {company}', filing, { returns: { form: 'string' } }) await $.human('Check the draft', { form }, { role: 'paralegal' }) // unseen: a step return { form } }) }, sign: 'drafted', // seen: the Customer's turn file: { in: 'signed', run: agentic('File the signed report with the state') }, accept: 'filed', reject: 'filed', // the Customer accepts, or it is redone }, }, on: { 'Filing.ordered': 'Filing.draft', 'Filing.signed': 'Filing.file' }, offer: { 'Filing.order': { price: 25, when: 'accepted', accept: 'customer', acceptWithin: '7d', remedy: 'redo' }, }, }) ``` The paralegal's check is a step inside `draft`, so no one outside sees it. The signature is a state: the Customer acts, and `file` becomes allowed. **curl** ```sh title="Request" curl -X POST https://filings.apis.dev/filings/filing_1/sign \ -H "Authorization: Bearer $FILINGS_KEY" ``` **TypeScript** ```ts title="TypeScript" import { Filings } from 'filings' const api = new Filings({ key: process.env.FILINGS_KEY }) const { data, meta } = await api.signFiling('filing_1') // data: { 'id': 'filing_1', 'company': 'acme.com', 'year': 2026, … } // meta.cost: { price: 0, currency: 'USD' } ``` **Python** ```python title="Python" import os from filings import Filings api = Filings(key=os.environ["FILINGS_KEY"]) data, meta = api.sign_filing("filing_1") # data: { "id": "filing_1", "company": "acme.com", "year": 2026, … } # meta.cost: { "price": 0, "currency": "USD" } ``` **CLI** ```sh title="Terminal" $ npx filings login --key $FILINGS_KEY $ npx filings filings sign filing_1 { "id": "filing_1", "company": "acme.com", "year": 2026, … } price 0 USD · req_47d0a7b035cb42c3265d ``` **MCP** ```sh title="Request" curl https://filings.apis.dev/mcp \ -H "Authorization: Bearer $FILINGS_KEY" \ --json '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "signFiling", "arguments": { "id": "filing_1" } } }' ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_724d327d688bd322a86d { "jsonrpc": "2.0", "id": 1, "result": { "content": [ … ], "structuredContent": { "id": "filing_1" }, "_meta": { "cost": { "price": 0, "currency": "USD" }, "requestId": "req_724d327d688bd322a86d" } } } ``` ```http title="Response" HTTP/1.1 200 OK X-Action-Id: act_3 X-Cost: price=0, currency="USD" X-Request-Id: req_47d0a7b035cb42c3265d { "id": "filing_1", "company": "acme.com", "year": 2026, "status": "signed", "createdAt": "2026-10-24T09:00:00Z", "updatedAt": "2026-10-25T16:40:00Z" } ``` When the Customer accepts, the fee is charged, and the delivery says so. ```http title="Delivery" POST /hooks/filings HTTP/1.1 Host: hooks.acme.com X-Webhook-Attempt: 1 X-Webhook-Signature: t=1793175300,v1=ac668e6901ac0de4e796256b7d3c62e3ac59d85a0230fea22ab0c430b952eab7 { "id": "evt_11", "type": "Filing.accepted", "at": "2026-10-28T08:15:00Z", "seq": 11, "data": { "id": "filing_1", "status": "accepted" }, "charged": { "price": 25, "currency": "USD" } } ``` It costs 25 when accepted, the only pattern priced by outcome; a rejected filing is redone at no charge. ```json title="/pricing" { "currency": "USD", "rates": [ { "name": "Filing.order", "price": 25, "unit": "outcome", "when": "accepted" } ] } ``` ```sh title="Terminal" $ npx apis.dev init service-api $ npx apis.dev deploy ✓ published v1 to https://filings.apis.dev $ curl https://filings.apis.dev/filings \ -H "Authorization: Bearer $FILINGS_KEY" \ --json '{"company":"acme.com","year":2026}' ``` ## Built on each other Every API product is a Source for the next one. Name it by its `api`, and say what each of its calls costs you. ```ts title="definition.ts" import { define } from 'apis.dev' /** Sales leads: a company and its size, priced per lead. */ export default define({ name: 'leads', domain: 'leads.apis.dev', sources: { firms: { api: 'companyfacts.apis.dev', // another API product cost: { 'Company.get': 0.002, 'Company.search': 0.005 }, }, }, Lead: { domain: { type: 'url!#', from: 'firms', map: 'domain' }, employees: { type: 'number?', from: 'firms', map: 'employees' }, }, offer: { 'Lead.get': 0.005, 'Lead.search': 0.0125 }, }) ``` Deploy prints your margin on each Verb, to you alone, and refuses a price below its Sources' cost with [`below_cost`](/reference.md#below_cost). ```sh title="Terminal" $ npx apis.dev deploy ✓ Lead.get 0.005 · Source 0.002 · margin 60% ✓ Lead.search 0.0125 · Source 0.005 · margin 60% ✓ published to https://leads.apis.dev ``` Your cost never reaches a Customer: `X-Cost` carries the price and its currency, and nothing else. --- title: "Dataset API" url: /patterns/dataset-api --- # Dataset API > Sell the facts about a thing, priced per answer. ```json title="firms.example.com answers" { "website": "acme.com", "headcount": 240, "industry": "Software" } ``` ```http title="companyfacts.apis.dev/companies/acme.com?include=provenance" HTTP/1.1 200 OK X-Cost: price=0.002, currency="USD" { "domain": "acme.com", "employees": 240, "sector": "Software", "provenance": { "domain": { "basis": "Observed", "writer": "firms", "at": "2026-10-19T06:00:00Z" }, "employees": { "basis": "Observed", "writer": "firms", "at": "2026-10-19T06:00:00Z" }, "sector": { "basis": "Observed", "writer": "firms", "at": "2026-10-19T06:00:00Z" } } } ``` ## How a field is filled It is not a proxy. A [Source](/reference.md#source) answers in its own shape, a [Mapping](/reference.md#mapping) reads each field into yours, and each value carries its [Provenance](/reference.md#provenance). ```ts title="firms: the Source" firms: { origin: 'https://firms.example.com', key: secret('FIRMS_KEY'), // set with npx apis.dev secrets set resources: { companies: { detail: '/companies/{website}' } }, schedule: 'every Mon 06:00', // Trigger: read again each Monday license: 'CC-BY-4.0', }, ``` The credential is a secret you set once, never a line of the file, and no Customer ever sees it. A value is a kept copy read again on its schedule, or a live answer through a cache. A ranked `from` tries each writer in order until one answers. ```ts title="definition.ts" employees: { type: 'number?', from: 'firms', map: 'headcount' }, // [!code --] employees: { type: 'number?', map: 'headcount', // [!code ++] from: ['firms', agentic('Find headcount for {domain}')] }, // [!code ++] ``` A Source's write is Observed, an agent's Inferred, and Observed wins for facts. So the agent's guess at headcount stands until `firms` observes the real one. A Source holds only the members its grammar names: an unknown one is refused on publish. ## What your Customers call A get by the `#` key, and a search by any field, on every [Interface](/every-product.md). **curl** ```sh title="Request" curl https://companyfacts.apis.dev/companies?sector=Software \ -H "Authorization: Bearer $COMPANYFACTS_KEY" ``` **TypeScript** ```ts title="TypeScript" import { Companyfacts } from 'companyfacts' const api = new Companyfacts({ key: process.env.COMPANYFACTS_KEY }) const { data, meta } = await api.searchCompanies({ sector: 'Software' }) // data: { 'items': [{ 'domain': 'acme.com', 'employees': 240, … } // meta.cost: { price: 0.005, currency: 'USD' } ``` **Python** ```python title="Python" import os from companyfacts import Companyfacts api = Companyfacts(key=os.environ["COMPANYFACTS_KEY"]) data, meta = api.search_companies(sector="Software") # data: { "items": [{ "domain": "acme.com", "employees": 240, … } # meta.cost: { "price": 0.005, "currency": "USD" } ``` **CLI** ```sh title="Terminal" $ npx companyfacts login --key $COMPANYFACTS_KEY $ npx companyfacts companies search --sector Software { "items": [{ "domain": "acme.com", "employees": 240, … } price 0.005 USD · req_1e9cb0ffe275994adbe7 ``` **MCP** ```sh title="Request" curl https://companyfacts.apis.dev/mcp \ -H "Authorization: Bearer $COMPANYFACTS_KEY" \ --json '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "searchCompanies", "arguments": { "sector": "Software" } } }' ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0.005, currency="USD" X-Request-Id: req_3f16f46aec098bc51cd7 { "jsonrpc": "2.0", "id": 1, "result": { "content": [ … ], "structuredContent": { "items": [ { "domain": "acme.com", "employees": 240, "sector": "Software" } ], "total": 1, "next": null }, "_meta": { "cost": { "price": 0.005, "currency": "USD" }, "requestId": "req_3f16f46aec098bc51cd7" } } } ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0.005, currency="USD" X-Request-Id: req_1e9cb0ffe275994adbe7 { "items": [{ "domain": "acme.com", "employees": 240, "sector": "Software" }], "total": 1, "next": null } ``` ## The price A price per answer, a Watch on one Company by the month, free calls each month, and the credit packs Checkout sells. ```ts title="definition.ts · offer" offer: { 'Company.get': 0.002, 'Company.search': 0.005 }, // [!code --] offer: { // [!code ++] 'Company.get': 0.002, 'Company.search': 0.005, // [!code ++] 'Company.watch': { month: 0.1 }, // [!code ++] free: { 'Company.get': 100 }, // [!code ++] credits: [10, 50, { pay: 200, get: 220 }], // [!code ++] }, // [!code ++] ``` A miss costs nothing. Each refresh writes `Company.refreshed`, so a Customer's Watch on `employees` delivers each change it makes. | Pattern | Noun: the shape you promise | Verb: what can be done to it | Source: where its data comes from | Trigger: what reacts to a change | Offer: the price | | --- | --- | --- | --- | --- | --- | | [Dataset API](/patterns/dataset-api.md): Sell the facts about a thing, priced per answer. Sources write it. | Company, keyed by its domain | get by key, search by a field | the API you resell, mapped into yours | a refresh on a schedule | a price per answer, from credits; a miss is free | [Start a Dataset API](/quickstart/dataset-api.md) → --- title: "Headless SaaS" url: /patterns/headless-saas --- # Headless SaaS > Sell a home for your Customers' records, priced by plan and seat. ```mermaid title="Deal: its lifecycle" stateDiagram-v2 [*] --> opened: open opened --> qualified: qualify qualified --> won: win opened --> lost: lose qualified --> lost: lose ``` - **Free**: 0 a month · 1 seat · 25 Deal.open included - **Team**: 49 a month · 5 seats, 9 each past them · 1,000 Deal.open included - **Deal.open**: 0.001 a call past a plan ## Records Nouns and their Relationships, CRUD and lifecycles, Roles and seats, and [Workflows](/workflow.md) on their Events: CRUD is the floor. With no `from`, your Customer writes the field, and the [Dataset API](/patterns/dataset-api.md) file becomes a Headless SaaS when its `sources` and every `from` go. ```ts title="definition.ts" import { define, secret } from 'apis.dev' // [!code --] import { define } from 'apis.dev' // [!code ++] /** Any company by its domain, with its headcount and sector. */ export default define({ name: 'companyfacts', domain: 'companyfacts.apis.dev', sources: { // [!code --] firms: { origin: 'https://firms.example.com', key: secret('FIRMS_KEY') }, // [!code --] }, // [!code --] Company: { domain: { type: 'url!#', from: 'firms', map: 'website' }, // [!code --] domain: 'url!#', // [!code ++] employees: { type: 'number?', from: 'firms', map: 'headcount' }, // [!code --] employees: 'number?', // [!code ++] sector: { type: 'string?', from: 'firms', map: 'industry' }, // [!code --] sector: 'string?', // [!code ++] }, offer: { 'Company.get': 0.002, 'Company.search': 0.005 }, // [!code --] offer: { Free: { month: 0, seats: 1 } }, // [!code ++] }) ``` Create, get, list, update, delete and restore answer on each Noun, in each Customer's own space. A record answers at its id and at its `!#` key, so `/companies/acme.com` is `company_1`. A delete keeps the record, and `POST /companies/company_1/restore` brings it back. **create** ```sh title="Request" curl https://pipeline.apis.dev/companies \ -H "Authorization: Bearer $PIPELINE_KEY" \ --json '{"name":"Acme","domain":"acme.com"}' ``` ```http title="Response" HTTP/1.1 201 Created Location: /companies/company_1 X-Action-Id: act_1 X-Cost: price=0, currency="USD" X-Request-Id: req_df59a11235e22d4bc20f { "id": "company_1", "name": "Acme", "domain": "acme.com", "createdAt": "2026-10-24T09:00:00Z", "updatedAt": "2026-10-24T09:00:00Z" } ``` **get** ```sh title="Request" curl https://pipeline.apis.dev/companies/company_1 \ -H "Authorization: Bearer $PIPELINE_KEY" ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_2dc8be23d78d02570ee3 { "id": "company_1", "name": "Acme", "domain": "acme.com", "createdAt": "2026-10-24T09:00:00Z", "updatedAt": "2026-10-24T09:00:00Z" } ``` **get by key** ```sh title="Request" curl https://pipeline.apis.dev/companies/acme.com \ -H "Authorization: Bearer $PIPELINE_KEY" ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_abaab07988e66cd6576a { "id": "company_1", "name": "Acme", "domain": "acme.com", "createdAt": "2026-10-24T09:00:00Z", "updatedAt": "2026-10-24T09:00:00Z" } ``` **list** ```sh title="Request" curl https://pipeline.apis.dev/companies?domain=acme.com&limit=50 \ -H "Authorization: Bearer $PIPELINE_KEY" ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_ea1dd29332afc974c134 { "items": [ { "id": "company_1", "name": "Acme", "domain": "acme.com", "createdAt": "2026-10-24T09:00:00Z", "updatedAt": "2026-10-24T09:00:00Z" } ], "total": 1, "next": null } ``` **update** ```sh title="Request" curl -X PATCH https://pipeline.apis.dev/companies/company_1 \ -H "Authorization: Bearer $PIPELINE_KEY" \ --json '{"name":"Acme Inc."}' ``` ```http title="Response" HTTP/1.1 200 OK X-Action-Id: act_2 X-Cost: price=0, currency="USD" X-Request-Id: req_47bafc37faaf0d937892 { "id": "company_1", "name": "Acme Inc.", "domain": "acme.com", "createdAt": "2026-10-24T09:00:00Z", "updatedAt": "2026-10-24T09:05:00Z" } ``` **delete** ```sh title="Request" curl -X DELETE https://pipeline.apis.dev/companies/company_1 \ -H "Authorization: Bearer $PIPELINE_KEY" ``` ```http title="Response" HTTP/1.1 200 OK X-Action-Id: act_3 X-Cost: price=0, currency="USD" X-Request-Id: req_e0627ba7efa530eb73ac { "id": "company_1", "name": "Acme Inc.", "domain": "acme.com", "createdAt": "2026-10-24T09:00:00Z", "updatedAt": "2026-10-24T09:05:00Z", "deletedAt": "2026-10-24T09:06:00Z" } ``` A Relationship reads from either side: the Deals of a Company are one call. **curl** ```sh title="Request" curl https://pipeline.apis.dev/companies/company_1/deals \ -H "Authorization: Bearer $PIPELINE_KEY" ``` **TypeScript** ```ts title="TypeScript" import { Pipeline } from 'pipeline' const api = new Pipeline({ key: process.env.PIPELINE_KEY }) const { data, meta } = await api.listDeals('company_1') // data: { 'items': [{ 'id': 'deal_1', 'title': 'Pilot', … } // meta.cost: { price: 0, currency: 'USD' } ``` **Python** ```python title="Python" import os from pipeline import Pipeline api = Pipeline(key=os.environ["PIPELINE_KEY"]) data, meta = api.list_deals("company_1") # data: { "items": [{ "id": "deal_1", "title": "Pilot", … } # meta.cost: { "price": 0, "currency": "USD" } ``` **CLI** ```sh title="Terminal" $ npx pipeline login --key $PIPELINE_KEY $ npx pipeline deals list company_1 { "items": [{ "id": "deal_1", "title": "Pilot", … } price 0 USD · req_88eb811626931380df5a ``` **MCP** ```sh title="Request" curl https://pipeline.apis.dev/mcp \ -H "Authorization: Bearer $PIPELINE_KEY" \ --json '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "listDeals", "arguments": { "id": "company_1" } } }' ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_55a7c051d846f85cffb3 { "jsonrpc": "2.0", "id": 1, "result": { "content": [ … ], "structuredContent": { "items": [ … ] }, "_meta": { "cost": { "price": 0, "currency": "USD" }, "requestId": "req_55a7c051d846f85cffb3" } } } ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_88eb811626931380df5a { "items": [ { "id": "deal_1", "title": "Pilot", "amount": "5000.00", "company": "company_1", "stage": "opened", "createdAt": "2026-10-24T09:10:00Z", "updatedAt": "2026-10-24T09:10:00Z" } ], "total": 1, "next": null } ``` ## The lifecycle You write the moves, never the states: each state is its move's past form, and each move writes that Event. A move may name the [Role](/reference.md#role) that makes it, and a member without it gets 403. ```ts title="definition.ts · stage" open: [], qualify: 'opened', win: 'qualified', // [!code --] open: [], qualify: 'opened', // [!code ++] win: { in: 'qualified', role: 'manager' }, // [!code ++] ``` A move the lifecycle does not allow answers 409, with the moves it does allow, as the quickstart's [early win](/quickstart/headless-saas.md#win-it-too-early) shows. ## Integrations Each Customer can keep their records in their own CRM. Your Company is then served from it through their [Integration](/reference.md#integration): the paths, Events, SDK types and price stay the same. ```ts title="definition.ts" sources: { crm: { system: 'crm', scope: 'customer' } }, // [!code ++] Company: { name: 'string', domain: 'url!#', // [!code --] name: { type: 'string', from: 'crm', map: 'name' }, // [!code ++] domain: { type: 'url!#', from: 'crm', map: 'website' }, // [!code ++] deals: '<-Deal.company', }, ``` A change in their CRM arrives as `Company.updated`, Observed, on the change feed and on every [Watch](/reference.md#events) that picks it. ```sh title="Request" curl https://pipeline.apis.dev/events?type=Company.updated&after=evt_3 \ -H "Authorization: Bearer $PIPELINE_KEY" ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_9385689ed087052f4167 { "items": [ { "id": "evt_4", "type": "Company.updated", "at": "2026-10-24T11:00:00Z", "seq": 4, "via": "crm", "provenance": "Observed", "data": { "id": "company_1", "name": "Acme Corporation" } } ], "next": null } ``` ## Plans and seats A plan is a price a month for its seats, with the calls it `includes`. A plan at month 0 stops at them and its refusal offers the next plan; a paid plan draws the price past them from credits. A seat past the plan's own is billed at `seat`, and `keep: true` keeps current subscribers on a plan's old price. ```json title="/pricing" { "currency": "USD", "rates": [ { "name": "Free", "price": 0, "unit": "month", "seats": 1, "includes": { "Deal.open": 25 } }, { "name": "Team", "price": 49, "unit": "month", "seats": 5, "seat": 9, "includes": { "Deal.open": 1000 } }, { "name": "Deal.open", "price": 0.001, "unit": "call" } ] } ``` | Pattern | Noun: the shape you promise | Verb: what can be done to it | Source: where its data comes from | Trigger: what reacts to a change | Offer: the price | | --- | --- | --- | --- | --- | --- | | [Headless SaaS](/patterns/headless-saas.md): Sell a home for your Customers' records, priced by plan and seat. Your Customer writes it. | your Customers' records and their Relationships | create, get, list, update and delete, and a lifecycle | each Customer's own CRM, as an Integration | a Workflow on Deal.won | plans with seats that include calls, and a price past them | [Start a Headless SaaS](/quickstart/headless-saas.md) → --- title: "Workflow" url: /workflow --- # Workflow > Work that runs for days and never loses its place: durable code, a state machine, or both. Work that runs for days and never loses its place: a Workflow is durable code, a state machine, or both. It is the body of a long Verb, and each moment your Customer sees is a state. ```ts title="kickoff: durable code" export default define({ … Deal: { … kickoff: workflow(async ($, deal) => { const { plan } = await $.generative('Draft the kickoff plan for {company.name}', deal, { returns: { plan: 'string' } }) await $.human('Approve the plan', { plan }, { role: 'manager' }) const at = await $.step('stamp it', () => new Date().toISOString()) return { plan, at } }), }, on: { 'Deal.won': 'Deal.kickoff' }, // Trigger: what reacts to a change … }) ``` ```mermaid title="kickoff: the machine" stateDiagram-v2 [*] --> reviewing: Deal.won reviewing --> approved: approve reviewing --> escalated: after 2d escalated --> approved: approve ``` ## Durable code A won Deal starts the kickoff: a model drafts the plan, a manager approves it, and the run keeps the time it was approved. The `on` line is its [Trigger](/patterns.md#the-blocks). A Verb the `on` line names starts when its Event happens, once per Event, with no call. Each `$` call is a step: it runs once, and its result is kept. A failed step retries until its limit runs out, and a run survives restarts and waits of weeks. *What $ gives a run* | Call | What it does | | --- | --- | | `$.step(name, fn)` | Runs `fn` once and keeps its result: a replay reads the result and never runs it again. | | `$.code(fn, input)` | Runs a code Function as a step. | | `$.generative(prompt, input, { returns })` | Asks a model, and answers the fields `returns` names. | | `$.agentic(prompt, { tools, returns })` | Gives an agent the job and the Sources it may use; it answers when the job is done. | | `$.human(task, input, { role })` | Waits for a member who holds the Role to complete the task at `/actions/{id}/complete`. | | `$.wait('14d'), $.wait({ event, where })` | Waits for a duration, or for the first Event that matches `where`. | | `$.call('Company.get', input)` | Calls a Verb of this API, or of a Source that is an API product, and answers its reply. | | `return output` | Ends the run: the value is the Action's `output`. | Durations read `'2d'` or `'30m'`, and a schedule reads `'every Mon, Wed, Fri 09:00'`, run once per Customer in that Customer's timezone. While the manager has not approved, the run is Pending, and its reason says who holds the ball. **get** ```sh title="Request" curl https://pipeline.apis.dev/actions/act_8 \ -H "Authorization: Bearer $PIPELINE_KEY" ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_1154ca02bc01e341ac7b { "id": "act_8", "verb": "Deal.kickoff", "status": "pending", "reason": "AwaitingWorker", "poll": "/actions/act_8" } ``` **complete** ```sh title="Request" curl https://pipeline.apis.dev/actions/act_8/complete \ -H "Authorization: Bearer $MANAGER_KEY" \ --json '{"output":{"approved":true}}' ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_5881a5df799b8e82a74b { "id": "act_8", "verb": "Deal.kickoff", "status": "done", "output": { "plan": "Week 1: import. Week 2: first review.", "at": "2026-10-26T15:00:00Z" } } ``` *Why an Action waits* | Reason | What causes it | | --- | --- | | `AwaitingWorker` | A `$.human` step waits for a member who holds its Role. | | `AwaitingInput` | A machine waits in a state for an event it `accepts`. | | `AwaitingTime` | A `$.wait` duration or an `after` timer is running. | | `AwaitingSource` | A Source has not answered yet, or a `$.wait` waits for its Event. | | `AwaitingAction` | A `$.call` waits for another Action to finish. | | `Retrying` | A step failed and runs again after its backoff, until its limit runs out. | | `Running` | A step or a Function is working now. | ## State machine Same job, other style: write code when the work is a list of steps, and a machine when it waits on people and the clock. A machine names its states, the events each one takes and its timers. ```ts title="kickoff: a state machine" export default define({ … Deal: { … kickoff: machine({ initial: 'reviewing', states: { reviewing: { // the draft runs on the way in entry: generative('Draft the kickoff plan for {company.name}', { returns: { plan: 'string' } }), on: { approve: { target: 'approved', role: 'manager' } }, after: { '2d': 'escalated' }, }, escalated: { on: { approve: { target: 'approved', role: 'manager' } } }, approved: { type: 'final' }, }, }), }, on: { 'Deal.won': 'Deal.kickoff' }, // Trigger: what reacts to a change … }) ``` A machine's events are Verbs on its Noun, each sent by the Role it names; it fires `after` on its timer, and resumes where it left off. The machine's state is the Action's `state`, and `accepts` lists the events it takes. `approve` is a Verb on Deal: `POST /deals/deal_1/approve`, `approveDeal` and `pipeline deals approve`. **get** ```sh title="Request" curl https://pipeline.apis.dev/actions/act_9 \ -H "Authorization: Bearer $PIPELINE_KEY" ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_e51d811890c4fe07c5e4 { "id": "act_9", "verb": "Deal.kickoff", "status": "pending", "reason": "AwaitingInput", "state": "reviewing", "accepts": ["approve"], "poll": "/actions/act_9" } ``` **approve** ```sh title="Request" curl -X POST https://pipeline.apis.dev/deals/deal_1/approve \ -H "Authorization: Bearer $MANAGER_KEY" ``` ```http title="Response" HTTP/1.1 200 OK X-Action-Id: act_9 X-Cost: price=0, currency="USD" X-Request-Id: req_f157fac4bfe56e473ecf { "id": "act_9", "verb": "Deal.kickoff", "status": "done", "state": "approved", "output": { "plan": "Week 1: import. Week 2: first review.", "at": "2026-10-26T15:00:00Z" } } ``` An event the current state does not take answers 409, and a member without its Role answers 403. ## When a Verb is internal A Verb that an `on` line starts and that names no Role is internal: it serves no path, tool or command. While it runs, its Thing is in its activity state, such as `drafting`. **get** ```sh title="Request" curl https://filings.apis.dev/filings/filing_1 \ -H "Authorization: Bearer $FILINGS_KEY" ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_f781a6ad9678c1e5146d { "id": "filing_1", "company": "acme.com", "year": 2026, "status": "drafting", "createdAt": "2026-10-24T09:00:00Z", "updatedAt": "2026-10-24T09:00:01Z" } ``` **draft** ```sh title="Request" curl -X POST https://filings.apis.dev/filings/filing_1/draft \ -H "Authorization: Bearer $FILINGS_KEY" ``` ```http title="Response" HTTP/1.1 404 Not Found Content-Type: application/problem+json X-Cost: price=0, currency="USD" X-Request-Id: req_537212d0f3c7ec2b2bb7 { "type": "https://filings.apis.dev/docs/errors#no_route", "title": "Not Found", "status": 404, "detail": "draft is internal; Filing.ordered starts it.", "code": "no_route", "instance": "req_537212d0f3c7ec2b2bb7" } ``` ## What you never write A read, a create, an update, a delete and a lifecycle move answer the record, and a [Function](/patterns.md#function-api) or a Workflow answers its [Action](/reference.md#action). Durable runs survive restarts and long waits, and each run stays on the [Version](/reference.md#version) it started on. *What you never write* | What you never write | What does it instead | | --- | --- | | Code that starts a run | The `on` line, or a call to the Verb | | A route that takes an event | A Verb on the Noun, which the machine accepts | | A check for a wrong move | The lifecycle, which answers 409 before anything runs | | A status route | The Action at `/actions/{id}`, with its reason while it waits | | A history table | The change feed: every change as an Event, in order | --- Seen is a state and unseen is a step: the drafted plan is a step, and `reviewing`, `escalated` and `approved` are states. --- title: "What every product gets" url: /every-product --- # What every product gets > Everything around your API, live on your own domain from one deploy: CLI, MCP, SDKs, docs, a landing site, a portal and Stripe billing. **Build** | What you get | Where | What it does | | --- | --- | --- | | The API | each Noun's path | REST at the paths of your Nouns and Verbs, with one error shape, Batch, and the price and a request id on every reply. | | Records | POST, GET, PATCH and DELETE /{nouns} | Create, get, list, update and delete on every Noun your Customers write, each record in its Customer's own space. | | Lifecycle moves | POST /{nouns}/{id}/{verb} | One Verb per move, refused with 409 from the wrong state and with 403 for the wrong Role. | | The change feed | /events | Every change as an Event in `seq` order, read back from any point with `after`. | | Outbound webhooks | POST /watches | A Watch per URL your Customer picks: each change signed, retried until it lands, each attempt kept, and replayable. | | Inbound webhooks | a Source's deliveries | Each delivery verified, then written as an Event before anything runs. | | Durable runs | /actions/{id} | A Workflow's run as an Action: Pending with its reason, then done, across restarts and long waits. | **Launch** | What you get | Where | What it does | | --- | --- | --- | | CLI | a command per Verb | A command per Verb under your API's own name, generated from the same file. | | MCP | /mcp | One tool per Verb with its price, then search, fetch and do, each answering the same body and price as REST; a Key or OAuth 2.1 signs in. | | SDK | /sdk | TypeScript and Python clients under your API's name, generated from the file and published with it. | | Docs | /docs | HTML and Markdown, a page per Noun, Verb and Event, with OpenAPI, llms.txt and agents.json. | | Landing site | / | The pitch, Try it with a Test key, the prices and Get started, on your own domain. | | Developer portal | /dashboard | Each Customer's keys, usage and balance, plan, invoices, spend ceiling, webhook deliveries with Replay, and why a run waits. | **Monetize** | What you get | Where | What it does | | --- | --- | --- | | The price on every reply | X-Cost and /pricing | The posted price of each Verb, on /pricing and on every reply, with a miss free. | | Keys | Test and Live | Keys your Customers sign up for, each Live key with a Test twin whose calls never post. | | Free calls | the Offer's free calls | The Offer's `free` calls come off each month before any credit is drawn. | | Prepaid credits | each Customer's balance | Each priced call draws once from the Customer's credit balance, at the price it was served at, even when retried; a plan bills a month ahead. | | Stripe billing | your own Stripe account | Each Customer buys credits through Stripe Checkout and plans through a Stripe subscription, with Stripe invoices and receipts, into your own Stripe account. | | Your Admin | your account | Your revenue by Verb and by Customer, each payment beside the calls it paid for, and your margin per Verb. | ## Build - **The API**: each Noun's path - **Records**: `POST, GET, PATCH and DELETE /{nouns}` - **Lifecycle moves**: `POST /{nouns}/{id}/{verb}` - **The change feed**: `/events` - **Outbound webhooks**: `POST /watches` - **Inbound webhooks**: a Source's deliveries - **Durable runs**: `/actions/{id}` Every change writes one Event: create, update, delete and restore give `created`, `updated`, `deleted` and `restored`, and each move its own past form. A [Watch](/reference.md#events) sends each change your Customer picks to their URL, signed, and retries a failed delivery until it lands. Each attempt is kept, and any delivery replays from the developer portal. ```http title="Delivery" POST /hooks/pipeline HTTP/1.1 Host: hooks.acme.com X-Webhook-Attempt: 1 X-Webhook-Signature: t=1792833000,v1=2c067386fcd9006ece95389e2fde364074ad34388decdfc0a31f3edce14d0252 { "id": "evt_2", "type": "Deal.opened", "at": "2026-10-24T09:10:00Z", "seq": 2, "data": { "id": "deal_1", "title": "Pilot", "amount": "5000.00", "company": "company_1", "stage": "opened" } } ``` ```ts title="Receive it" import { createHmac, timingSafeEqual } from 'node:crypto' // verify(request, secret): true when the delivery was signed with your secret in the last 5 minutes export async function verify(request: Request, secret: string) { const header = request.headers.get('X-Webhook-Signature') ?? '' const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('='))) if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false const want = createHmac('sha256', secret).update(`${t}.${await request.text()}`).digest() const got = Buffer.from(v1 ?? '', 'hex') return got.length === want.length && timingSafeEqual(got, want) } // The receiver: verify a copy, then read the Event once by its id. const seen = new Set() export default { async fetch(req: Request) { if (!(await verify(req.clone(), process.env.PIPELINE_WEBHOOK_SECRET!))) return new Response(null, { status: 401 }) const event = await req.json() if (!seen.has(event.id)) seen.add(event.id) // a retry carries the same id return new Response(null, { status: 204 }) }, } ``` ## Launch One call answers the same body and price on every [Interface](/reference.md#interface). The CLI and both SDKs carry your API's own name, generated from the file and published with it. **curl** ```sh title="Request" curl https://companyfacts.apis.dev/companies/acme.com \ -H "Authorization: Bearer $COMPANYFACTS_KEY" ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0.002, currency="USD" X-Request-Id: req_3cd59a9a1ede10c5bf21 { "domain": "acme.com", "employees": 240, "sector": "Software" } ``` **MCP** ```sh title="Request" curl https://companyfacts.apis.dev/mcp \ -H "Authorization: Bearer $COMPANYFACTS_KEY" \ --json '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "getCompany", "arguments": { "domain": "acme.com" } } }' ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0.002, currency="USD" X-Request-Id: req_f9cf5fc65654d907192f { "jsonrpc": "2.0", "id": 1, "result": { "content": [ … ], "structuredContent": { "domain": "acme.com", "employees": 240, "sector": "Software" }, "_meta": { "cost": { "price": 0.002, "currency": "USD" }, "requestId": "req_f9cf5fc65654d907192f" } } } ``` **TypeScript** ```ts title="TypeScript" import { Companyfacts } from 'companyfacts' const api = new Companyfacts({ key: process.env.COMPANYFACTS_KEY }) const { data, meta } = await api.getCompany('acme.com') // data: { 'domain': 'acme.com', 'employees': 240, 'sector': 'Software' } // meta: { status: 200, cost: { 'price': 0.002, 'currency': 'USD' }, // requestId: 'req_9fd1df32ff7df11f695a' } ``` **Python** ```python title="Python" import os from companyfacts import Companyfacts api = Companyfacts(key=os.environ["COMPANYFACTS_KEY"]) data, meta = api.get_company("acme.com") # data: { "domain": "acme.com", "employees": 240, "sector": "Software" } # meta.cost: { "price": 0.002, "currency": "USD" } ``` **CLI** ```sh title="Terminal" $ npx companyfacts login --key $COMPANYFACTS_KEY $ npx companyfacts companies get acme.com { "domain": "acme.com", "employees": 240, "sector": "Software" } price 0.002 USD · req_3cd59a9a1ede10c5bf21 ``` Your landing site and developer portal come from the same file. The pitch is the comment above it, and the prices are its Offer. *companyfacts.apis.dev/* | Part | What it shows | | --- | --- | | The hero | The file's name and its pitch, the comment above the file, with Get started. | | Try it | Each Verb with its example, called live with a Test key the page mints. | | Pricing | A card per plan from `/pricing`, then each price per call, the free calls and the credit packs. | | Get started | Sign-up, Stripe Checkout for a plan or credits, then a Live key. | | Docs and MCP | The docs, `llms.txt` and the MCP server, for people and for agents. | *companyfacts.apis.dev/dashboard* | Part | What it shows | | --- | --- | | Keys | Each Live key and its Test twin: made, rolled and revoked. | | Usage and balance | Usage by Verb and day, the credit balance, and a top-up through Checkout. | | Plan | The plan and its seats, and a switch to another plan. | | Invoices | Each Stripe invoice and receipt. | | Spend ceiling | A ceiling on each Key; past it, a call asks for Approval. | | Webhook deliveries | Each Watch, its deliveries and their attempts, with Replay. | | Runs | Each Action that waits, and its Pending reason. | ### What an agent sees An agent reads `/.well-known/agents.json` first, then the product's own `llms.txt`, then `tools/list`. It lists one tool per Verb, with its schemas, hints from its kind and its price. ```json title="tools/list" { "tools": [ { "name": "getCompany", "description": "Answers one company by its domain, with its headcount and sector.", "inputSchema": { "type": "object", "properties": { "domain": { "type": "string" } }, "required": ["domain"] }, "outputSchema": { … }, "annotations": { "readOnlyHint": true }, "_meta": { "price": 0.002, "currency": "USD" } }, { "name": "searchCompanies", "description": "Lists the companies in one sector, each with its domain.", "inputSchema": { "type": "object", "properties": { "sector": { "type": "string" } }, "required": ["sector"] }, "outputSchema": { … }, "annotations": { "readOnlyHint": true }, "_meta": { "price": 0.005, "currency": "USD" } }, { "name": "search" }, { "name": "fetch" }, { "name": "do" } ] } ``` ## Monetize ```json title="/pricing" { "currency": "USD", "rates": [ { "name": "Company.get", "price": 0.002, "unit": "call" }, { "name": "Company.search", "price": 0.005, "unit": "call" }, { "name": "Company.watch", "price": 0.1, "unit": "month" } ], "free": { "Company.get": 100 }, "credits": [ 10, 50, { "pay": 200, "get": 220 } ] } ``` Each priced call draws once from prepaid credits, at the price it was served at. Each Customer pays through Stripe, into your account. ### When a Customer needs more Every refusal sells. A 402 or a 403 carries `offers`: credit packs, a plan or a Verb, each with its Stripe Checkout link. The call after payment goes through. **curl** ```sh title="Request" curl https://companyfacts.apis.dev/companies/acme.com \ -H "Authorization: Bearer $COMPANYFACTS_KEY" ``` **TypeScript** ```ts title="TypeScript" import { Companyfacts } from 'companyfacts' const api = new Companyfacts({ key: process.env.COMPANYFACTS_KEY }) const { error, meta } = await api.getCompany('acme.com') // error: { status: 402, code: 'payment_required' } // meta.cost: { price: 0, currency: 'USD' } ``` **Python** ```python title="Python" import os from companyfacts import Companyfacts api = Companyfacts(key=os.environ["COMPANYFACTS_KEY"]) error, meta = api.get_company("acme.com") # error: { "status": 402, "code": "payment_required" } # meta.cost: { "price": 0, "currency": "USD" } ``` **CLI** ```sh title="Terminal" $ npx companyfacts login --key $COMPANYFACTS_KEY $ npx companyfacts companies get acme.com error 402 payment_required price 0 USD · req_71f702a3d4250104f0bf ``` **MCP** ```sh title="Request" curl https://companyfacts.apis.dev/mcp \ -H "Authorization: Bearer $COMPANYFACTS_KEY" \ --json '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "getCompany", "arguments": { "domain": "acme.com" } } }' ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_fe35bd47ea83662091f1 { "jsonrpc": "2.0", "id": 1, "result": { "isError": true, "content": [ … ], "structuredContent": { "status": 402, "code": "payment_required" }, "_meta": { "cost": { "price": 0, "currency": "USD" }, "requestId": "req_fe35bd47ea83662091f1" } } } ``` ```http title="Response" HTTP/1.1 402 Payment Required Content-Type: application/problem+json X-Cost: price=0, currency="USD" X-Request-Id: req_71f702a3d4250104f0bf { "type": "https://companyfacts.apis.dev/docs/errors#payment_required", "title": "Payment Required", "status": 402, "detail": "The credit balance is 0; Company.get draws 0.002 from it.", "code": "payment_required", "instance": "req_71f702a3d4250104f0bf", "offers": [ { "kind": "credits", "price": 10, "checkout": "https://companyfacts.apis.dev/checkout/credits-10" }, { "kind": "credits", "price": 50, "checkout": "https://companyfacts.apis.dev/checkout/credits-50" }, { "kind": "credits", "price": 200, "checkout": "https://companyfacts.apis.dev/checkout/credits-200" } ] } ``` An agent sets `Max-Cost`, and a call above it is refused before it runs, at no charge. A spend ceiling the owner sets on a [Key](/reference.md#key) answers 403 with an Approval URL. ```sh title="Request" curl https://companyfacts.apis.dev/mcp \ -H "Authorization: Bearer $COMPANYFACTS_KEY" \ --json '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "getCompany", "arguments": { "domain": "acme.com" }, "_meta": { "maxCost": 0.001 } } }' ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0, currency="USD" X-Request-Id: req_30d98961416616e608d0 { "jsonrpc": "2.0", "id": 1, "result": { "isError": true, "content": [ … ], "structuredContent": { "type": "https://companyfacts.apis.dev/docs/errors#over_max_cost", "title": "Payment Required", "status": 402, "detail": "Company.get costs 0.002; the call allows 0.001.", "code": "over_max_cost", "instance": "req_30d98961416616e608d0" }, "_meta": { "cost": { "price": 0, "currency": "USD" }, "requestId": "req_30d98961416616e608d0" } } } ``` ### Your revenue *your Admin, at apis.dev* | Part | What it shows | | --- | --- | | Revenue | Revenue by Verb and by Customer, each payment beside the calls it paid for. | | Customers | Each Customer with their plan, balance and usage. | | Margin | Your margin per Verb, from your Sources' cost. | --- title: "Reference" url: /reference --- # Reference > Every call your file generates, with its parameters, reply and errors; then every word, every key of the file, and every error by its code. ## Calls Each Verb in your file is one call on every Interface. Here is the API the Dataset sample generates, call by call, as its own docs print it. Download it as OpenAPI 3.1: [`/samples/companyfacts/openapi.json`](/samples/companyfacts/openapi.json), the copy of what `companyfacts.apis.dev/openapi.json` serves. This site serves its own at [`/openapi.json`](/openapi.json). ### One company `GET /companies/{domain}` · `getCompany` · `companyfacts companies get` · 0.002 USD a call Answers one company by its domain, with its headcount and sector. - `domain` string, in path, required: The company's domain. For example `acme.com`. Returns the [Company](#object-company). Errors, each one free: 401 [`invalid_key`](#invalid_key), 401 [`key_required`](#key_required), 429 [`rate_limited`](#rate_limited), 402 [`over_max_cost`](#over_max_cost), 402 [`payment_required`](#payment_required), 404 [`not_found`](#not_found), 400 [`invalid_param`](#invalid_param). **curl** ```sh title="Request" curl https://companyfacts.apis.dev/companies/acme.com \ -H "Authorization: Bearer $COMPANYFACTS_KEY" ``` **TypeScript** ```ts title="TypeScript" import { Companyfacts } from 'companyfacts' const api = new Companyfacts({ key: process.env.COMPANYFACTS_KEY }) const { data, meta } = await api.getCompany('acme.com') // data: { 'domain': 'acme.com', 'employees': 240, 'sector': 'Software' } // meta.cost: { price: 0.002, currency: 'USD' } ``` **Python** ```python title="Python" import os from companyfacts import Companyfacts api = Companyfacts(key=os.environ["COMPANYFACTS_KEY"]) data, meta = api.get_company("acme.com") # data: { "domain": "acme.com", "employees": 240, "sector": "Software" } # meta.cost: { "price": 0.002, "currency": "USD" } ``` **CLI** ```sh title="Terminal" $ npx companyfacts login --key $COMPANYFACTS_KEY $ npx companyfacts companies get acme.com { "domain": "acme.com", "employees": 240, "sector": "Software" } price 0.002 USD · req_3cd59a9a1ede10c5bf21 ``` **MCP** ```sh title="Request" curl https://companyfacts.apis.dev/mcp \ -H "Authorization: Bearer $COMPANYFACTS_KEY" \ --json '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "getCompany", "arguments": { "domain": "acme.com" } } }' ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0.002, currency="USD" X-Request-Id: req_f9cf5fc65654d907192f { "jsonrpc": "2.0", "id": 1, "result": { "content": [ … ], "structuredContent": { "domain": "acme.com", "employees": 240, "sector": "Software" }, "_meta": { "cost": { "price": 0.002, "currency": "USD" }, "requestId": "req_f9cf5fc65654d907192f" } } } ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0.002, currency="USD" X-Request-Id: req_3cd59a9a1ede10c5bf21 { "domain": "acme.com", "employees": 240, "sector": "Software" } ``` ### Companies in one sector `GET /companies` · `searchCompanies` · `companyfacts companies search` · 0.005 USD a call Lists the companies in one sector, each with its domain. - `sector` string, in query, required: A sector as the records name it. For example `Software`. Returns a list: `items`, each a [Company](#object-company), with `total` and `next`. Errors, each one free: 401 [`invalid_key`](#invalid_key), 401 [`key_required`](#key_required), 429 [`rate_limited`](#rate_limited), 402 [`over_max_cost`](#over_max_cost), 402 [`payment_required`](#payment_required), 400 [`invalid_param`](#invalid_param). **curl** ```sh title="Request" curl https://companyfacts.apis.dev/companies?sector=Software \ -H "Authorization: Bearer $COMPANYFACTS_KEY" ``` **TypeScript** ```ts title="TypeScript" import { Companyfacts } from 'companyfacts' const api = new Companyfacts({ key: process.env.COMPANYFACTS_KEY }) const { data, meta } = await api.searchCompanies({ sector: 'Software' }) // data: { 'items': [{ 'domain': 'acme.com', 'employees': 240, … } // meta.cost: { price: 0.005, currency: 'USD' } ``` **Python** ```python title="Python" import os from companyfacts import Companyfacts api = Companyfacts(key=os.environ["COMPANYFACTS_KEY"]) data, meta = api.search_companies(sector="Software") # data: { "items": [{ "domain": "acme.com", "employees": 240, … } # meta.cost: { "price": 0.005, "currency": "USD" } ``` **CLI** ```sh title="Terminal" $ npx companyfacts login --key $COMPANYFACTS_KEY $ npx companyfacts companies search --sector Software { "items": [{ "domain": "acme.com", "employees": 240, … } price 0.005 USD · req_1e9cb0ffe275994adbe7 ``` **MCP** ```sh title="Request" curl https://companyfacts.apis.dev/mcp \ -H "Authorization: Bearer $COMPANYFACTS_KEY" \ --json '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "searchCompanies", "arguments": { "sector": "Software" } } }' ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0.005, currency="USD" X-Request-Id: req_3f16f46aec098bc51cd7 { "jsonrpc": "2.0", "id": 1, "result": { "content": [ … ], "structuredContent": { "items": [ { "domain": "acme.com", "employees": 240, "sector": "Software" } ], "total": 1, "next": null }, "_meta": { "cost": { "price": 0.005, "currency": "USD" }, "requestId": "req_3f16f46aec098bc51cd7" } } } ``` ```http title="Response" HTTP/1.1 200 OK X-Cost: price=0.005, currency="USD" X-Request-Id: req_1e9cb0ffe275994adbe7 { "items": [ { "domain": "acme.com", "employees": 240, "sector": "Software" } ], "total": 1, "next": null } ``` ### A call, in order The checks a call passes before it runs, in order. The first that fails answers, and the call costs nothing. A call that cannot be checked against its Limits answers 503 [`unavailable`](#unavailable), and a Verb that fails while it runs answers 500 [`internal`](#internal). 1. A presented Key is one the API accepts. Else 401: [`invalid_key`](#invalid_key). 2. A Test Key stays in Test. Else 400: [`bad_environment`](#bad_environment). 3. The path is one of the API's Verbs. Else 404: [`no_route`](#no_route). 4. The method is the one the Verb takes. Else 405: [`method_not_allowed`](#method_not_allowed). 5. A call presents a Key when the Offer requires one. Else 401: [`key_required`](#key_required). 6. The Key covers the Verb, and its member holds the Role a move names. Else 403: [`not_entitled`](#not_entitled) or [`role_required`](#role_required). 7. The call fits under the rate limit. Else 429: [`rate_limited`](#rate_limited). 8. The price fits under the call's `Max-Cost`, the plan and the credit balance. Else 402: [`over_max_cost`](#over_max_cost) or [`plan_limit`](#plan_limit) or [`payment_required`](#payment_required). 9. The call fits under the spend ceiling on its Key, and nothing it needs waits on a person. Else 403: [`approval_required`](#approval_required). 10. Every required parameter and field is there, of its type and pattern. Else 400: [`missing_param`](#missing_param) or [`invalid_param`](#invalid_param). 11. A move is allowed from the Thing's state, and a create's unique key is free. Else 409: [`wrong_state`](#wrong_state) or [`already_exists`](#already_exists). 12. A record matches the input; a miss answers 404 and costs nothing. Else 404: [`not_found`](#not_found). ### Replies Every reply carries `X-Cost` and `X-Request-Id`, and its body is one of four shapes. A read, a create, an update, a delete and a lifecycle move answer the record, with `X-Action-Id`; a Function or a Workflow answers its Action. *Reply shapes* | Shape | Status | Body | | --- | --- | --- | | A record | 200 or 201 | The Thing itself: its fields, `id`, `createdAt` and `updatedAt`, and its lifecycle state. | | A list | 200 | `{ "items": [ … ], "total": n, "next": cursor }`, each item a record; `next` is null on the last page. | | An Action | 200 or 202 | A Function's or a Workflow's run: `id`, `verb` and `status`, then `output`, a Problem, or Pending's `reason` and `poll`. | | A Problem | 4xx or 5xx | `application/problem+json`: `type`, `title`, `status`, `detail`, `code` and `instance`, and `offers` on a refusal that sells. | A Thing's path is its id, and a Noun with a `!#` field answers at that key too: `/companies/acme.com` and `/companies/company_1` are one record. A list takes `?limit` (50, at most 100) and `?cursor`, and `?include=provenance` adds each field's Provenance. ### The Action An Action is in one of five states. `Prefer: wait=30` waits for it, and `POST /actions/{id}/cancel` stops it at no charge. *An Action's five states* | State | What it carries | | --- | --- | | `pending` | Not finished: it carries `reason` and `poll`, and a machine adds its `state` and the events it `accepts`. | | `done` | Finished: it carries `output`, and `usage` and `cost` when it is billed by consumption. | | `failed` | Ended by an error: it carries the Problem, and it costs nothing. | | `cancelled` | Stopped by `POST /actions/{id}/cancel`: nothing more runs, and it costs nothing. | | `refused` | Never started: a check refused it, with the Problem the call answered. | *Why an Action waits* | Reason | What causes it | | --- | --- | | `AwaitingWorker` | A `$.human` step waits for a member who holds its Role. | | `AwaitingInput` | A machine waits in a state for an event it `accepts`. | | `AwaitingTime` | A `$.wait` duration or an `after` timer is running. | | `AwaitingSource` | A Source has not answered yet, or a `$.wait` waits for its Event. | | `AwaitingAction` | A `$.call` waits for another Action to finish. | | `Retrying` | A step failed and runs again after its backoff, until its limit runs out. | | `Running` | A step or a Function is working now. | ### Headers - `Authorization: Bearer `: The Key; `/mcp` also takes an OAuth 2.1 token, advertised at `/.well-known/oauth-protected-resource`. - `X-Api-Key`: The Key, the other way. - `Idempotency-Key`: A key a retried write sends again: the write runs once, and is billed once. - `Max-Cost`: The most a call may cost; a priced call above it is refused before it runs, and a consumption run stops at it. `maxCost` in the SDK, `_meta.maxCost` over MCP. - `Prefer: wait=30`: Wait up to 30 seconds for an Action: it answers 200 when done, else 202. - `Api-Version`: The Version a call is answered at; a Key keeps the Version it first called until it sends one. - `X-Cost`: The price of the call and its currency: 0 for a miss, a refusal, or a call a plan or `free` covers. - `X-Request-Id`: The id to quote when asking about a call. - `X-Action-Id`: The Action a write ran as, at `/actions/{id}`. - `Location`: Where a created record, or a Pending Action, lives. - `Retry-After`: The seconds to wait before a call again: after a 429, a 503 or a 202. - `X-Webhook-Signature`: On a webhook delivery. `t=…,v1=…`: `v1` is the hex HMAC-SHA256 of `{t}.{body}` under the Watch's secret. - `X-Webhook-Attempt`: On a webhook delivery. Which attempt this is: a failed delivery is retried, and each attempt is kept. ## Objects Every reply body is one of these. A Noun's record is its fields; an Action is a run; a Problem is a refusal or an error. ### Company What `/companies/{domain}` answers, and each item of a list. - `domain` string, the key: The record answers at `/companies/{domain}`. - `employees` number: The record's employees. - `sector` string: The record's sector. ```json title="The Company object" { "domain": "acme.com", "employees": 240, "sector": "Software" } ``` ### Action What a Function or a Workflow answers: one run of a Verb, from `pending` to `done`. - `id` string: The Action's id: `/actions/{id}` answers it. - `verb` string: The Verb it runs, `Noun.verb`. - `status` string: One of [the five states](#the-action). - `output` object: What the Verb returned, once it is done. - `problem` object: A [Problem](#object-problem), when it failed. - `reason` string: Why it waits, while it is Pending. - `poll` string: Where to ask again, while it is Pending. ```json title="The Action object" { "id": "act_1", "verb": "Company.summarize", "status": "done", "output": { "summary": "Acme sells pipeline software to B2B sales teams." } } ``` ### Problem What a miss, a refusal or an error answers, as `application/problem+json`, at price 0. - `type` string: A URL to the code's entry on the API's errors page. - `title` string: The HTTP status, in words. - `status` integer: The HTTP status. - `detail` string: What happened, for this call. - `code` string: One code from [Errors](#errors). - `instance` string: The request id, as `X-Request-Id` carries it. - `offers` list: On a refusal that sells: each plan or pack, with its Stripe Checkout link. ```json title="The Problem object" { "type": "https://companyfacts.apis.dev/docs/errors#not_found", "title": "Not Found", "status": 404, "detail": "No Company has domain nobody.example.com.", "code": "not_found", "instance": "req_84326ad21bc488454616" } ``` An Event is the envelope under [Events and Watches](#events). ## The file The file is `definition.ts`: `import { define } from 'apis.dev'`, then `export default define({ … })`. Sources go under `sources`, each Noun at a capitalised key with its Verbs inside it, Triggers under `on`, and prices under `offer`. [`/definition.schema.json`](/definition.schema.json) is the same file as JSON Schema, a `$defs` entry per block. ### Concepts | Concept | One sentence | Taught on | | --- | --- | --- | | Action | What a Verb call answers: done, a 200 or 201 with its record; Refused, a 409; or Pending, a 202 with where to poll. | [Workflow](/workflow.md#the-lifecycle) | | API product | Everything around an API's few lines of code, on your own domain, with each part reading your one file. | [What every product gets](/every-product.md) | | Customer | The party that buys from you, and the tenant its own data belongs to. | [Headless SaaS](/patterns/headless-saas.md) | | Event | A Verb's past form, written when it happens: `Deal.opened`, `Company.updated`. | [What every product gets](/every-product.md#build) | | Function | How a Verb runs: code, generative, agentic or human; the caller gets the same [Action](/workflow.md#the-lifecycle) back whichever it is. | [Patterns](/patterns.md#function-api) | | Host | Your own Domain: every [Interface](/every-product.md) of the API answers there, and every link stays there. | [Reference](/reference.md) | | Integration | A Source bound to credentials, yours or each Customer's own, that reads from and writes back to their system. | [Headless SaaS](/patterns/headless-saas.md#integrations) | | Interface | One way a person or an agent reaches a product on its own domain: the API, CLI, MCP, SDK and docs among them. | [What every product gets](/every-product.md) | | Key | The credential a Customer presents on a call; every call checks it, and no record ever holds it. | [Reference](/reference.md) | | Mapping | How a [Source](/patterns/dataset-api.md#how-a-field-is-filled)'s fields become your Noun's fields, ranked when more than one Source can answer. | [Dataset API](/patterns/dataset-api.md#how-a-field-is-filled) | | Meter | Counts each priced call, token and minute once, so a retried request counts once. | [Reference](/reference.md) | | Noun | A type of Thing, with its fields, its Verbs, and its lifecycle. | [Patterns](/patterns.md#the-blocks) | | Offer | What you sell, at a price (zero allowed: free is a price), on terms, made by you. | [Patterns](/patterns.md#the-blocks) | | Outcome contract | The terms on an Offer that make you warrant a Service's result: what done is, who accepts it, and the remedy. | [Patterns](/patterns.md#service-api) | | Price | The posted price of one call that returns a result: the same number in `X-Cost`, on `/pricing` and on the bill. | [Reference](/reference.md) | | Problem | The error document a failed call answers: the RFC 9457 fields plus a stable machine code. | [Reference](/reference.md#errors) | | Provenance | Who wrote a value and on what basis: Stated, Inferred or Observed; Observed wins for facts. | [Dataset API](/patterns/dataset-api.md#how-a-field-is-filled) | | Relationship | A link from one Noun to another, written with an arrow, `->` or `<-`, and read from either side. | [Headless SaaS](/patterns/headless-saas.md) | | Role | A seat that owns a set of work, and holds the Permissions for it. | [Headless SaaS](/patterns/headless-saas.md) | | Source | What a product draws on and did not build: data, a service or people; a [Mapping](/patterns/dataset-api.md#how-a-field-is-filled) reads its records into your Noun. | [Dataset API](/patterns/dataset-api.md#how-a-field-is-filled) | | Test key | A Key whose calls stay in the Test twin and never bill; the first deploy prints one, and a Live key answers the same. | [Start a Dataset API](/quickstart/dataset-api.md#call-it) | | Test twin | The separate space a Test Key calls into; its books never post and it never touches Live records. | [Reference](/reference.md) | | Thing | An instance of one or more Nouns, with an identity and a current state. | [Patterns](/patterns.md#service-api) | | Trigger | What starts work: an Event, a schedule, or a Thing entering a state. | [Patterns](/patterns.md#the-blocks) | | Verb | What can be done to a Noun: a read, a create, a lifecycle move, or a Function. | [Patterns](/patterns.md#the-blocks) | | Version | One published digest of the file; a Customer pinned to it keeps its behaviour until it moves. | [Reference](/reference.md) | | Watch | A standing pick of Events, delivered somewhere: `POST /watches` makes one, and a webhook is a Watch to a URL. | [Reference](/reference.md#events) | | Webhook | What sends each change your Customer picks to a URL as a signed request, retried until it lands. | [What every product gets](/every-product.md#build) | | Workflow | Durable code, a state machine, or both: the private body of one Action. | [Workflow](/workflow.md) | ### The file itself - `import { define } from 'apis.dev'`: The file is typed: `define()` checks every key as you write it, and each call you use is imported beside it. - `name: 'companyfacts'`: The CLI command, the npm and PyPI package, the SDK class and the MCP server name; deploy asks for it when it is missing. - `domain: 'companyfacts.apis.dev'`: The Host every Interface answers on: `.apis.dev` from the first deploy, or a Domain of your own, which deploy checks once by a TXT record. - `/** Any company by its domain. */`: A comment above the file, a Noun, a field or a Verb is its description on every Interface; the file's own is the pitch. ### Source - `sources: { firms: { origin: 'https://firms.example.com' } }`: The Sources you draw on, each by the name `from` uses, at its origin. - `key: secret('FIRMS_KEY')`: A Source's credential, set with `npx apis.dev secrets set FIRMS_KEY`: never in the file, never shown to a Customer. - `{ api: 'companyfacts.apis.dev' }`: A Source that is another API product, read through its own Verbs. - `cost: { 'Company.get': 0.002 }`: What each of the Source's calls costs you: deploy prints your margin per Verb, and a price below it is refused. - `{ system: 'crm', scope: 'customer' }`: An Integration: each Customer's own system. - `{ type, from, map }`: A field a Source writes: its type, the Source, and the Source's own field. - `from: [a, b]`: A ranked `from`: each writer in turn, until one answers. - `schedule: 'every Mon 06:00'`: When a kept copy is read again, in the time grammar every schedule uses. ### Noun - `Company`: A capitalised key is a Noun: the shape you promise, with its fields and its Verbs. - `'string'`, `'number'`, `'boolean'`, `'date'`, `'url'`, `'decimal(12,2)'`: A field's type, as a lower-case word. - `?`, `!`, `#`: After a type: optional; required and unique; indexed. `'url!#'` is a key the record also answers at. - `'->Company.deals'`, `'<-Deal.company'`: A Relationship: `->` points at another Noun and `<-` reads one back; after the dot is its other side. - `{ type: 'url!#', example: 'acme.com' }`: A field as an object: its type, and `example:`, the field's example in the docs, OpenAPI, the SDK and Try it. - `{ type: 'string?', from: 'Company.research' }`: A field whose `from` names a Verb keeps that Verb's latest output, stamped Inferred. ### Verb - `stage: { open: [], qualify: 'opened' }`: Lifecycle moves: each key is a Verb, and its value the state it moves from; `[]` is no state, so `open` creates the Thing. - `lose: ['opened', 'qualified']`: A move allowed from any of these states. - `win: { in: 'qualified', role: 'manager' }`: A move with the state it moves from and the Role that may call it; `run` gives it a Function or a Workflow. - `agentic('Find the head of sales at {domain}')`: A Function: `code`, `generative`, `agentic` or `human`. A key whose value is one is a Verb; a Verb with no comment gets one written from its Noun and kind. - `{ returns: { summary: 'string' }, input, tools, role }`: A Function's options: `returns` types its output everywhere, `input` its input on a Noun with no records, `tools` the Sources an agent may use, `role` who does a human one. - `'{domain}'`, `'{company.name}'`: In a prompt, a field of the Thing, or a field across a Relationship. - `workflow(fn)`: A Workflow as durable code: a key whose value is one is a Verb, the work's body; `$` gives each step. - `machine({ initial, states })`: A Workflow as a state machine: each state's `entry`, its `on` events and its `after` timers. - `on: { approve: { target: 'approved', role: 'manager' } }`: A machine's event: a Verb on the Noun, with its target state, the Role that may send it, and a `guard`. ### Trigger - `on: { 'Deal.won': 'Deal.kickoff' }`: A Trigger: when the Event happens, the Verb it names starts. Nothing else in the file starts work. - `on: { 'every Mon, Wed, Fri 09:00': 'Meeting.book' }`: A Trigger on a schedule, run once per Customer, in that Customer's timezone; a duration is `'2d'` or `'30m'`. ### Offer - `offer: { 'Company.get': 0.002 }`: A `Noun.verb` key is a price per call; free is an explicit 0. A price applies on every plan unless the plan's `includes` covers it. - `Team: { month: 49, seats: 5, seat: 9 }`: A capitalised key is a plan: a price a month for its seats, and `seat` for each seat past them; `year`, `trial` and `keep` are its other keys. - `includes: { 'Deal.open': 1000 }`: The calls a plan covers each month, at price 0; a plan at month 0 stops at them, and a paid plan draws the price past them. - `Enterprise: { contact: true }`: A plan you sell by conversation: /pricing shows it, and Get started opens a contact form. - `free: { 'Company.get': 100 }`: Calls each month that cost nothing, on any plan or none. - `credits: [10, 50, { pay: 200, get: 220 }]`: The credit packs Checkout sells; `{ pay, get }` is a pack with a bonus. - `keyPrefix: 'cf'`: The prefix of every Key: `cf_test_…` and `cf_live_…`, then 30 characters and a checksum; with none, the file's name. - `'Company.watch': { month: 0.1 }`: A price by the month on one Verb, such as a Watch on one record. - `{ per1000Tokens, perMinute }`: A price by consumption: per 1,000 tokens and per minute. - `{ price, when, accept, acceptWithin, remedy }`: A price by outcome: charged once, when the Thing enters `when`; a rejected one gets its remedy: `redo`, `refund` or `credit`. ### Rules 1. A Noun whose fields name a Source in `from` gets `get` by its `#` key and `search` by a field; a Noun your Customers write, or an Integration holds, gets create, get, list, update and delete. 2. A move enters its past-form state and writes that Event: `open` enters `opened` and writes `Deal.opened`; `win` enters `won`; `lose` enters `lost`. 3. Every Verb you write, a move, a Function or a machine event, has a price, a plan or an outcome, or the publish is refused; a generated Verb serves when one covers it. A plan covers the Verbs it leaves unpriced, and an outcome every Verb of its Thing. 4. `Noun.verb` becomes `verbNoun` over MCP and in the SDK, plural for a list: `Deal.open` is `openDeal`, `Company.search` is `searchCompanies`. 5. A Trigger names its Verb in full: `'Deal.won': 'Deal.kickoff'`. 6. A Verb that an `on` line starts and that names no `role` is internal: it serves no path, tool or command, and a direct call answers 404 `no_route`; while it runs, the Thing is in its activity state, `drafting`. A Verb may not take a path the API serves itself. Those are `/`, `/mcp`, `/docs` or a path under it, `/openapi.json`, `/llms.txt`, `/pricing`, `/.well-known/agents.json`, `/sdk`, `/batch`, `/dashboard`, `/actions`, `/events`, `/watches` and `/checkout`. Every `{name}` in a path is a declared parameter, and every Verb's name is lowerCamelCase and used once. ### Names A Verb `Noun.verb` is `verbNoun` over MCP, in the TypeScript SDK and on its docs page, the Noun plural when it answers a list, and `verb_noun` in Python. In the CLI it is the API's name, the Noun in the plural, then the Verb. The SDK takes the `#` key first and everything else as one object. The Event it writes is its past form. *One name per Verb* | File | REST | MCP | TypeScript | Python | CLI | Event | | --- | --- | --- | --- | --- | --- | --- | | `Company.get` | `GET /companies/{domain}` | `getCompany` | `api.getCompany()` | `api.get_company()` | `companyfacts companies get` | — | | `Company.search` | `GET /companies?sector=` | `searchCompanies` | `api.searchCompanies()` | `api.search_companies()` | `companyfacts companies search` | — | | `Deal.open` | `POST /deals` | `openDeal` | `api.openDeal()` | `api.open_deal()` | `pipeline deals open` | `Deal.opened` | | `Deal.win` | `POST /deals/{id}/win` | `winDeal` | `api.winDeal()` | `api.win_deal()` | `pipeline deals win` | `Deal.won` | | `Deal.approve` | `POST /deals/{id}/approve` | `approveDeal` | `api.approveDeal()` | `api.approve_deal()` | `pipeline deals approve` | `Deal.approved` | | `Company.summarize` | `POST /companies/{domain}/summarize` | `summarizeCompany` | `api.summarizeCompany()` | `api.summarize_company()` | `summaries companies summarize` | `Company.summarized` | | `Text.translate` | `POST /texts/translate` | `translateText` | `api.translateText()` | `api.translate_text()` | `summaries texts translate` | `Text.translated` | ## Publish One command publishes the file as a Version, and every Interface moves to it at once. Each command is a call to the platform API at `https://apis.dev/v1`, signed with `APIS_DEV_TOKEN`; a refusal is a Problem like any API's. ### The apis.dev command *The apis.dev command* | Command | What it does | | --- | --- | | `npx apis.dev init dataset-api` | Starts a folder with a `definition.ts` for the pattern you name: `dataset-api`, `headless-saas`, `function-api`, `agent-api` or `service-api`. | | `npx apis.dev dev` | Serves every Interface on localhost against your Test twin, and reloads on every save. | | `npx apis.dev check` | Checks the file against the schema and the publish rules, and prints every Problem with its code and `at`. | | `npx apis.dev plan` | Prints what the next deploy changes: each Verb, each price, your margin, and any breaking change. | | `npx apis.dev deploy` | Publishes the file as a new Version, every Interface at once; `--json` prints the result as data. | | `npx apis.dev secrets set FIRMS_KEY` | Stores a Source's credential for `secret()`: never in the file, and never shown to a Customer. | | `npx apis.dev logs --follow` | Streams every call as it is answered: its Verb, status, price and request id. | | `npx apis.dev calls req_3cd59a9a1ede10c5bf21` | Prints one call by its request id: its input, its reply, its price and what it ran. | | `npx apis.dev rollback` | Moves every Interface back to the Version before, as one. | | `npx apis.dev events listen --forward localhost:3000/hooks` | Sends each Event to a local URL, signed, so a receiver is tested before a Watch points at it. | | `APIS_DEV_TOKEN` | Signs every command in without a browser, for CI and for agents. | ### When a publish is refused A refused publish answers coded Problems, each with a JSON Pointer `at` into the file and its fix, and nothing that serves changes. `npx apis.dev check --json` prints the same list. *Why a publish is refused* | Code | at | Means | Fix | | --- | --- | --- | --- | | `invalid_definition` | `/Company/employees` | The file does not parse as data inside `define()`, or a key does not fit the schema. | Fix the key at `at`; `/definition.schema.json` holds every key. | | `unpriced_verb` | `/Deal/stage/win` | A served Verb has no price, no plan covers it, and no outcome charges for its Thing. | Price it in `offer`, such as `'Deal.win': 0`, or name a plan whose `includes` covers it. | | `reserved_path` | `/Company/docs` | A Verb would take a path the API serves itself. | Rename the Verb; the reserved paths are listed with the rules of the file. | | `unknown_source` | `/Company/domain/from` | `from` names a Source that `sources` does not hold. | Add the Source under `sources`, or name one that is there. | | `unknown_verb` | `/on/'Deal.won'` | A Trigger or a price names a Verb no Noun has. | Name the Verb in full, `Noun.verb`, as the Noun declares it. | | `one_sided_relationship` | `/Deal/company` | A Relationship names an other side that does not point back. | Give the other Noun its side: `deals: '<-Deal.company'` for `company: '->Company.deals'`. | | `duplicate_verb` | `/Deal/stage/open` | Two Verbs on one Noun share a name, or two serve one route. | Rename one of them; every Verb is used once. | | `bad_state` | `/Deal/stage/win` | A move names a state no move enters. | Name a state a move enters: a move's past form, such as `qualified`. | | `bad_type` | `/Company/employees` | A field's type is not one of the types, or its marks are out of order. | Write a type, then `?`, `!` or `#`: `'number?'`, `'url!#'`. | | `below_cost` | `/offer/'Lead.get'` | A Verb's price is below what its Sources cost you for one call. | Raise the price above the cost the Problem names, or read a Source that costs less. | ### Check a file `POST /v1/products/{name}/checks` · `npx apis.dev check` Checks a file against the schema and the publish rules. It changes nothing, and needs no account. - `name` string, in path, required: The product's name, as the file's `name` gives it. For example `companyfacts`. - `file` string, in body, required: Your whole `definition.ts`, as text. For example `@definition.ts` in curl. Returns `ok`, and every Problem with its code, its JSON Pointer `at` and what to fix; `definition` holds the compiled file when `ok` is true. Errors: none of its own. A file that does not check answers 200 with its Problems, each a code from [When a publish is refused](#publish-errors). **curl** ```sh title="Request" curl https://apis.dev/v1/products/companyfacts/checks \ -H "Authorization: Bearer $APIS_DEV_TOKEN" \ --data-binary @definition.ts ``` **CLI** ```sh title="CLI" $ npx apis.dev check ``` ```http title="Response" HTTP/1.1 200 OK Content-Type: application/json X-Request-Id: req_f5a14fbf78907e246b5c { "ok": false, "problems": [ { "code": "unknown_source", "at": "/Company/employees/from", "message": "Company.employees reads firm, which sources does not hold" } ] } ``` ### Plan a deploy `POST /v1/products/{name}/plans` · `npx apis.dev plan` Answers what deploying a file would change against the Version now live: each Verb, its price and its names, and every change, marked when it breaks a caller. - `name` string, in path, required: The product's name, as the file's `name` gives it. For example `companyfacts`. - `file` string, in body, required: Your whole `definition.ts`, as text. For example `@definition.ts` in curl. Returns `verbs`, each with its price, MCP tool and CLI command, and `changes`, each with `breaking`. Errors: 401 [`invalid_key`](#invalid_key); 404 [`not_found`](#not_found) when you own no product by that name. **curl** ```sh title="Request" curl https://apis.dev/v1/products/companyfacts/plans \ -H "Authorization: Bearer $APIS_DEV_TOKEN" \ --data-binary @definition.ts ``` **CLI** ```sh title="CLI" $ npx apis.dev plan ``` ```http title="Response" HTTP/1.1 200 OK Content-Type: application/json X-Request-Id: req_3379506a7f8f750bbcf4 { "ok": true, "verbs": [ { "verb": "Company.get", "price": 0.003, "mcp": "getCompany" }, { "verb": "Company.search", "price": 0.005, "mcp": "searchCompanies" } ], "changes": [ { "change": "price", "at": "Company.get", "from": 0.002, "to": 0.003, "breaking": true } ] } ``` ### Deploy a Version `POST /v1/products/{name}/deploys` · `npx apis.dev deploy` Publishes a file as a new Version. Every Interface moves to it at once, and a run in flight finishes on the Version it started on. - `name` string, in path, required: The product's name, as the file's `name` gives it. For example `companyfacts`. - `file` string, in body, required: Your whole `definition.ts`, as text. For example `@definition.ts` in curl. - `Idempotency-Key` string, header, optional: Sent again on a retry, so one file publishes once. Returns the Version, its digest, the address it answers on and the changes it made. Errors: 401 [`invalid_key`](#invalid_key); 400 with every Problem from [When a publish is refused](#publish-errors), and nothing that serves changes. **curl** ```sh title="Request" curl https://apis.dev/v1/products/companyfacts/deploys \ -H "Authorization: Bearer $APIS_DEV_TOKEN" \ --data-binary @definition.ts ``` **CLI** ```sh title="CLI" $ npx apis.dev deploy ``` ```http title="Response" HTTP/1.1 201 Created Content-Type: application/json X-Request-Id: req_bd196c92ccf8d1dfb3bc { "version": "v2", "digest": "f1b536fb8c49", "url": "https://companyfacts.apis.dev", "previous": "v1", "changes": [ { "change": "price", "at": "Company.get", "from": 0.002, "to": 0.003, "breaking": true } ] } ``` ### Roll back `POST /v1/products/{name}/rollbacks` · `npx apis.dev rollback` Moves every Interface back to the Version before, as one. - `name` string, in path, required: The product's name, as the file's `name` gives it. For example `companyfacts`. Returns the Version now live and the one it replaced. Errors: 401 [`invalid_key`](#invalid_key); 404 [`not_found`](#not_found) when there is no Version before. **curl** ```sh title="Request" curl https://apis.dev/v1/products/companyfacts/rollbacks \ -H "Authorization: Bearer $APIS_DEV_TOKEN" \ -X POST ``` **CLI** ```sh title="CLI" $ npx apis.dev rollback ``` ```http title="Response" HTTP/1.1 200 OK Content-Type: application/json X-Request-Id: req_bc1acd9f19a060624e73 { "version": "v1", "digest": "be8c6389ef6c", "url": "https://companyfacts.apis.dev", "replaced": "v2" } ``` ### Set a secret `PUT /v1/products/{name}/secrets/{key}` · `npx apis.dev secrets set FIRMS_KEY` Stores a Source's credential for `secret()`. No reply, page or log shows it again. - `name` string, in path, required: The product's name, as the file's `name` gives it. For example `companyfacts`. - `key` string, in path, required: The name `secret()` reads. For example `FIRMS_KEY`. - `value` string, in body, required: The credential itself. Returns the secret by its name and when it was set, never its value. Errors: 401 [`invalid_key`](#invalid_key); 400 [`missing_param`](#missing_param) without a value. **curl** ```sh title="Request" curl https://apis.dev/v1/products/companyfacts/secrets/FIRMS_KEY \ -H "Authorization: Bearer $APIS_DEV_TOKEN" \ -X PUT \ --json '{ "value": "" }' ``` **CLI** ```sh title="CLI" $ npx apis.dev secrets set FIRMS_KEY ``` ```http title="Response" HTTP/1.1 200 OK Content-Type: application/json X-Request-Id: req_8d1c4047d8d1e1de3b25 { "key": "FIRMS_KEY", "updatedAt": "2026-10-24T09:00:00Z" } ``` ### Read one call `GET /v1/products/{name}/calls/{id}` · `npx apis.dev calls` One call by the request id its reply carried: what was asked, what it answered, what it cost and what it ran. - `name` string, in path, required: The product's name, as the file's `name` gives it. For example `companyfacts`. - `id` string, in path, required: The `X-Request-Id` of the call. Returns the call: its Verb, input, status, reply, price, Key and time. Errors: 401 [`invalid_key`](#invalid_key); 404 [`not_found`](#not_found) for an id your product never answered. **curl** ```sh title="Request" curl https://apis.dev/v1/products/companyfacts/calls/req_3cd59a9a1ede10c5bf21 \ -H "Authorization: Bearer $APIS_DEV_TOKEN" ``` **CLI** ```sh title="CLI" $ npx apis.dev calls req_3cd59a9a1ede10c5bf21 ``` ```http title="Response" HTTP/1.1 200 OK Content-Type: application/json X-Request-Id: req_320fae8b65bca1866c66 { "id": "req_3cd59a9a1ede10c5bf21", "verb": "Company.get", "input": { "domain": "acme.com" }, "status": 200, "output": { "domain": "acme.com", "employees": 240, "sector": "Software" }, "cost": { "price": 0.002, "currency": "USD" }, "key": "companyfacts_test_NNTT…", "at": "2026-10-24T09:00:00Z", "ms": 39 } ``` ### Follow the calls `GET /v1/products/{name}/logs` · `npx apis.dev logs --follow` Every call as it is answered, as a stream of Server-Sent Events, one `call` event each. - `name` string, in path, required: The product's name, as the file's `name` gives it. For example `companyfacts`. - `verb` string, in query, optional: Only this Verb's calls. For example `Company.get`. Returns a `text/event-stream`; each event holds a call as [Read one call](#platform-calls) answers it, without its reply. Errors: 401 [`invalid_key`](#invalid_key). **curl** ```sh title="Request" curl https://apis.dev/v1/products/companyfacts/logs \ -H "Authorization: Bearer $APIS_DEV_TOKEN" \ -N ``` **CLI** ```sh title="CLI" $ npx apis.dev logs --follow ``` ```http title="Response" HTTP/1.1 200 OK Content-Type: text/event-stream event: call id: req_3cd59a9a1ede10c5bf21 data: {"verb":"Company.get","status":200,"price":0.002} ``` ### List keys `GET /v1/products/{name}/keys` Your Test keys, and the Live keys your Customers hold, without their secrets. - `name` string, in path, required: The product's name, as the file's `name` gives it. For example `companyfacts`. - `env` string, in query, optional: `test` or `live`. Returns a list: each Key by its id, its environment, the start of its secret and who holds it. Errors: 401 [`invalid_key`](#invalid_key). **curl** ```sh title="Request" curl https://apis.dev/v1/products/companyfacts/keys \ -H "Authorization: Bearer $APIS_DEV_TOKEN" ``` ```http title="Response" HTTP/1.1 200 OK Content-Type: application/json X-Request-Id: req_62678a7a0339e43505a7 { "items": [ { "id": "key_1", "env": "test", "starts": "companyfacts_test_NNTT", "customer": null, "createdAt": "2026-10-24T09:00:00Z" } ], "total": 1, "next": null } ``` ### Read usage `GET /v1/products/{name}/usage` Calls and revenue by Verb for a period, and by Customer with `?by=customer`. - `name` string, in path, required: The product's name, as the file's `name` gives it. For example `companyfacts`. - `period` string, in query, optional: A month, `YYYY-MM`; the current one when absent. - `by` string, in query, optional: `verb` or `customer`. Returns the period's calls and revenue, line by line, in the Offer's currency. Errors: 401 [`invalid_key`](#invalid_key); 400 [`invalid_param`](#invalid_param) for a period that is not a month. **curl** ```sh title="Request" curl https://apis.dev/v1/products/companyfacts/usage?period=2026-10 \ -H "Authorization: Bearer $APIS_DEV_TOKEN" ``` ```http title="Response" HTTP/1.1 200 OK Content-Type: application/json X-Request-Id: req_d37442816c70008023a6 { "period": "2026-10", "currency": "USD", "lines": [ { "verb": "Company.get", "calls": 12400, "price": 0.002, "revenue": 24.8 }, { "verb": "Company.search", "calls": 1300, "price": 0.005, "revenue": 6.5 } ] } ``` ### List payments `GET /v1/products/{name}/payouts` The payments your connected Stripe account received, each beside the calls it paid for. - `name` string, in path, required: The product's name, as the file's `name` gives it. For example `companyfacts`. - `period` string, in query, optional: A month, `YYYY-MM`. Returns a list: each payment by its Stripe id, its Customer, its amount and the calls behind it. Errors: 401 [`invalid_key`](#invalid_key). **curl** ```sh title="Request" curl https://apis.dev/v1/products/companyfacts/payouts \ -H "Authorization: Bearer $APIS_DEV_TOKEN" ``` ```http title="Response" HTTP/1.1 200 OK Content-Type: application/json X-Request-Id: req_965da285f7dc14481f8d { "items": [ { "payment": "py_1QxAcme", "customer": "cus_Acme", "amount": 31.3, "currency": "USD", "period": "2026-10", "calls": 13700 } ], "next": null } ``` ### Build with an agent The docs MCP at [`/mcp`](/mcp.md) lets an agent build: fetch [`/llms.txt`](/llms.txt), write `definition.ts`, call `check` until it answers no Problems, then `deploy`. `check` and `plan` need no account; `deploy` signs in with GitHub through OAuth. *The docs MCP, for an agent that builds* | Tool | What it does | | --- | --- | | `search` | Finds a page or a section of these docs by words. | | `fetch` | Reads a page, or one of its sections, as Markdown. | | `check` | Takes a `definition.ts` and answers the compiled Definition, or every Problem with its code and `at`. | | `plan` | Answers what deploying the file would change: its Verbs, prices, margins and breaking changes. | | `deploy` | Publishes the file to your own domain once `check` answers no Problems, signed in with GitHub through OAuth. | ### Changes *What a change reaches* | Change | What happens | | --- | --- | | `Every deploy` | A Version with a digest; `Api-Version` on a request pins one. | | `A new Noun, Verb, field or plan` | Reaches every caller at once. | | `A removal, a rename, a narrowed type, a higher price per call` | Deploy names it as breaking, and each Key keeps the Version it first called until it sends `Api-Version`. | | `A lower price per call` | Reaches every caller at once. | | `A plan's new price` | Reaches each subscriber at renewal, with a Stripe notice; `keep: true` keeps current subscribers on the old price. | | `A run in flight` | Stays on the Version it started on, to its end. | | `npx apis.dev rollback` | Moves every Interface back to the Version before, as one. | ```sh title="Terminal" $ npx apis.dev deploy ! breaking: Company.domain is renamed to Company.website each Key keeps v3 until it sends Api-Version: v4 ✓ Team 49 → 59: 3 Customers move at their next renewal ✓ published v4 to https://pipeline.apis.dev ``` ## Events and Watches Every change writes one Event. Every Noun emits `created`, `updated`, `deleted` and `restored`, each move its past form, and a creating move only its own: `Deal.opened`, never `Deal.created`. *The Event envelope* | Field | What it carries | | --- | --- | | `id` | `evt_` and a sequence: the key a receiver dedupes on. | | `type` | The Event's name, `Noun.verbed`: `Deal.opened`, `Company.updated`. | | `at` | When it happened, in ISO 8601; a delivery's `t` is the same moment. | | `seq` | Its place in the API's change feed: rising by one, with no gaps. | | `data` | The Thing after the change; for an update, the fields that changed. | | `charged` | On the Event an outcome charges at: the price and its currency. | | `via` | The Integration or Source that wrote the change, with its `provenance`, when one did. | The change feed is `GET /events?type=&after=&limit=`, answering `{ items, next }` in `seq` order. Watch is a Noun on every API: `POST /watches` with `{ events, where?, url }` answers 201 with the Watch's `secret`, as `createWatch` over MCP and `pipeline watches create` in the CLI. Each delivery is signed, retried until it lands, and replayable from the developer portal. A receiver checks `X-Webhook-Signature`, refuses a `t` older than 5 minutes, and dedupes on `id`. `npx apis.dev events listen --forward` sends each Event to localhost, signed the same way. ## Errors A miss, a refusal or an error answers `application/problem+json` with its `type`, `title`, `status`, `detail`, `code` and `instance`, at price 0. ```json title="404 Not Found" { "type": "https://companyfacts.apis.dev/docs/errors#not_found", "title": "Not Found", "status": 404, "detail": "No Company has domain nobody.example.com.", "code": "not_found", "instance": "req_84326ad21bc488454616" } ``` | Code | HTTP | Means | Fix | | --- | --- | --- | --- | | `invalid_key` | 401 | The Key the call presented is not one the API accepts. | Send a current Key as `Authorization: Bearer `, or as `X-Api-Key`. | | `bad_environment` | 400 | The call asked a Test Key to act on Live records. | Call with a Live Key for Live records, or keep the call in Test. | | `no_route` | 404 | The path is not a Verb of the API. | Read `/openapi.json` or `/docs` for the paths the API serves. | | `method_not_allowed` | 405 | The API serves the path, but not with that HTTP method. | Call the Verb with the method its docs page shows. | | `key_required` | 401 | The Offer requires a Key, and the call presented none. | Send the Key as `Authorization: Bearer `, or as `X-Api-Key`. | | `not_entitled` | 403 | The Entitlement of the Key does not cover this Verb. | Take one of the `offers` the Problem carries, a plan or the Verb, through its Stripe Checkout link, then call again with the same Key. | | `role_required` | 403 | The move needs a Role the member behind the Key does not hold. | Call with the Key of a member who holds the Role the docs page names. | | `rate_limited` | 429 | The call is over a rate limit, which the clock refills. | Call again after the seconds `Retry-After` names; the refused call costs nothing. | | `over_max_cost` | 402 | The call would cost more than its `Max-Cost` allows, so it was refused before it ran. | Raise `Max-Cost` to the price the Problem names, or call without it; the refused call costs nothing. | | `plan_limit` | 402 | The call is past what the Customer's plan includes, and a plan at month 0 stops there. | Move to a plan in the `offers` the Problem carries, through its Stripe Checkout link, then call again with the same Key. | | `payment_required` | 402 | The credit balance is below the price of the call, which a payment refills. | Buy a pack in the `offers` the Problem carries, through its Stripe Checkout link, then call again with the same Key. | | `approval_required` | 403 | The call waits on a person: an Approval past the spend ceiling the owner set on the Key, or the move to Live. | Open the Approval URL the Problem carries; once it is granted, call again. | | `missing_param` | 400 | A required parameter or field is missing. | Send every parameter and field the Verb marks as required. | | `invalid_param` | 400 | A parameter or field is not of its type, misses its pattern, or is not one of its values. | Send it as the Verb declares it, as its docs page shows. | | `wrong_state` | 409 | The move is not allowed from the Thing's current state. | Make one of the moves the Problem lists in `allowed` first. | | `already_exists` | 409 | A record already holds the unique key the create sent. | Get the record by its key, or send another key. | | `not_found` | 404 | No record matches the input, so the call is a miss. | Check the input; a miss costs nothing. | | `invalid_batch` | 400 | The Batch body is not a list of 1 to 100 calls. | Send `{"calls": [...]}` with 1 to 100 items, each a Verb and its input. | | `invalid_call` | 400 | An MCP tool call named an unknown Verb or sent input it cannot read; 400 is the tool result's status, inside an HTTP 200 with `isError`. | Call `search` for the Verb, then `do` with its name and input. | | `unavailable` | 503 | The API could not check the call against its Limits, so nothing ran. | Call again after the seconds `Retry-After` names; nothing ran and the call cost nothing. | | `internal` | 500 | The Verb failed while it ran, and the call cost nothing. | Call again, and quote the `X-Request-Id` when asking about the call. |