> For the complete documentation index, see [llms.txt](https://docs.khotan.com/llms.txt). A markdown version of any page is available at its `index.md` URL.

# Conventions

Rules that hold across every `/api/v1` operation, so you can write one client rather than one per resource.

## Versioning

The version lives in the path. `/api/v1` is the only version, and operations are added to it rather than replacing it. A breaking change to an existing operation would arrive as a new version path, not as a silent reshape — so a working client keeps working.

## Errors

Every failure uses one envelope, so a client can branch on `error.code` without special-casing routes:

```json
{
  "error": {
    "code": "not_found",
    "message": "App not found.",
    "details": {}
  }
}
```

Status codes carry consistent meaning:

| Status | Meaning |
|---|---|
| `400` | The request failed validation. `message` names the first problem. |
| `401` | The key is missing, malformed, expired, or revoked. |
| `403` | The key is valid but its organization or role cannot perform this operation. |
| `404` | The resource does not exist **within the key's organization**. |
| `429` | A rate limit was exceeded. |
| `502` | An upstream provider failed. Usually retryable. |
| `500` | An unexpected failure. |

A resource belonging to another organization returns `404`, not `403`. The API does not confirm that a resource exists outside your scope.

## Request identifiers

Every response carries a request identifier header. Log it — quoting it is the fastest way to have a specific request investigated.

## Rate limits

Limits are per route and, for authenticated requests, per key — so one noisy integration cannot exhaust another's budget. Read operations are generous; provisioning and credential operations are tight and fail closed.

When a route is rate-limited, successful responses and `429` responses include:

- `RateLimit-Limit` — the route's configured request quota for the window
- `RateLimit-Remaining` — requests left in the current window after this response
- `RateLimit-Reset` — whole seconds until capacity may free in the sliding window

A `429` also carries a `Retry-After` header in seconds. Honour it. Retry with exponential backoff and jitter rather than a fixed interval; several clients retrying on the same schedule reproduce the burst that caused the limit.

File upload prepare (`POST /files/uploads`) and complete (`POST /files/uploads/complete`) each allow **120** requests per minute per key. The counters are independent. Each HTTP request counts as one toward that route's limit, regardless of how many files the body lists.

Sending feedback (`POST /feedback`) allows **10** requests per minute per key, and fails closed.

## Retries and idempotency

Retry `429`, `502`, and `500`. Do not blindly retry `400`, `401`, `403`, or `404` — those describe the request, and repeating it produces the same answer.

**Read operations are safe to retry.** Write operations are not uniformly idempotent, and the ones that are not say so in their description. Provisioning operations are the ones to watch: creating a database responds only once provisioning has settled, and retrying a request that already succeeded provisions a second database rather than returning the first. Where an operation accepts an idempotency key, supply one.

If a write times out, prefer reading the collection to discover whether it took effect over resending it.

## Long-running operations

Provisioning runs inside the request and can take tens of seconds. These operations respond `201` with the persisted record whether provisioning ended ready or failed — a `201` means the record exists, not that the resource is usable. **Branch on the returned `status`** (`creating`, `ready`, `failed`) rather than on the HTTP status, and read `failureState` when it is `failed`.

Set your client's timeout accordingly; a 10-second default will abort work that was going to succeed.

## Pagination

Collection endpoints that paginate document their parameters on their own reference page and return a cursor for the next page. Follow the returned cursor rather than constructing offsets. Endpoints without pagination parameters return the organization's full set, which is bounded by what a workspace can hold.

## Engines, not vendors

The API names engines and capabilities: `databases` for PostgreSQL, `sqlite-databases` for SQLite, `redis-databases` for Redis, `buckets` for object storage. Which provider runs a given resource is a fact about that resource, not part of the interface, so it is reported in the `provider` field. This is enforced, so paths and operation names stay stable if a provider changes underneath.

## Server-to-server only

The API sends no cross-origin resource sharing headers and does not answer preflight requests. Browser JavaScript cannot call it, and that is deliberate: an organization-scoped key grants full access to a workspace, so it must never be shipped to client-side code where any visitor can read it.

Call the API from your server, and let your own frontend talk to your server.
