---
title: "Your API"
url: https://services.dev/docs/api
captured: [api-offer.txt, api-quote.txt]
---
# Your API

URL: https://services.dev/docs/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.

## The first call needs no key

Ask a Service what it is and what it costs, with no key, account or card:

```bash
curl http://localhost:8787/translation.json
```

```json title="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.

**CLI**

```bash title="CLI"
npx services.dev translation document=https://example.com/lease.pdf to=fr-CA --key lease-fr-CA
```

```txt title="magic values: none"
OFFER · $49 per job, within 48 hours, charged on delivery.
  next    npx services.dev translation document=lease.pdf to=fr-CA --max 49
```

**API**

```bash title="API"
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"}'
```

```json title="402 · magic values: none"
{
  "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",
      "max_usd": 49,
      "key": "lease-fr-CA",
      "service": "translation"
    },
    "tool": "order_job",
    "http": "POST http://localhost:8787/jobs {\"service\":\"translation\"}"
  },
  "simulated": true
}
```

**MCP**

```txt title="MCP"
→ tools/call order_job {"service":"translation","document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","key":"lease-fr-CA"}
```

```txt title="magic values: none"
← {
  "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",
      "max_usd": 49,
      "key": "lease-fr-CA",
      "service": "translation"
    },
    "tool": "order_job",
    "http": "POST http://localhost:8787/jobs {\"service\":\"translation\"}"
  },
  "simulated": true
}
```

**SDK**

```ts title="SDK"
await lingua.translation.order({"document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","key":"lease-fr-CA"})
```

```txt title="magic values: none"
← {
  "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",
      "max_usd": 49,
      "key": "lease-fr-CA",
      "service": "translation"
    },
    "tool": "order_job",
    "http": "POST http://localhost:8787/jobs {\"service\":\"translation\"}"
  },
  "simulated": true
}
```

```txt
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](https://services.dev/docs.md) follows it all the way to an accepted delivery.

## One job, four calls

Every buyer, and every buyer's agent, makes the same four calls. Each is one row of [Operations](https://services.dev/docs/reference/operations.md), 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.

## What an order takes

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:

```ts
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:

```txt
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.

## Your routes

These are the routes lingua serves, read from its own `openapi.json`:

```txt
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
POST /jobs/{id}/accept                 job.accept
POST /jobs/{id}/send_back              job.send_back
POST /jobs/{id}/cancel                 job.cancel
POST /jobs/{id}/end                    job.end
POST /jobs/{id}/again                  job.again
POST /jobs/{id}/policy                 job.policy
POST /jobs/{id}/signals                job.signal
POST /jobs/{id}/messages               job.message
GET /jobs/{id}/messages                job.thread
GET /jobs/{id}/record                  job.record
POST /{portal}/jobs/{id}/approve       job.approve
POST /{portal}/join                    member.join
POST /{portal}/members/{id}/verify     member.verify
POST /me/availability                  member.availability
GET /me/ledger                         member.ledger.get
POST /me/ledger                        member.ledger.add
GET /me/ledger/checks                  member.ledger.checks
GET /me/missed                         member.missed
GET /tasks                             task.list
GET /tasks/{id}                        task.get
GET /tasks/{id}/sheet                  task.sheet
POST /tasks/{id}/pass                  task.pass
POST /tasks/{id}/claim                 task.claim
POST /tasks/{id}/clear                 task.clear
GET /tasks/{id}/packet                 task.packet
POST /tasks/{id}/release               task.release
POST /owner/tasks/{id}/offer           task.offer
GET /clocks                            clock.list
GET /clocks/{id}                       clock.get
POST /clocks/{id}/ack                  clock.ack
POST /clocks/{id}/satisfy              clock.satisfy
POST /clocks/{id}/choose               clock.choose
POST /owner/clocks/{id}/assign         clock.assign
GET /subscriptions                     subscription.list
GET /subscriptions/{id}                subscription.get
POST /subscriptions/{id}/cancel        subscription.cancel
POST /owner/members/{id}/login         member.login
GET /asks/{id}                         ask.get
POST /asks/{id}                        ask.complete
POST /asks/{id}/revoke                 ask.revoke
POST /asks/{id}/reissue                ask.reissue
GET /jobs/{id}/files                   job.files.list
PUT /jobs/{id}/files/{sha256}          job.files.put
GET /jobs/{id}/files/{sha256}          job.files.get
GET /jobs/{id}/export                  job.export
GET /owner/jobs/{id}                   job.ledger
POST /keys                             key.create
POST /keys/{key}                       key.raise
POST /test_clocks                      test_clock.create
POST /test_clocks/{id}/advance         test_clock.advance
GET /events                            event.list
POST /events                           event.inbound
GET /events/stream                     event.stream
POST /watches                          watch.create
GET /watches                           watch.list
DELETE /watches/{id}                   watch.delete
POST /watches/{id}/replay              watch.replay
GET /index.json                        brand.home
GET /healthz                           brand.health
GET /fixtures                          sandbox.fixtures
GET /.well-known/agents.json           brand.card
GET /openapi.json                      brand.openapi
GET /llms.txt                          brand.llms
GET /manifest.json                     brand.manifest
GET /pricing                           brand.pricing
GET /verify/suite.json                 brand.verify

94 routes · 94 operations · webhooks: 58
```

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](https://services.dev/docs/reference/operations.md) prints them all, and [the five registries](https://services.dev/docs/surfaces/contract.md) explains how they join.

Paths use generic words until `Brand.words` names your own.

## What every call carries

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

```txt
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.
```

## Where to go next

Every call answers OK, EMPTY, OFFER or BLOCKED: [Answers](https://services.dev/docs/api/answers.md). What a job does next reaches you as [your events](https://services.dev/docs/api/events.md). What an agent reads first is in [Machine registers](https://services.dev/docs/api/machine.md), and [your conformance suite](https://services.dev/docs/api/verify.md) proves it all holds.

Your buyers read this API's reference on your domain, one page per operation: [lingua's](https://services.dev/docs/examples/lingua.md).

---

- Up: [Your first paid job](https://services.dev/docs.md)
- Previous: [The contract](https://services.dev/docs/surfaces/contract.md)
- Next: [Answers: OK, EMPTY, OFFER, BLOCKED](https://services.dev/docs/api/answers.md)
