# Integration (API reference) Route: `/integration`. Component: `IntegrationPage.dc.html`, mounted by `AppFrame`. The API keys sub-page is in `screens/integration-api-keys.md`. Visual reference: `RenewCMS Screens Back-office.dc.html`, group "Integration". ## Navigation decision Sticky section index on the right (desktop), "Jump to" select (mobile). A reference is read in one scroll and searched with the browser, so tabs would hide content and break find-in-page and deep links; the index keeps one page with anchors and shows position. ## Page copy - Header: breadcrumb "Integration", title "API reference", description "Everything a developer needs to build on RenewCMS." - Intro: "RenewCMS is headless: it stores your content and your own code displays it. There are two ways in. Public read-only access lets a website read online articles. API keys let a script or service manage articles, media and users without the back-office." - Base URL: label "Base URL", a CodeSnippet with one tab ("Base URL") and the button "Copy" / "Copied" (2 s, announced). - Authentication, card "Public": "No key needed. Two read-only routes, and only online articles are returned." `GET /v1/article/`, `GET /v1/article/{id}`. - Card "API key": "Send `Authorization: Bearer rcms_…`. Requests act as the user who created the key, with that user's role (owner or admin). A key never has more rights than its creator." CodeSnippet tabs curl, fetch (GET /v1/auth/me). "Keys start with `rcms_` and are shown in full once, when you create them." Link "Manage API keys". - Note 1: "A key can't create, list or revoke API keys, log in, register, verify or reset a password, or call anything under `/v1/system`. Those routes answer 403 "This action needs a real session." An invalid, unknown, expired or revoked key answers 401, even on public routes, and so does a key whose user was deleted or unverified." - Note 2: "Each key can make 120 requests per minute. Beyond that the API answers 429 with a `Retry-After` header." - Groups: Articles (7), Media (6), Users and invites (6), Settings (1), Session only (12 routes, 3 sub-groups; every `/v1/system` route is in this group). Session-only banner: "These routes need a signed-in browser session. With an API key they answer 403 "This action needs a real session."" - Errors: "Every error is a plain-text body with a single sentence. The status code is in the HTTP status line, and the Content-Type is text/plain." Table 400, 401, 403, 404, 429, 500 with the real messages below. The 403 row has a note line under it: "Site SEO defaults are owner only. Article SEO fields need only the right to edit the article." (12/18, `--ui-text-muted`, in the cause column, 2 px under the cause text). It is the only row with a note; rows below it move down by the wrapped note height (two 18 px lines on desktop, three on mobile, plus 2 px). Sample: a status line plus the text body (`HTTP/1.1 403 Forbidden`, `Content-Type: text/plain`, blank line, `This action needs a real session.`). - Notes: image URL prefix, CORS (existing copy), and "Article routes accept `?format=markdown`, `?format=html` or `?format=json` to choose how `body` is returned. With `json`, `body` is the stored JSON structure. Articles have `body_format: markdown` by default." ## EndpointCard states - Collapsed (44 px row): method badge, mono path, one-line description, chips, chevron. Desktop is one line; mobile is three (method and path, description, chips). - Expanded: "Request body" table (Name, Type, Required, Rule; "No request body." when empty), tabs fetch / curl / Response in one CodeSnippet with Copy, then "Errors": status chip plus one-line caption (also its `title` tooltip). - Session only: dashed border, muted fill, lock icon, "Session only" chip, no method colour, no expand. - Chips: access "Public", "Verified user", "Owner only"; key "API key OK" (success soft) or "Session only" (dashed, muted). - Method badges: GET `--ui-bg-inverted`, POST success soft, PATCH warning soft, DELETE error soft. Existing tokens only. ## States Desktop 1440×900, mobile 390×844, light and dark. 1 Default · 2 Media group, POST /v1/media/ expanded with curl tab · 3a Base URL copied · 3b CodeSnippet copied · 4 Authentication · 5 Session only · 6 Errors · 7 Navigation (index active item Media; mobile select open). ## Error messages Plain text, one sentence. Used in the table and in the per-endpoint error captions. - 400: "The request cannot be processed due to a mismatch in the format of the body." / "The expiry must be a date in the future." Image requests (`/media/photo.png?v=`) answer 400 with a JSON body (`status`, `error`, `message`, `detail`) in two cases: `{ "status": 400, "error": "unknown_variant", "message": "Unknown variant", "detail": "unknown" }` for an unknown or disabled variant, and `{ "status": 400, "error": "unsupported_parameter", "message": "Unsupported parameter", "detail": "width" }` for any other query parameter (`width`, `height`, `format`, `quality`). Both are rows of the "Responses" table in "Images and variants". - 401: "You must be logged in to access this resource." - 403: "This action needs a real session." (session-only routes called with a key) / "Only the owner can do this." / "You must verify your account to access this resource." Note under the 403 row: "Site SEO defaults are owner only. Article SEO fields need only the right to edit the article." The inline parenthetical that said the same thing in the row's cause text was removed to avoid saying it twice. - 404: "Article not found" / "Image not found" / "Invite not found" / "API key not found" / `site_not_found` (GET /v1/site/{key}/sitemap only; body is the code, not a sentence) - 429: "Too many requests. Please try again later." A per-key limit also sends `Retry-After`. - 500: "Internal server error" - 409, only as examples under their endpoints: `POST /v1/user/invite` "This address already has an account."; `POST /v1/media/merge` "Some of the images to merge changed. Refresh and try again." There is no 409 or 422 row in the table. - 413 and 415 on `POST /v1/media/` keep their earlier plain captions; no real message is known. ## Request bodies - `POST /v1/article/`: `title` (string, required, 3 to 200 characters), `body` (string, optional, no length limit). - `PATCH /v1/article/{id}`: `body` (string, optional, no length limit). - `PATCH /v1/media/{id}` (rename): `filename` (string, required). - `POST /v1/media/merge`: `keep_id` (number, required, > 0), `merge_ids` (number[], required, at least 1, each > 0). - `POST /v1/media/`: multipart/form-data, 1 file field (shown as "multipart/form-data, 1 file field"). - `PATCH /v1/user/me`: `display_name` (string, required, max 60 characters). - `POST /v1/user/invite`: `email` (string, required, valid email). - `POST /v1/system/update/restart`: optional body `{ "kind": "update" | "restart" }`. ## Component inventory and Nuxt UI v4 - EndpointCard (extended): `UCollapsible` (trigger is a button with aria-expanded), `UBadge` (method, chips, status), `UTable` (body fields), CodeSnippet (fetch, curl, Response). CodeSnippet is the only code block in the product: Integration, Integration images, API keys (one-time key) all use it. Selected tab: 2 px `--ui-primary` underline. Syntax colouring was dropped so there is one rendering. - SectionIndex (new): custom `