# Request & response

These conventions apply to every documented API key endpoint under `/api/public/v1`.

## Base URL

```
https://api.sair.foundation/api/public/v1
```

URL segments are `kebab-case`; JSON fields are `camelCase`.

## Envelope

Success:

```json
{
  "ok": true,
  "data": { "...": "..." }
}
```

Error:

```json
{
  "ok": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable English",
    "details": {}
  }
}
```

- The top-level value is always an object with `ok` plus either `data` or `error`.
- Write endpoints return the response documented by each endpoint. `DELETE` returns `204` with no body unless documented otherwise.
- `error.code` is a stable `UPPER_SNAKE_CASE` enum suitable for programmatic handling.
- `error.message` is human-readable English and may change between releases.
- `error.details` is endpoint-specific (e.g. `fieldErrors[]` for `422`).

Endpoints that return non-JSON content do not use this envelope — the Playground [run-events](./endpoints/playground.md#stream-events) SSE stream and the submission [text-body download](./endpoints/competitions.md#download-the-stored-text-body) return their bodies directly.

## HTTP status

| Status | When                                                                |
| :----: | :------------------------------------------------------------------ |
| `200`  | Successful read or update                                           |
| `201`  | Resource created                                                    |
| `204`  | `DELETE` succeeded                                                  |
| `400`  | Malformed request (bad JSON, unknown top-level field, type mismatch)|
| `401`  | Missing or invalid key                                              |
| `403`  | Scope or business rule denies the action                            |
| `404`  | Resource doesn't exist or isn't visible to you                      |
| `409`  | Conflict with current state (e.g. cancel an already-terminal run)   |
| `413`  | Payload too large                                                   |
| `422`  | Request schema valid but semantics invalid (e.g. unknown enum)      |
| `429`  | Rate limit hit                                                      |
| `5xx`  | Server error                                                        |

## Identifiers

Resource IDs are opaque strings. Format varies by resource type and may change between minor releases.

The only ID constructed by clients is the **competition ID**, a human-readable slug (e.g. `modular-arithmetic-challenge`). All other IDs are generated by the API and surfaced in responses.

## Time and naming

- Timestamps are ISO 8601 UTC with a trailing `Z`: `2026-05-18T12:34:56Z`.
- All numeric times are milliseconds when explicitly labelled (e.g. `elapsedMs`).
- Field names: `camelCase`. URL segments: `kebab-case`. Enum values are lowercase (multi-token use `-`, e.g. `model-reference`). Error codes use `UPPER_SNAKE_CASE`; see [Errors](./errors.md).

## Content type

Use `Content-Type: application/json` for requests that send a body. Text values are carried in JSON fields such as strings or arrays of strings.

## Unknown fields

Request bodies with unknown top-level fields return `400`. New optional fields may appear in responses; clients should ignore unrecognized fields.
