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.
Open in
You need Node 20 and a GitHub account. The first deploy answers at once on pipeline.apis.dev; your own domain is one line.
Start the folder
Terminal $ npx apis.dev init headless-saas ✓ wrote definition.ts:a Headless SaaS next: npx apis.dev dev Write the file
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.stageadds a Verb per move. Free stops at what itincludes; Team draws the price past it.Try it locally
Terminal $ npx apis.dev dev ✓ every Interface on http://localhost: 8787, against your Test twin reloads on every save Publish it
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.
Add a Company
POST pipeline.apis.dev/companiesRequest curl https://pipeline.apis.dev/ companies \ -H "Authorization: Bearer $PIPELINE_KEY" \ --json '{"name": "Acme", "domain": "acme.com"}' 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" } 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' } 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" } 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" } 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" } 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 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" } 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" } } }' 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" } } } A create answers 201 with the record, where it lives, and the Action it ran as.
Open a Deal
POST pipeline.apis.dev/dealsRequest curl https://pipeline.apis.dev/ deals \ -H "Authorization: Bearer $PIPELINE_KEY" \ --json '{"title": "Pilot", "amount": "5000.00", "company": "company_1"}' 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" } 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' } 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" } 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" } 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" } 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 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" } 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" } } }' 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" } } } Opening the Deal wrote
Deal.opened. Free includes it, so its price is 0.Get the webhook
POST pipeline.apis.dev/watchesRequest curl https://pipeline.apis.dev/ watches \ -H "Authorization: Bearer $PIPELINE_KEY" \ --json '{ "events": ["Deal.opened"], "url": "https: //hooks.acme.com/ hooks/ pipeline" }' 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" } 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' } 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" } 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" } 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" } 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 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" } 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" } } }' 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" } } } 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" } } 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<string>() 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, and each
Deal.openedgoes out signed, retried until their URL answers.npx apis.dev events listen --forward localhost:tests the receiver first.3000/ hooks Win it too early
POST pipeline.apis.dev/deals/deal_1/winRequest curl -X POST https://pipeline.apis.dev/ deals/ deal_1/ win \ -H "Authorization: Bearer $PIPELINE_KEY" 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"] } 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' } 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"] } 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" } 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"] } Terminal $ npx pipeline login --key $PIPELINE_KEY $ npx pipeline deals win deal_1 error 409 wrong_state price 0 USD · req_66b1b3cad16f500d936fResponse 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"] } 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" } } }' 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" } } } A Deal moves only along its lifecycle:
winneedsqualifiedfirst, and the refusal lists the moves it allows.Qualify it, then win it
Request curl -X POST https://pipeline.apis.dev/ deals/ deal_1/ qualify \ -H "Authorization: Bearer $PIPELINE_KEY" 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" } Request curl -X POST https://pipeline.apis.dev/ deals/ deal_1/ win \ -H "Authorization: Bearer $PIPELINE_KEY" 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.Kick off when a Deal is won
import { define, workflow } from 'apis.dev'
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' },curl https: //pipeline.apis.dev/ actions/ act_8 \
-H "Authorization: Bearer $PIPELINE_KEY"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"
}curl https: //pipeline.apis.dev/ actions/ act_8/ complete \
-H "Authorization: Bearer $MANAGER_KEY" \
--json '{"output": {"approved": true}}'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, and the run ends with its output. See Workflow.
- Outgrow the Free plan
curl https: //pipeline.apis.dev/ deals \
-H "Authorization: Bearer $PIPELINE_KEY" \
--json '{"title": "Renewal", "amount": "1200.00", "company": "company_1"}'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"
}
]
}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' }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"
}
]
}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" }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"
}
]
}$ 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_02f4f9b0fd2d3af756baHTTP/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"
}
]
}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/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"
}
}
}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
Build: you wrote 18 lines: two Nouns, a Relationship, a lifecycle and an Offer, then 8 more for a Workflow on Deal.won. Launch: 15 Verbs answer on pipeline.apis.dev, a wrong move answers 409, and each change goes out on a signed webhook. Monetize: Free includes 25 Deal.open a month, and the call past them answers 402 with Team on Stripe Checkout. You wrote no CRUD, no state checks, no Events and no webhook code.
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 → Another API writes each field. You pick the shape and the price.
- Headless SaaS: records, the lifecycle, Integrations, plans and seats.
- Workflow: what runs when a Deal is won.