Your API
Every Service you write is an API on your own domain, generated from its file. The first call needs no key.
You never write a route. Each Service you write gets three: an order route whose body is the Service's own input, a keyless read of what it is and costs, and a waitlist. Every job, ask, event and watch then gets the same shared calls, named alike on the API, CLI, MCP and SDK. This page runs lingua, the example brand, on localhost.
Ask a Service what it is and what it costs, with no key, account or card:
curl http://localhost:8787/translation.json{
"type": "OK",
"id": "translation",
"name": "Translation",
"price": {
"per": "job",
"cents": 4900,
"within": "48h",
"ai_cap": "20%",
"final_after": "7d",
"charged": "on delivery",
"sentence": "$49 per job, within 48 hours, charged on delivery."
},
"surfaces": {
"route": "POST /jobs {\"service\":\"translation\"}",
"tool": "order_translation"
}
}Then order it without max_usd. Nothing is held. The answer is an offer: the price, the terms, and the exact call to send next.
npx services.dev translation document=https://example.com/lease.pdf to=fr-CA --key lease-fr-CAOFFER · $49 per job, within 48 hours, charged on delivery.
next npx services.dev translation document=lease.pdf to=fr-CA --max 49curl -X POST http://localhost:8787/jobs -H 'content-type: application/json' -d '{"service":"translation","document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","key":"lease-fr-CA"}'{
"type": "OFFER",
"object": "offer",
"service": "translation",
"title": "Translation",
"price": {
"usd": 49,
"sentence": "$49 per job, within 48 hours, charged on delivery."
},
"eta": "48h",
"terms": "Charged on delivery. Cancel at $0 until it ships. Send it back with a note and the AI redoes it, or you pay $0. Final 7 days after delivery.",
"next": {
"do": "order",
"service": "translation",
"arguments": {
"document": {
"url": "https://example.com/lease.pdf",
"name": "lease.pdf"
},
"to": "fr-CA",
→ tools/call order_job {"service":"translation","document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","key":"lease-fr-CA"}← {
"type": "OFFER",
"object": "offer",
"service": "translation",
"title": "Translation",
"price": {
"usd": 49,
"sentence": "$49 per job, within 48 hours, charged on delivery."
},
"eta": "48h",
"terms": "Charged on delivery. Cancel at $0 until it ships. Send it back with a note and the AI redoes it, or you pay $0. Final 7 days after delivery.",
"next": {
"do": "order",
"service": "translation",
"arguments": {
"document": {
"url": "https://example.com/lease.pdf",
"name": "lease.pdf"
},
"to": "fr-CA",
await lingua.translation.order({"document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","key":"lease-fr-CA"})← {
"type": "OFFER",
"object": "offer",
"service": "translation",
"title": "Translation",
"price": {
"usd": 49,
"sentence": "$49 per job, within 48 hours, charged on delivery."
},
"eta": "48h",
"terms": "Charged on delivery. Cancel at $0 until it ships. Send it back with a note and the AI redoes it, or you pay $0. Final 7 days after delivery.",
"next": {
"do": "order",
"service": "translation",
"arguments": {
"document": {
"url": "https://example.com/lease.pdf",
"name": "lease.pdf"
},
"to": "fr-CA",
magic values: none
same offer, translation, and the same body on every surface that has the call, request ids asideSend next unchanged and the job starts. Your first paid job follows it all the way to an accepted delivery.
Every buyer, and every buyer's agent, makes the same four calls. Each is one row of Operations, named alike on every surface:
| Step | API | CLI | MCP | SDK |
|---|---|---|---|---|
| 1. Ask the price | POST /jobs with service and no max_usd: 402, an offer | npx services.dev translation k=v … | order_translation without max_usd | lingua.translation.order(input) |
| 2. Order | send the offer's next unchanged: 202, a job | the next it printed | the offer's next | lingua.translation.order(offer.next.arguments) |
| 3. Wait | GET /jobs/:id?wait=50 | jobs wait <job> | job://<job>?wait=50, or get_job with wait | lingua.jobs.wait(id) |
| 4. Accept | POST /jobs/:id/accept | jobs accept <job> | accept_job | lingua.jobs.accept(id) |
POST /translation is an alias, with the Service in the path.
The body of POST /jobs is service and the input you wrote, plus the fields every order takes. This is the whole of lingua's Translation:
import { Service } from 'services.dev'
export default Service({
name: 'Translation',
input: { document: 'file', to: 'language: a BCP 47 tag, such as fr-CA' },
output: { document: 'file' },
price: 49,
})And this is the body it takes:
magic values: none
$ curl http://localhost:8787/translation.json # input: the Service's own fields, each by its type word
document file required { url, name }
to language required a BCP 47 tag, such as fr-CA
# and the fields every order takes, from the service.order row
service string The Service to order. POST /<service> names it in the path instead.
max_usd number The most you will pay, in dollars. Missing or below the price, you get an offer instead of a job.
key string Any string you choose. Retrying with the same key returns the same job.
metadata object Your own keys, echoed on the job and its events.
test_clock string A test clock the job runs on: tc_….The text after a type word becomes the field's description in its JSON Schema, which openapi.json and the MCP tool both carry. Add a field to input and, in one change, it's a body field here, a field=value argument on the CLI, a tool parameter over MCP and a typed property in the SDK.
These are the routes lingua serves, read from its own openapi.json:
magic values: none
$ curl http://localhost:8787/openapi.json # each path and method, by its operationId
POST /jobs service.order
GET /jobs job.list
POST /certified-translation service.order.certified-translation
POST /store-listing service.order.store-listing
POST /translated-summary service.order.translated-summary
POST /translation service.order.translation
GET /certified-translation.json service.quote.certified-translation
GET /store-listing.json service.quote.store-listing
GET /translated-summary.json service.quote.translated-summary
GET /translation.json service.quote.translation
POST /certified-translation/waitlist service.waitlist.certified-translation
POST /store-listing/waitlist service.waitlist.store-listing
POST /translated-summary/waitlist service.waitlist.translated-summary
POST /translation/waitlist service.waitlist.translation
GET /catalog service.list
GET /catalog/{id} service.get
GET /jobs/{id} job.getEach Service adds POST /<service> (the same order as POST /jobs), GET /<service>.json and POST /<service>/waitlist. Every other route is shared. Each route is a row of the operation registry, and its CLI command, MCP tool and SDK method are columns of that row: Operations prints them all, and the five registries explains how they join.
Paths use generic words until Brand.words names your own.
| Header | Sent with | What it does |
|---|---|---|
Authorization: Bearer <key> | every call but keyless reads | who is calling; another brand's key gets 401 key_invalid; on localhost, no header means the printed sk_test_ key |
Idempotency-Key | every write | the first answer, kept 24 hours and replayed byte for byte; a different body gets 409 key_reused |
Services-Version | both ways | the version that answered; pin a known one, or get 400 version_unknown |
Request-Id | every answer | matches an error's request_id; quote it about a call |
Idempotent-Replayed: true | a replay | this is the kept answer; nothing ran twice |
An order's own key makes the order safe to retry: the same key returns the same job. The Idempotency-Key header does the same for any write, for 24 hours. Here one order is sent twice under one header, then the header with a different language:
magic values: none
$ curl -i -X POST http://localhost:8787/jobs -H 'content-type: application/json' -H 'Idempotency-Key: lease-fr-CA' -d '{"service":"translation","document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","max_usd":49,"key":"lease"}'
HTTP/1.1 202
request-id: req_WppKI6
services-version: 2026-10-25
← job_7Hq2 · working · $49.00 held
$ curl -i -X POST http://localhost:8787/jobs -H 'content-type: application/json' -H 'Idempotency-Key: lease-fr-CA' -d '{"service":"translation","document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","max_usd":49,"key":"lease"}'
HTTP/1.1 202
request-id: req_m8cwgn
services-version: 2026-10-25
idempotent-replayed: true
← job_7Hq2 · working · $49.00 held
$ curl -i -X POST http://localhost:8787/jobs -H 'content-type: application/json' -H 'Idempotency-Key: lease-fr-CA' -d '{"service":"translation","document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"de","max_usd":49,"key":"lease"}'
HTTP/1.1 409
request-id: req_d8ix4b
services-version: 2026-10-25
← key_reused · param to · This Idempotency-Key was first sent with a different body: to differs. Send a fresh key for a different call.Every call answers OK, EMPTY, OFFER or BLOCKED: Answers. What a job does next reaches you as your events. What an agent reads first is in Machine registers, and your conformance suite proves it all holds.
Your buyers read this API's reference on your domain, one page per operation: lingua's.