# HTTP API

> Every route a GoLocomotive app serves, with its body and its answer; PocketBase's wire shape throughout.

An app speaks PocketBase's REST wire. Any PocketBase client works against it.

## Conventions

- **Auth**: `Authorization: <token>`. A `Bearer ` prefix is accepted. An invalid or expired token makes the caller a guest, never an error.
- **Errors**: every error is `{ status, message, data }`. `data` is `{ <field>: { code, message } }` for a validation error, else `{}`.
- **Unknown failures**: a throw that is not an `ApiError` answers 400 `Something went wrong while processing your request.` and is logged.
- **Dates**: UTC text, `YYYY-MM-DD HH:MM:SS.sssZ`. Ids: 15 characters `[a-z0-9]`.

## Health

| route | answer |
|---|---|
| `GET /api/health` | `{ code: 200, message: 'API is healthy.', data: {} }`. A superuser also gets `data: { canBackup, realIP, possibleProxyHeader }` |

## Records

| route | body or query | answer |
|---|---|---|
| `GET /api/collections/:c/records` | `page perPage sort filter expand fields skipTotal` | `{ page, perPage, totalItems, totalPages, items }`; totals are `-1` under `skipTotal` |
| `GET /api/collections/:c/records/:id` | `expand fields` | the record |
| `POST /api/collections/:c/records` | JSON, or multipart with files (the JSON part in `@jsonPayload`) | the record |
| `PATCH /api/collections/:c/records/:id` | the same | the record |
| `DELETE /api/collections/:c/records/:id` | | 204 |
| `POST /api/batch` | `{ requests: [{ method, url, body }] }`, at most 50 | `[{ status, body }]` in one transaction. A failure rolls back all: 400 `Batch transaction failed.` with `data.requests.<i>` |

- `perPage` defaults to 30 and stops at 1000.
- `sort`: a comma list of fields, `-` for descending, `@random`, `@rowid`.
- `expand`: a comma list of relation fields, one level deep. The target's `viewRule` applies.
- `fields`: a comma list, `*`, or `expand.x.y` paths.
- A record carries `collectionId` and `collectionName`. Hidden fields show to superusers only. `email` shows to its owner, to a superuser, or when `emailVisibility` is true.
- `field+` and `field-` keys add and remove values on file, select, relation, and add and subtract on number.

## Status codes of the rules

| case | status |
|---|---|
| the action's rule is `null`, the caller is not a superuser | 403 `Only superusers can perform this action.` |
| list, a record fails the rule | left out of the page |
| view, update, delete, the record fails the rule | 404 |
| create, the new record fails the rule | 400 `Failed to create record.` |
| a field fails validation | 400 with `data.<field>` |
| a unique index is violated | 400, `validation_not_unique` on its fields |

## Auth

All under `/api/collections/:c/` for an auth collection `:c`.

| route | body | answer |
|---|---|---|
| `POST auth-with-password` | `{ identity, password, identityField? }`, `mfaId` in the query or the body | `{ token, record }`, or 401 `{ mfaId }` when a second factor is due |
| `POST request-otp` | `{ email }` | `{ otpId }`, also for an unknown email |
| `POST auth-with-otp` | `{ otpId, password, mfaId? }` (the code is `password`) | `{ token, record }`, or 401 `{ mfaId }` |
| `POST auth-refresh` | the auth header of that collection | `{ token, record }` |
| `POST request-password-reset` | `{ email }` | 204, also for an unknown email. One mail per 2 minutes |
| `POST confirm-password-reset` | `{ token, password, passwordConfirm }` | 204; every token of the record dies |
| `POST request-email-change` | `{ newEmail }`, signed in to `:c` | 204; mails a link to the new address |
| `POST confirm-email-change` | `{ token, password }` | 204; sets `email` and `verified`, every token of the record dies |

- Superusers sign in on `/api/collections/_superusers/auth-with-password`.
- Sign ins are limited to 5 per 3 s per client IP: 429 `Too Many Requests.` Code attempts: 5 per 180 s per account.
- `authRule` failing after the factors: 403.

## Files

| route | answer |
|---|---|
| `POST /api/files/token` | `{ token }`, valid 180 s. Any signed in caller |
| `GET /api/files/:c/:id/:filename` | the bytes, under a sandboxing CSP. `?token=` for a protected field, `?download=1` for an attachment |

## Collections

Superuser only. Read only: `schema.ts` owns the collections.

| route | answer |
|---|---|
| `GET /api/collections` | a page of collections; `page perPage sort filter skipTotal` over `id created updated name system type` |
| `GET /api/collections/:c` | one collection in PocketBase's JSON: rules, fields, indexes, its auth options flat (every token secret blank), its `viewQuery` |

## Backups

Superuser only.

| route | body | answer |
|---|---|---|
| `GET /api/backups` | | `[{ key, size, modified }]` |
| `POST /api/backups` | `{ name? }`, `^[a-z0-9_-]+\.zip$`, unique | 204 |
| `POST /api/backups/upload` | multipart `file`, a zip, streamed, no size limit | 204 |
| `GET /api/backups/:key` | `?token=` a superuser's file token | the zip as an attachment |
| `DELETE /api/backups/:key` | | 204; 400 while that backup is being made or restored |
| `POST /api/backups/:key/restore` | | 204, then the restore and a restart |

A second backup or restore while one runs: 400 `Try again later - another backup/restore process has already been started`.

## App system routes

| route | body | answer |
|---|---|---|
| `GET /api/_/changelog` | `since` (optional) | `{ version, seen, releases }`. Signed in |
| `POST /api/_/seen` | `{ version? }` | `{ version, seen }`. Signed in |
| `GET /api/_/logo` | | the logo of `loco.config.ts` |
| `POST /api/_/users/copy-in` | `{ collection?, records: [{ email, passwordHash, name?, role?, verified?, disabled? }] }` | `{ created, updated, items }`. Superuser only: upserts accounts by email with their bcrypt hash, used to copy users from one copy of an app to another |

## MCP and OAuth

Only with `createApp({ mcp })`. The details: [MCP protocol and OAuth](https://golocomotive.fr/docs/reference/mcp-protocol.md).

| route | what |
|---|---|
| `POST /mcp` | one JSON-RPC 2.0 message, `Authorization: Bearer`. `GET` and `DELETE` answer 405 |
| `GET /.well-known/oauth-protected-resource[/mcp]` | RFC 9728 metadata |
| `GET /.well-known/oauth-authorization-server` | RFC 8414 metadata |
| `POST /oauth/register` | RFC 7591 dynamic client registration |
| `GET /oauth/authorize`, `POST /oauth/authorize` | the consent page and its decision |
| `POST /oauth/token` | the code and refresh grants, PKCE S256 |

## Body limits

| route | limit |
|---|---|
| every route | 32 MB |
| record create and update | 32 MB plus each file field's `maxSize` (5 MB when unset) times its `maxSelect` |
| `/api/batch` | 128 MB |
| backup upload | none |

Over the limit: 413 `Request entity too large`, before the handler runs. A custom route sets its own with `bodyLimit(bytes)`.

## Custom routes

An app adds its own routes with `app.route(method, path, handler, ...middlewares)`. They answer whatever the handler returns. See [Server: hooks, routes, crons](https://golocomotive.fr/docs/developers/server.md#custom-routes).
