Files
RenewCMS/docs/api.md
T

14 KiB

API reference

The full, searchable reference is the Integration page of the running app (/integration). It renders web/src/data/apiCatalogue.ts, which is the single source for this page. This file is for readers without the UI.

Base URL

The origin that serves the app, for example https://cms.example. Every path starts with /v1, for example https://cms.example/v1/article/.

Authentication

  • Public routes need nothing: GET /v1/article/ and GET /v1/article/{id}. They return online articles only.
  • Everything else needs a signed-in browser session (the jwt cookie) or an API key: Authorization: Bearer rcms_.... Create keys at /integration/api-keys. A key acts as its creator, with that user's role, and shows in full once.
  • A key cannot call POST /v1/auth/verify, DELETE /v1/auth/register, /v1/api-key/* or /v1/system/*. Those answer 403 "This action needs a real session." An invalid, unknown, expired or revoked key answers 401, even on public routes.
  • Each key can make 120 requests per minute. Beyond that: 429 with a Retry-After header.

Routes a key can call (24)

Group Routes
Articles GET /v1/article/, GET /v1/article/{id}, POST /v1/article/, PATCH /v1/article/{id}, PATCH /v1/article/{id}/publish, PATCH /v1/article/{id}/unpublish, DELETE /v1/article/{id}
Media GET /v1/media/, POST /v1/media/, PATCH /v1/media/{id}, DELETE /v1/media/{id}, POST /v1/media/merge, GET /v1/media/{id}/usage, GET /v1/media/purge-settings (owner or admin), PATCH /v1/media/purge-settings (owner or admin), GET /v1/media/purge-preview (owner or admin), POST /v1/media/purge (owner or admin), GET /v1/media/{id}/variants (owner or admin, session only), POST /v1/media/{id}/variants (owner or admin, session only), POST /v1/media/{id}/variants/{name} (owner or admin, session only), DELETE /v1/media/{id}/variants/{name} (owner or admin, session only)
Users and invites GET /v1/user/, PATCH /v1/user/me, POST /v1/user/invite (owner only), DELETE /v1/user/invite/{id} (owner only), POST /v1/user/invite/{id}/renew (owner only), GET /v1/auth/me
Settings GET /v1/settings

Activity log

The activity routes are session only: a key answers 403 "This action needs a real session." and a key never reads the audit trail. Events are append-only; there is no POST, PUT, PATCH or DELETE under /v1/activity.

Route Who Purpose
GET /v1/activity Any verified user Filtered, paged events. Owner and admin see every event; any other role is scoped to its own events, and actor set to someone else answers 403 "Only owners and admins can do this."
GET /v1/activity/summary Owner, admin Counts for the range, the previous period, active people and seven-bucket series.
GET /v1/activity/export Owner, admin The same filters as the list without page or size, as a text/csv; charset=utf-8 attachment (when,actor,action,target,outcome,source,ip). Cells starting with =, +, - or @ get a leading quote.

Query: q, actor (user:ID, key:ID, system), category (articles, media, users, api_keys, auth, system), outcome (all, success, failed), security (true), range (today, 7d, 30d, all, custom; default 7d), from and to (dates, with custom), page (from

  1. and size (25, 50 or 100). Any invalid value answers 400 with the body-mismatch message. Dates are UTC. The list answers { items, total, page, size }, newest first (created_at, then id).

GET /v1/settings also returns activity_retention_days (30, 90, 365 or 0 for forever, default 90), activity_retention_max_gb (0 for no size limit), activity_log_size_bytes, activity_last_cleanup_at and activity_last_cleanup_removed. PATCH /v1/settings with {"activity_retention_days": 30}, {"activity_retention_max_gb": 5} or both is session only and owner only (403 "Only the owner can do this."); any other value answers 400. The daily cleanup removes events past the age limit, then the oldest events until the log fits the size limit. Whichever limit is reached first wins.

What is recorded

Every real action is recorded by its use case, once, after the change was made (outcome success) or after the use case refused it (outcome failed, with a plain failure_reason). Nothing is recorded for a read (no GET, no list, no public article read, no status poll), for a request that fails validation, or for a target that does not exist (404). A failed audit write is logged and never changes the response or the change itself.

Category Actions
articles article.create, article.edit, article.publish, article.unpublish, article.delete
media media.upload, media.rename, media.delete, media.merge, media.purge, media.variant_create, media.variant_update, media.variant_delete, media.variant_enable, media.variant_disable, media.variant_purge, media.variant_generate_all, media.variant_purge_image, media.variant_purge_all, media.variant_purge_unused
users user.register, user.verify, user.update_profile, invite.create, invite.cancel, invite.renew, invite.accept
system system.media_purge (scheduled), system.update_checked, system.update_downloaded, system.restart_requested, system.activity_cleanup, system.activity_retention_changed

Target types: article, image, image_variant (id and name are the variant name), user, invite, api_key and release (id and name are the tag). Only the fields title, online, body, filename, display_name, width, height, fit, format and quality appear in changes, with the kinds text and body; a body edit carries a signed character count delta and never the text. A change that leaves a value as it was (same body, same publish state, same file name, same display name) records nothing. Params: media.merge and media.purge and system.media_purge carry merged and articles_updated, plus failed_groups when a group failed (still outcome success); media.variant_generate_all carries generated, failed and causes (a list of too_large, unsupported_format or failed) and is failed only when nothing was generated and something failed; media.variant_purge_image carries purged_files and purged_bytes; both have an image target and are recorded only when files were generated or removed; media.variant_purge_all and media.variant_purge_unused (no target) carry purged_files and purged_bytes, the latter also older_than_days; a partial purge adds failed_files and stays success, and a purge that removed nothing while some files failed is failed; nothing is recorded when no file was removed or failed; system.update_checked carries update_available; system.restart_requested carries kind and, for an update, tag (it has no target). system.update_downloaded is recorded when the job reaches ready or failed, with the origin captured at request time; a refusal at the start is recorded immediately. A failed registration or invite acceptance has an anonymous actor named by the attempted username.

failure_reason is one fixed sentence per domain error, matched with errors.Is, at most 255 characters, for example "The title must be between 3 and 200 characters.", "That file name is not valid.", "The merge request is not valid.", "A purge is already running.", "Registration is closed on this site.", "That invitation has expired.", "The release feed could not be reached." or "The update could not be downloaded.". Any other error becomes "Something went wrong on our side. Please try again."; the text of the error never reaches the event, the API or the CSV. Passwords, hashes, codes, article bodies, file bytes, storage paths and asset urls are never recorded.

Article routes accept ?format=markdown|html|json (default markdown). GET /v1/media/ accepts q, hash, cursor, filter, sort (newest, oldest, name, largest or total_size, the original plus its generated variants), variants (with or without generated variant files) and limit. Every item carries variant_count and variant_size (the index rows of the image and their bytes, 0 when none); counts stays library-wide. Any other variants or sort value is 400.

Image variants

Owners and admins manage named variants of the stored images. These routes are session only (no API key) and sit behind the verified-user check; any other role gets 403 "Only owners and admins can do this." Behaviour, caching and the public ?v=<name> contract are in image-variants.md.

Route Purpose
GET /v1/image-variants { "items": [...] }, ordered by name.
POST /v1/image-variants Create. 201 with the variant. 409 when the name exists.
PUT /v1/image-variants/{name} Replace the definition (the name in the body is ignored). 200 with the variant and purged_files.
PATCH /v1/image-variants/{name} { "enabled": true } or { "enabled": false }. Keeps the generated files.
DELETE /v1/image-variants/{name} 204. Removes the definition and its generated files.
POST /v1/image-variants/{name}/purge { "purged_files", "purged_bytes" }. Keeps the definition.
GET /v1/image-variants/cache Cache statistics, see below.
POST /v1/image-variants/cache/purge Purge generated files by scope, see below.

A variant is { name, width, height, fit, format, quality, generate_on_upload, enabled, generated_files, cache_size, created_at, updated_at }. width and height are 1 to 8000 or null (at least one is required), fit is cover, contain or inside (default cover), format is original, jpeg, png or webp (default original), quality is 1 to 100 (default 82, null for png), generate_on_upload defaults to false, enabled to true, and cache_size is in bytes. A PUT that omits generate_on_upload or enabled keeps the stored value.

Errors are plain text: 400 for a body that is not valid JSON, 404 "Variant not found", 409 "A variant named “thumb” already exists. Choose another name." and 422 with one sentence for the first invalid field (name, width and height, fit, format, quality).

Cache statistics and purge by scope

GET /v1/image-variants/cache answers { generated_files, disk_used, variants_defined, variants_enabled, variants_disabled, rejected_requests, rejected_window_days, unused: { files, bytes, days }, by_variant: [{ name, enabled, files, bytes }], top_images: [{ id, filename, url, files, bytes }] }. by_variant lists every defined variant by bytes descending then name; top_images lists the ten images using the most variant bytes (ties by id, images that no longer exist are skipped). unused counts the files of enabled variants whose reference time (the last public request, or the creation time when never requested) is older than 30 days. rejected_requests counts the public requests refused with 400 over today and the six previous UTC days. Pending request tracking is flushed before the read. Arrays are never null.

POST /v1/image-variants/cache/purge takes { "scope": "all" | "unused" | "variant" | "image", "variant": "<name>", "image_id": <id> } (variant for the scope variant, image_id for image; a target given for another scope is ignored) and answers { scope, purged_files, purged_bytes, failed_files }. A purge that fails on some files still answers 200 with failed_files above 0; the failed rows are kept so the same request can be sent again. Errors are plain text: 400 for a body that is not valid JSON, 422 "Scope must be all, unused, variant or image." or "A variant name or an image id is required for this scope.", 404 "Variant not found" or "Image not found", 500 when the rows cannot be listed.

Variants of one image

Owners and admins only, session only: a request with an API key is 401. All ids are image ids.

Route Purpose
GET /v1/media/{id}/variants { "unsupported": null, "items": [...] } for the enabled variants in name order. unsupported is "svg" (with items: []) for an SVG.
POST /v1/media/{id}/variants Generates every enabled variant that is missing. { "generated", "failed", "failures": [{ "name", "cause" }], "items": [...] }, 200 even when some fail.
POST /v1/media/{id}/variants/{name} Generates one enabled variant. 200 with the item.
DELETE /v1/media/{id}/variants/{name} Removes the generated file. { "purged_files", "purged_bytes" }, also for a disabled variant.

An item is { name, width, height, fit, format, output_width, output_height, output_format, generated, size, url }: width and height are the settings (null when auto), output_* the size and format the file has or will have (never above the source), generated is true only when the file exists with the variant's current settings, size is its bytes or null, and url is /static/uploadedImages/<stored name>?v=<name>. A cause is too_large, unsupported_format or failed. A file generated here is the same file the public url serves, and the other way round. Uploads generate the variants flagged generate_on_upload (enabled ones, raster sources only) before answering; a failure there is logged and never fails the upload.

Errors are plain text: 400 for a malformed id, 404 "Image not found" or "Variant not found" (also for a disabled variant on POST), 413 "The source image is too large to generate this variant.", 415 "The source image can't be read, so this variant can't be generated.", 422 "Variants aren't generated for this format." and 500 "Internal server error".

Errors

Every error is a plain-text body with one sentence; the status is in the HTTP status line. The statuses are 400, 401, 403, 404, 409, 429 and 500. Messages are listed on the Integration page.

Notes

  • Images in body use paths like /static/uploadedImages/<uuid>.png. Prefix them with the base URL.
  • Browsers can call the API only from origins in the server's CORS setting.