Management API
The management API under /api/v1 is how the dashboard, the CLI and your own scripts write content, publish it and run a project.
Why a script can do what the dashboard does
The dashboard has no private back door. Every save, publish and setting you see there is a request to this API, and so is every command the CLI runs. So anything a person does in the dashboard, a script can do too, with the same validation and the same refusals. The routes, their parameters and the shape of every answer are written down in one contract, /openapi.json. This site serves it as an exact copy of the CMS's own. Generate a client from it, or keep it open beside this page.
What the routes cover and how they refuse
The surfaces
Every path below sits under /api/v1/projects/<project>, and <env> stands for an environment's key. Authentication lists which credential each route takes.
| Surface | Routes |
|---|---|
| Documents | envs/<env>/documents, envs/<env>/documents/<key> (GET, PUT, DELETE), envs/<env>/references/<key> |
| Versions | envs/<env>/versions/<key>, newest first, bodies only with ?bodies=1 |
| Publishing | envs/<env>/publish, envs/<env>/unpublish, envs/<env>/publish-impact/<key> |
| Schemas | documents under _schemas/, plus envs/<env>/schemas, envs/<env>/schemas/<name>/validate, envs/<env>/types |
| Translations | envs/<env>/translations/<key>, envs/<env>/translations/<key>/review, envs/<env>/work-queue |
| Assets | assets (upload, list), assets/<id>, assets/<id>/references; folders for the media library |
| Environments | environments, environments/<env>, and envs/<env>/preview-tokens |
| Promotion | envs/<env>/compare, envs/<env>/promote, envs/<env>/promotions |
| Sync | envs/<env>/sync/diff and envs/<env>/sync/bulk, what quillet push and pull use |
| Import | import, the staged restore quillet restore drives |
| Keys | keys |
| Webhooks | webhooks and its deliveries, on Webhooks |
| Search | envs/<env>/search/reindex |
Requests and answers are JSON. A list that can grow is paged. Send limit, then pass the nextCursor of one answer as cursor to get the next page, and stop when it is null.
Saving is not publishing
PUT …/documents/<key> appends a version and moves the draft. It never changes what delivery serves; only publish does that. If you send a body equal to the current one, nothing happens, and no empty version is created. Every document answer carries version and contentHash, the hash of the current version's content.
POST …/publish takes up to 200 documents and makes them live in one transaction, at one instant. publish-impact/<key> tells you what a publish would change, without doing it. It answers with the version that would go live, the live pages that include the document, and the problems a publish would refuse with.
Two writers at once
To make a PUT conditional on the version you read, send If-Match: W/"<contentHash>" with it. If someone has saved since you read, the answer is 409 conflict and nothing is written. A header the API could not have issued, such as *, gets a 422 instead. Without the header, the save overwrites whatever is there, and that is what quillet content put does when you leave out --if-match. A CLI push is conditional in its own way. It sends each document's baseHash, and it refuses on a conflict unless you pass --force. A publish entry may carry version in the same spirit. If the draft has moved past that version, the whole batch is refused with a 409 that names both versions.
Refusals
| Status | error |
Meaning |
|---|---|---|
| 401 | unauthorized |
No credential, or one the API does not recognise. |
| 403 | forbidden |
The credential cannot do this; message says why. |
| 404 | not_found |
No such thing, or none this credential may know about. |
| 409 | conflict, in_use |
A lost race (read again and retry), or something live still refers to what you changed; usedBy names it. |
| 413 | payload_too_large |
The body is larger than the server accepts. Split the request. |
| 422 | validation_failed |
fields holds one sentence per offending input, keyed by its dotted path, such as documents.3.content.title. _form is about the body as a whole. |
| 429 | rate_limited |
The caller spent its budget for the current window. |
A 422 is always safe to show, because every value in fields is a sentence written for the person who sent the request.
Rate limits
The management API has no general request budget. Sign-in and uploads each have a budget you can configure, and so do compare, promote and clone; Configuration lists them. Three budgets are fixed. Preview tokens are limited to thirty a minute per credential, webhook test events to ten a minute per webhook, and a search reindex to five an hour per environment.
How to
API
Save a draft, using a CLI key that holds content:write:
curl -s -u '<keyId>:<secret>' -X PUT \
-H 'Content-Type: application/json' \
-d '{"type":"md","title":"Introduction","content":"Welcome.","message":"First draft"}' \
'http://localhost:3000/api/v1/projects/<project>/envs/<env>/documents/<key>'The answer is the document, with its version and contentHash. Now save again, on condition that nobody else has saved in between, by sending the hash you were given:
curl -s -u '<keyId>:<secret>' -X PUT \
-H 'Content-Type: application/json' \
-H 'If-Match: W/"<contentHash>"' \
-d '{"type":"md","title":"Introduction","content":"Welcome, again."}' \
'http://localhost:3000/api/v1/projects/<project>/envs/<env>/documents/<key>'Run the same request a second time. The hash is no longer the current one, so the answer is {"error":"conflict",…} with status 409, and the document stays as it was.
Publish the document, using a key that holds content:publish:
curl -s -u '<keyId>:<secret>' -X POST \
-H 'Content-Type: application/json' \
-d '{"documents":[{"key":"<key>"}]}' \
'http://localhost:3000/api/v1/projects/<project>/envs/<env>/publish'Each entry in results says "status":"published", with the version that is now live. Then read the document's history:
curl -s -u '<keyId>:<secret>' \
'http://localhost:3000/api/v1/projects/<project>/envs/<env>/versions/<key>?limit=5'CLI
The CLI sends the same requests. Sign in as Install and sign in shows, with a .quilletrc that names the project and the environment. Then put a file as a draft:
npx quillet content put <key> --file <file.md> --message "First draft"Publish it. At a terminal, publish shows what else changes and asks before it goes ahead. --yes answers for you:
npx quillet publish <key> --yes--verbose prints each request's method, path and status, so you can see which route sits behind a command.
Limits
If-Matchcovers a document's content only. If somebody else changes the title, the tags or the schema binding, your save is not a conflict.- There is no merging. A 409 is a decision for a person to make: read again, compare, and save.
- One publish takes at most 200 documents, and one promotion at most 500 entries.
- There is no scheduled publishing, no approval step and no named release.
JSON_BODY_LIMITcaps a request body, andASSET_MAX_BYTEScaps an upload. Both are on Configuration.- Keys, webhooks, members, changes to project settings, and creating, editing or deleting environments refuse every API key, whatever its scopes. Reading the project and its environments works with any CLI key of the project.
- The API has one version,
v1, and no GraphQL endpoint.
Related
- Authentication: which credential each route accepts.
- Delivery API: reading what you published.
- Webhooks: being told when it changed.
- Versions and publishing: why a save and a publish are two acts.
- When two people edit: the conflict as the dashboard shows it.
- Command reference: the CLI commands over these routes.