---
title: "Conventions"
url: https://services.dev/docs/api/conventions
captured: [api-events-list.txt, api-quote.txt, api-replay.txt]
---
# Conventions

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

> The rules every call follows: money on the wire, idempotency, lists and the feed's cursor, versions, webhook signatures, request ids and casing.

Every brand's API follows the same rules on every route, so an integrator learns them once. Each one below is shown as a captured call.

## Money

You write prices in dollars: `price: 49`, `max_usd: 49`. A buyer reads dollars where a person reads money: an order's `max_usd`, a job's `payment.amount_usd`, a receipt's `fee_usd`, an event's `amount_usd`. A machine computes in integer cents where the manifest and the quote carry a price, beside the sentence a person reads:

```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"
  }
}
```

## Idempotency

Every write takes an `Idempotency-Key` header; an order's `key` field is the same key, for the CLI and MCP. The first answer is kept for 24 hours. Sending the same key again replays it byte for byte, with its original status and `idempotent-replayed: true`, and emits no event twice. The same key with a different body is `409 key_reused`, and `param` names the first field that differs.

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

```txt title="magic values: none"
HTTP/1.1 202
request-id: req_WppKI6
services-version: 2026-10-25
← job_7Hq2 · working · $49.00 held
```

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

```txt title="magic values: none"
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.
```

## Lists

A list is `{ object: 'list', data, has_more, url }`. `limit` takes up to 100, and `starting_after` takes the last id you saw. The events feed pages by `seq` instead, with `after` and `next_seq`: the feed is an ordered log, each event's `seq` never changes, and a reader that stopped at seq 41 resumes after 41, however many events arrived meanwhile.

```bash title="API"
curl 'http://localhost:8787/events?after=0&limit=2'
```

```json title="200 · magic values: none"
{
  "type": "OK",
  "object": "list",
  "data": [
    {
      "id": "evt_5gX1X2B5swzY61",
      "object": "event",
      "seq": 1,
      "type": "job.ordered",
      "created": "2026-10-25T14:02:11.000Z",
      "job": "job_7Hq2",
      "service": "translation",
      "data": {
        "by": "buyer",
        "amount_usd": 49
      },
      "request": {
        "id": "req_WppKI6",
        "idempotency_key": null
      },
      "livemode": false,
      "simulated": true
    },
    {
      "id": "evt_88cLb4PC0MfpT4",
      "object": "event",
      "seq": 2,
      "type": "charge.held",
      "created": "2026-10-25T14:02:11.000Z",
      "job": "job_7Hq2",
      "service": "translation",
      "data": {
        "amount_usd": 49
      },
      "request": {
        "id": "req_WppKI6",
        "idempotency_key": null
      },
      "livemode": false,
      "simulated": true
    }
  ],
  "has_more": true,
  "url": "/events",
  "next_seq": 11
}
```

## Versions

Every answer carries `services-version`, such as `2026-10-25`. A request may pin one with the `Services-Version` header, and a version the brand doesn't serve is `400 version_unknown`. `npx services.dev diff` marks every change to a Service breaking or not: a removed field, a newly required field, a changed type or a removed enum value breaks. A key can pin a version, so its answers never change shape under it, and `check --against live` fails a breaking change before you deploy it.

## Webhook signatures

Each delivery carries `services-signature: t=<unix seconds>,v1=<hex>`, where `v1` is the HMAC-SHA256 of `<t>.<raw body>`, keyed with the watch's `whsec_` secret. Verify the raw bytes as they arrived, before parsing them, and refuse a `t` more than 300 seconds from now. [Your events](https://services.dev/docs/api/events.md#verify-without-the-sdk) has a verifier in six lines and the retry schedule.

## Request ids

Every answer carries a `request-id` header, and a refusal repeats it as `request_id`. Each event names the request that caused it, `{ id, idempotency_key }`, or null when the work or a clock did.

## Casing

Fields, query parameters, codes, events and paths are snake_case: `send_back`, `max_usd`, `next_seq`, `approved_ledger`. Every id starts with its object's prefix, such as `job_`, `ask_` or `rcp_`: [Objects](https://services.dev/docs/reference/objects.md) lists them.

---

- Up: [Your API](https://services.dev/docs/api.md)
- Previous: [Answers: OK, EMPTY, OFFER, BLOCKED](https://services.dev/docs/api/answers.md)
- Next: [Your events](https://services.dev/docs/api/events.md)
