Conventions
What every route of the API has in common.
Base URL and versions
Every endpoint starts with /api, which keeps the API apart from the web
app's pages and static files on the same origin. Application routes are
versioned under /api/v1; GET /api/health pings the database, reports
the release the binary was built from, and answers 503 when the database
cannot be reached. An unknown path under /api answers a JSON
404. Everything outside /api belongs to the web app when the server ships
with it.
Authentication
Routes marked as protected need a signed-in user, either by the session cookie or by a bearer token:
Authorization: Bearer <access_token>The web app and the extension both use the bearer token. See Authentication.
Response envelope
Every /api/v1 route outside /api/v1/auth answers with the same envelope. metadata
and message are left out when empty.
{
"data": {},
"metadata": { "total": 42, "offset": 0, "limit": 10 },
"message": "data has been retrieved"
}Routes under /api/v1/auth are served by Authula and are not enveloped: they
answer with their own shapes.
Errors
An error is a status code and a message:
{ "message": "viewers cannot change the item" }A request that fails validation answers 422 with the fields at fault:
{
"message": "validation failed",
"errors": [{ "field": "encrypted_data", "message": "…" }]
}| Status | Meaning |
|---|---|
400 | The request is malformed |
401 | Not signed in, or the token has expired |
403 | Signed in, but not allowed |
404 | Not found. An item you are not a member of answers 404, as if it did not exist |
409 | Conflict: a stale revision, or something that already exists |
422 | Validation failed |
429 | Rate limit reached |
500 | Unexpected error. It is logged; the response never carries details |
Pagination
List routes take offset and limit. limit defaults to 10 and is capped at
100; the total is in metadata.total.
Binary values
Ciphertext and keys are bytes. They travel as standard base64 strings in JSON, in both directions.
Rate limits
Limits are per client IP, per minute:
| Routes | Requests per minute |
|---|---|
/api/v1/auth/* | 30 |
everything else under /api/v1 | 120 |
In development, loopback is exempt. With REDIS_URL set every instance of
the API counts against the same budget; without it the counters are kept in
memory and each instance has its own.
Idempotency
A write under /api/v1 that needs a signed-in user may carry a key the
client makes up, and is then run only once:
Idempotency-Key: 0b6e2c1a-7d0f-4f0e-9a51-3f6c2a9d1e77Sent again within 30 minutes, the same request gets the first answer back,
with Idempotent-Replayed: true, instead of being run again; two sent at the
same moment are taken one after the other. A client can therefore repeat a
POST whose answer it never received without making a second item.
- The key is 16 to 128 letters, digits,
-or_; a UUID fits. Anything else is answered400. - A key repeats only the request it first came with: the same user, method, address and body. With any of those different it is a new request.
- Only an answer without an error is kept. A request that failed is run again when it is sent again.
- Without the header, and for
GET, nothing changes.
The web app and the extension send a key with every write. With REDIS_URL
set the answers, and the lock that keeps two copies of a request apart, are
shared by every replica. Without it they are kept in the server's memory, so
a repeat that reaches another replica is run again.
CORS
Browsers on another origin may call the API only when their origin is listed
in CORS_ALLOWED_ORIGINS. Extension origins (chrome-extension://,
moz-extension://) are accepted without being listed.