---
title: "Answers: OK, EMPTY, OFFER, BLOCKED"
url: https://services.dev/docs/api/answers
---
# Answers: OK, EMPTY, OFFER, BLOCKED

URL: https://services.dev/docs/api/answers

> Every call on every surface answers in one of four envelopes, and every no has one shape. An agent branches on them, never on prose.

A buyer's agent shouldn't parse a sentence to know what happened, so each answer is one of four envelopes, the AXP standard's, the same on the API, the CLI, MCP and the SDK. A list, an offer and every no carry theirs in `type`.

| Envelope | HTTP | It holds | What to do |
|---|---|---|---|
| **OK** | 200, 201, 202 | the object: a job, a list, a watch | read it; a job's `next` lists what you can call now |
| **EMPTY** | 200, 404 | a list with nothing in it, or an id nothing has | change the filter, or use an id an answer returned |
| **OFFER** | 402 | the price, the terms, the call that orders | send `next` unchanged, or don't |
| **BLOCKED** | 400 to 500 | a no, in the one shape below | send `next`, hand it to a person, or stop |

## OK

An OK is the object itself, with no wrapper. A job always carries `next`, the calls that would work now, so the caller never guesses what it may do. Here the buyer accepts a delivered job:

**CLI**

```txt
$ npx services.dev jobs accept job_7Hq2
job_7Hq2 · accepted · lease.fr-CA.pdf · $49.00 final
```

**API**

```txt
$ curl -X POST http://localhost:8787/jobs/job_7Hq2/accept -H 'content-type: application/json' -d '{}'
← 200 · job_7Hq2 · accepted · lease.fr-CA.pdf · $49.00 final
```

**MCP**

```txt
→ tools/call accept_job {"id":"job_7Hq2"}
← job_7Hq2 · accepted · lease.fr-CA.pdf · $49.00 final
```

**SDK**

```txt
await lingua.jobs.accept("job_7Hq2")
← job_7Hq2 · accepted · lease.fr-CA.pdf · $49.00 final
```

```txt
magic values: none
same job, job_7Hq2, and the same body on all four surfaces, request ids aside
```

The tabs print each surface's summary line. The body itself, whole, is the job a buyer may read: its input, its price, each step with its hash, and the output. An accepted job's `next` is empty, because nothing is left to call.

```txt
magic values: none

$ curl -X POST http://localhost:8787/jobs/job_7Hq2/accept -H 'content-type: application/json' -d '{}'
← 200 · {
  "id": "job_7Hq2",
  "object": "job",
  "created": "2026-10-25T14:02:11.000Z",
  "livemode": false,
  "simulated": true,
  "service": "translation",
  "input": {
    "document": {
      "url": "https://example.com/lease.pdf",
      "name": "lease.pdf"
    },
    "to": "fr-CA"
  },
  "metadata": {},
  "price": {
    "usd": 49,
    "approved": false,
    "max_usd": 49
  },
  "payment": {
    "status": "final",
    "amount_usd": 49
  },
  "human": "AI delivers every job.",
  "stage": "review",
  "steps": [
    {
      "id": "stp_1",
      "object": "step",
      "step": "draft",
      "by": "ai",
      "status": "committed",
      "hash": "sha256:7dbf1c28960226c2d8a1cc1ca8a5125d56fb7cd3ce11585532c2caedd411bff4"
    },
    {
      "id": "stp_2",
      "object": "step",
      "step": "review",
      "by": "ai",
      "review": "adversarial",
      "verdict": "pass",
      "status": "committed",
      "hash": "sha256:95851b48d6d0adf87f5c3d1210a3157976f66081111d2367176e5f8c92508cb2"
    }
  ],
  "next": [],
  "status": "accepted",
  "output": {
    "document": {
      "url": "data:text/plain,Simulated%20Translation%3A%20document",
      "name": "lease.fr-CA.pdf"
    }
  },
  "final_at": "2026-11-01T14:02:11.000Z"
}
```

Every field of the job, and every other object, is in [Objects](https://services.dev/docs/reference/objects.md).

### Nothing there is still an answer

A list with nothing in it is an OK with an empty `data`, never an error:

**CLI**

```txt
$ npx services.dev jobs list
{
  "object": "list",
  "data": [],
  "has_more": false,
  "url": "/jobs",
  "message": "No jobs yet."
}
```

**API**

```txt
$ curl 'http://localhost:8787/jobs?limit=10'
← 200 · {
  "object": "list",
  "data": [],
  "has_more": false,
  "url": "/jobs",
  "message": "No jobs yet."
}
```

**MCP**

```txt
→ tools/call list_jobs {}
← {
  "object": "list",
  "data": [],
  "has_more": false,
  "url": "/jobs",
  "message": "No jobs yet."
}
```

**SDK**

```txt
await lingua.jobs.list()
← {
  "object": "list",
  "data": [],
  "has_more": false,
  "url": "/jobs",
  "message": "No jobs yet."
}
```

```txt
magic values: none
same list, and the same body on every surface that has the call, request ids aside
```

A record that isn't there is EMPTY, a 404 `not_found`, with `param` naming the id:

**CLI**

```txt
$ npx services.dev jobs get job_0000
No job job_0000.
  docs    http://localhost:8787/docs/errors/not_found
```

**API**

```txt
$ curl http://localhost:8787/jobs/job_0000
← 404 · {
  "type": "EMPTY",
  "category": "invalid_request",
  "code": "not_found",
  "reason": "No job job_0000.",
  "param": "id",
  "next": null,
  "doc_url": "http://localhost:8787/docs/errors/not_found",
  "request_id": "req_c7PJaH",
  "simulated": true
}
```

**MCP**

```txt
→ tools/call get_job {"id":"job_0000"}
← isError: true, structuredContent:
← {
  "type": "EMPTY",
  "category": "invalid_request",
  "code": "not_found",
  "reason": "No job job_0000.",
  "param": "id",
  "next": null,
  "doc_url": "http://localhost:8787/docs/errors/not_found",
  "request_id": "req_vzPAPG",
  "simulated": true
}
```

**SDK**

```txt
await lingua.jobs.get("job_0000")
× throws InvalidRequestError<'job.get'> · 404
← {
  "type": "EMPTY",
  "category": "invalid_request",
  "code": "not_found",
  "reason": "No job job_0000.",
  "param": "id",
  "next": null,
  "doc_url": "http://localhost:8787/docs/errors/not_found",
  "request_id": "req_c7PJaH",
  "simulated": true
}
```

```txt
magic values: none
same refusal, not_found, and the same body on every surface that has the call, request ids aside
```

A list with nothing in it is EMPTY too, at 200, so an agent tells an empty answer from a full one by `type` alone.

## OFFER

An offer is a 402, not an error. Nothing is held until the buyer names the most they'll pay. An order without `max_usd`, or with less than the price, gets the offer: the price sentence, the terms written from the Service's steps, and the call to send. Its `next.arguments` are the order you sent, with `max_usd` filled in.

**CLI**

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

**API**

```txt
$ 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"}'
← 402 · {
  "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
→ 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",
      "max_usd": 49,
      "key": "lease-fr-CA",
      "service": "translation"
    },
    "tool": "order_job",
    "http": "POST http://localhost:8787/jobs {\"service\":\"translation\"}"
  },
  "simulated": true
}
```

**SDK**

```txt
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",
      "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
```

### Past the buyer key's cap

Every buyer key has a cap: the most its live holds may add up to. An order that would take the key past it gets an offer too, but its `next` is a checkout, because only a person raises a cap. The agent hands `next.url` to that person; once they have, it sends `next.then`, the order unchanged:

```txt
magic values: none

# this buyer key's cap is $500, and two Certified Translations already hold $378 on it
$ curl -X POST http://localhost:8787/jobs -H 'content-type: application/json' -d '{"service":"certified-translation","document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr","country":"FR","max_usd":189,"key":"lease-fr-3"}'
← 402 · {
  "type": "OFFER",
  "object": "offer",
  "service": "certified-translation",
  "title": "Certified Translation",
  "price": {
    "usd": 189,
    "sentence": "$189 per job, within 3 days, charged on delivery."
  },
  "eta": "3d",
  "terms": "Charged on delivery. Cancel at $0 until it ships, except the work a person attested once the signature before certify is done. Send it back with a note and the AI redoes it, or you pay $0. Final 7 days after delivery.",
  "checkout_url": "http://localhost:8787/keys/sk_test_7Hq2",
  "next": {
    "do": "checkout",
    "url": "http://localhost:8787/keys/sk_test_7Hq2",
    "tool": null,
    "http": "GET http://localhost:8787/keys/sk_test_7Hq2",
    "then": {
      "do": "order",
      "service": "certified-translation",
      "arguments": {
        "document": {
          "url": "https://example.com/lease.pdf",
          "name": "lease.pdf"
        },
        "to": "fr",
        "country": "FR",
        "max_usd": 189,
        "key": "lease-fr-3",
        "service": "certified-translation"
      },
      "tool": "order_job",
      "http": "POST http://localhost:8787/jobs {\"service\":\"certified-translation\"}"
    }
  },
  "simulated": true
}
```

`/pricing` publishes the cap as `hardCeiling`, so an agent can know it before it orders: [Machine registers](https://services.dev/docs/api/machine.md).

## BLOCKED: one shape for every no

Here a buyer's agent tries to accept a job that waits on the client's signature. Only the client can sign, through their own link, so the answer hands the link back:

**CLI**

```txt
$ npx services.dev jobs accept job_7Hq3
sign waits on Ada Lovelace to sign. Only they can, through their own link. Hand them the link in next.
  next    hand http://localhost:8787/a/ask_7Hq2?t=cHGjprSn08ODyOp_8zLIVH to the person it asks
  docs    http://localhost:8787/docs/errors/needs_the_party
```

**API**

```txt
$ curl -X POST http://localhost:8787/jobs/job_7Hq3/accept -H 'content-type: application/json' -d '{}'
← 403 · {
  "type": "BLOCKED",
  "category": "blocked",
  "code": "needs_the_party",
  "reason": "sign waits on Ada Lovelace to sign. Only they can, through their own link. Hand them the link in next.",
  "param": "ask_7Hq2",
  "next": {
    "do": "hand_to_person",
    "url": "http://localhost:8787/a/ask_7Hq2?t=cHGjprSn08ODyOp_8zLIVH",
    "arguments": {
      "ask": "ask_7Hq2"
    },
    "party": {
      "name": "Ada Lovelace",
      "email": "ada@example.com"
    },
    "to": "sign",
    "expires_at": "2026-11-01T14:02:11.000Z",
    "tool": null,
    "http": "GET http://localhost:8787/a/ask_7Hq2?t=cHGjprSn08ODyOp_8zLIVH"
  },
  "doc_url": "http://localhost:8787/docs/errors/needs_the_party",
  "request_id": "req_SvsTKa",
  "simulated": true
}
```

**MCP**

```txt
→ tools/call accept_job {"id":"job_7Hq3"}
← isError: true, structuredContent:
← {
  "type": "BLOCKED",
  "category": "blocked",
  "code": "needs_the_party",
  "reason": "sign waits on Ada Lovelace to sign. Only they can, through their own link. Hand them the link in next.",
  "param": "ask_7Hq2",
  "next": {
    "do": "hand_to_person",
    "url": "http://localhost:8787/a/ask_7Hq2?t=cHGjprSn08ODyOp_8zLIVH",
    "arguments": {
      "ask": "ask_7Hq2"
    },
    "party": {
      "name": "Ada Lovelace",
      "email": "ada@example.com"
    },
    "to": "sign",
    "expires_at": "2026-11-01T14:02:11.000Z",
    "tool": null,
    "http": "GET http://localhost:8787/a/ask_7Hq2?t=cHGjprSn08ODyOp_8zLIVH"
  },
  "doc_url": "http://localhost:8787/docs/errors/needs_the_party",
  "request_id": "req_4Ruoeo",
  "simulated": true
}
```

**SDK**

```txt
await lingua.jobs.accept("job_7Hq3")
× throws BlockedError<'job.accept'> · 403
← {
  "type": "BLOCKED",
  "category": "blocked",
  "code": "needs_the_party",
  "reason": "sign waits on Ada Lovelace to sign. Only they can, through their own link. Hand them the link in next.",
  "param": "ask_7Hq2",
  "next": {
    "do": "hand_to_person",
    "url": "http://localhost:8787/a/ask_7Hq2?t=cHGjprSn08ODyOp_8zLIVH",
    "arguments": {
      "ask": "ask_7Hq2"
    },
    "party": {
      "name": "Ada Lovelace",
      "email": "ada@example.com"
    },
    "to": "sign",
    "expires_at": "2026-11-01T14:02:11.000Z",
    "tool": null,
    "http": "GET http://localhost:8787/a/ask_7Hq2?t=cHGjprSn08ODyOp_8zLIVH"
  },
  "doc_url": "http://localhost:8787/docs/errors/needs_the_party",
  "request_id": "req_SvsTKa",
  "simulated": true
}
```

```txt
magic values: none
same refusal, needs_the_party, and the same body on all four surfaces, request ids aside
```

Every no on every surface has these fields:

| Field | Holds |
|---|---|
| `type` | the envelope: `BLOCKED`, or `EMPTY` for an id nothing has |
| `category` | the kind of no, from the table below |
| `code` | the reason, as one word to branch on |
| `reason` | one sentence for a person, written for this job |
| `param` | the field or id at fault, when there is one |
| `next` | the call that would work; `hand_to_person` with the `url`, the `party`, what they're asked `to` do and `expires_at`; or `null` when nothing would |
| `doc_url` | the code's page, on your domain |
| `request_id` | the id in the `Request-Id` header |
| `simulated` | `true` in the sandbox |

Over MCP it's the same body, marked `isError`. The SDK throws `BlockedError<'job.accept'>` carrying it, typed with the codes `job.accept` can return. The CLI prints the reason and exits with the category's code; with `--json` it prints the body.

### Whether to send it again

The category alone tells a caller whether the same call can ever work:

```txt
magic values: none

category         HTTP         codes exit  send it again?
invalid_request  400 404 422  10    3     No: the same call gets the same answer. Send next.
conflict         409 410      22    6     No, not until the job's state changes. Read the job, then send next.
authentication   401          2     4     No: send the right key first.
blocked          403          10    5     No: only a person can take this step. Hand next to one.
guard            422          1     6     No: fix the value the guard names, then send it again.
unavailable      403          3     7     Not yet: nothing the caller sends changes it. Join the waitlist in next, and the same call works once it opens.
api_error        500          1     10    Yes: the same call with the same Idempotency-Key, which never runs it twice.
```

Every code and its fix: [Errors](https://services.dev/docs/reference/errors.md).

### When only a person can

`needs_the_party` and `needs_a_person` mean a key or a tool reached an act that only a person may take: signing, consenting, approving. No retry helps. The agent passes `next.url` to that person, then reads the job again. [What an agent can never do](https://services.dev/docs/mcp/never.md) lists those acts, and [Asks](https://services.dev/docs/service/ask.md) covers the link the party opens.

---

- Up: [Your API](https://services.dev/docs/api.md)
- Previous: [Your API](https://services.dev/docs/api.md)
- Next: [Conventions](https://services.dev/docs/api/conventions.md)
