Your API

The running jobjob_7Hq2 · delivered · document.fr-CA.pdf · $49.00 charged

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:

Run
curl http://localhost:8787/translation.json
200 · request-id: req_cgqaTN · services-version: 2026-10-25 · magic values: none
{
  "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-CA
OFFER · $49 per job, within 48 hours, charged on delivery.
  next    npx services.dev translation document=lease.pdf to=fr-CA --max 49
curl -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",
Output
magic values: none
same offer, translation, and the same body on every surface that has the call, request ids aside

Send 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:

StepAPICLIMCPSDK
1. Ask the pricePOST /jobs with service and no max_usd: 402, an offernpx services.dev translation k=v …order_translation without max_usdlingua.translation.order(input)
2. Ordersend the offer's next unchanged: 202, a jobthe next it printedthe offer's nextlingua.translation.order(offer.next.arguments)
3. WaitGET /jobs/:id?wait=50jobs wait <job>job://<job>?wait=50, or get_job with waitlingua.jobs.wait(id)
4. AcceptPOST /jobs/:id/acceptjobs accept <job>accept_joblingua.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:

TypeScript
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:

Output
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:

Output
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.get

Each 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.

HeaderSent withWhat it does
Authorization: Bearer <key>every call but keyless readswho is calling; another brand's key gets 401 key_invalid; on localhost, no header means the printed sk_test_ key
Idempotency-Keyevery writethe first answer, kept 24 hours and replayed byte for byte; a different body gets 409 key_reused
Services-Versionboth waysthe version that answered; pin a known one, or get 400 version_unknown
Request-Idevery answermatches an error's request_id; quote it about a call
Idempotent-Replayed: truea replaythis 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:

Output
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.