# Requests and responses

Every request and response follows the same conventions. This page specifies content types, naming, the shape of single objects and lists, request ids and the headers returned on every response.

## JSON only

- Request and response bodies are JSON, UTF-8. Send `Content-Type: application/json` with every body; any other type answers [`unsupported_media_type`](/errors/unsupported_media_type), and a body that does not parse answers [`invalid_json`](/errors/invalid_json).
- An operation that documents no request body (for example pausing a campaign) ignores a body sent with it: it is not read, so it is never refused as `415`, `413` or `invalid_json`.
- `HEAD` is accepted wherever `GET` is and answers the same status and headers with no body.
- Field names, query parameters and enum values are `snake_case`.
- **Request bodies are strict.** A field the operation does not accept is [`validation_error`](/errors/validation_error), never silently ignored, so a typo cannot go unnoticed. Unknown query parameters are refused the same way, and each query parameter may be given once.
- Request bodies are limited to 1 MB ([`payload_too_large`](/errors/payload_too_large)). Some fields have their own limits, documented in the API reference.
- `GET` requests have no body. Build JSON with a JSON library, not by concatenating strings.

`validation_error` lists every problem it found, not just the first:

```json
{
  "error": {
    "code": "validation_error",
    "message": "One or more fields are invalid.",
    "details": {
      "errors": [{ "field": "agent_id", "issue": "required", "message": "agent_id is required." }]
    },
    "request_id": "req_4k7m2qat5xvz3c6vqnwpbdfh",
    "retry": "no",
    "doc_url": "https://docs.relationsai.com/errors/validation_error"
  }
}
```

## Request headers

| Header | When |
| --- | --- |
| `Authorization: Bearer <key>` | Every request. See [Authentication](/fundamentals/authentication). |
| `Content-Type: application/json` | Every request with a body. |
| `Idempotency-Key` | Optional on `POST` and `DELETE`: makes a retry safe. See [Idempotency](/fundamentals/idempotency). |
| `If-Match` | Required on `PATCH` of campaigns and contacts. See [Concurrency](/fundamentals/concurrency). |
| `Range` | Optional, when downloading recording audio. |

## Objects

A single object is returned as itself, never wrapped. Every object has:

- `id`: an opaque, prefixed id such as `call_6Vb3Nm8Qw2Er5Ty9Ui4Op7`;
- `object`: its type, such as `call`, `contact` or `campaign`;
- `livemode`: `true` for live mode, `false` for test mode.

Fields are always present in responses: a value that is not known or not set is `null`, not missing. A delete returns a small confirmation object:

```json
{ "id": "ct_8Rw2Tq5Lm9Xc3Vb7Nz4Kp1", "object": "contact", "livemode": false, "deleted": true }
```

## Lists

Every list operation returns the same envelope:

```json
{
  "object": "list",
  "data": [{ "id": "call_9Tg5Yh2Uj8Ik4Ol7Pm3Nb6", "object": "call", "livemode": false }],
  "has_more": false,
  "next_cursor": null
}
```

`data` holds full objects (a call in a list is the same as from [`GET /calls/{call_id}`](/api/calls#retrieve-a-call)). See [Pagination](/fundamentals/pagination) and [Filtering](/fundamentals/filtering).

## Status codes

| Status | Meaning |
| --- | --- |
| `200 OK` | Success. |
| `201 Created` | A new object was created (for example a contact or a campaign). |
| `202 Accepted` | Accepted; the work continues after the response (placing a call, sending a test event, retrying a delivery). Follow the object's status. |
| `206 Partial Content` | A byte range of recording audio. |
| `400`, `413`, `415`, `422` | The request cannot be done as sent; fix it. |
| `401`, `403` | Authentication or permission problem. |
| `402` | Balance or spend limit. |
| `404` | No such object for this key, or no such route. |
| `405` | Method not supported on this path. |
| `409` | Conflict with the current state, or an idempotency conflict. |
| `410` | A recording link has expired. |
| `412`, `428` | `If-Match` stale or missing. |
| `429` | Rate limit, daily call limit or calling capacity. |
| `500`, `503` | A problem on our side. |

Every error has the same envelope and a stable `code`; the status of each code never changes. See [Error handling](/fundamentals/error-handling).

## Request ids

Every response, success or error, has an `X-Request-Id` header:

```http
X-Request-Id: req_4k7m2qat5xvz3c6vqnwpbdfh
```

Error bodies repeat it as `error.request_id` (a replayed answer, marked `Idempotent-Replayed: true`, can carry the original request's id in the body; the header is always this request's). The id is generated by RelationsAI for each request; a value you send in an `X-Request-Id` request header is ignored. Log it with your own correlation id for every request, and quote it when you write to [support](/resources/support): it is how we find your request.

## Response headers

| Header | On | Meaning |
| --- | --- | --- |
| `X-Request-Id` | Every response | The request id. |
| `RateLimit-Limit` | Every response | Requests allowed in the current one-minute window. |
| `RateLimit-Remaining` | Every response | Requests left in the window. |
| `RateLimit-Reset` | Every response | Seconds until the window resets. |
| `Retry-After` | `429`, `503` and errors whose `retry` is `after` | Seconds to wait before retrying. |
| `Idempotent-Replayed` | Replayed `POST` and `DELETE` responses | `true` when the response is the stored result of an earlier request with the same key. |
| `ETag` | Campaigns and contacts | The version to send as `If-Match`. |
| `Allow` | `405` | The methods the path supports. |

See [Rate limits](/fundamentals/rate-limits). Browser code (from origins the API allows) can read `X-Request-Id`, the `RateLimit-*` headers, `Retry-After`, `Idempotent-Replayed` and `ETag`; the other headers are for server-side clients.

## Asynchronous operations

Calls and campaigns run after the request returns. `POST /v1/calls` answers `202` with the call `queued`. Starting a campaign answers `200` when the run has started, or `202` when the start was sent and is still being confirmed. Follow progress with [webhooks](/fundamentals/webhooks) or by reading the object, and treat the object's own status as the truth.
