openapi: 3.0.3 info: title: Grav CMS API description: | RESTful API for managing Grav CMS content, media, configuration, users, and system operations. ## Authentication The API supports three authentication methods. All three provide the same level of access — the authenticated user's permissions apply regardless of method. Every request (except `/auth/*` endpoints) must include one of: | Method | Header / mechanism | Best for | |---|---|---| | **API key** | `X-API-Key: grav_...` | CLI tools, MCP servers, server-to-server, automation | | **JWT token** (recommended) | `X-API-Token: ` | SPAs, mobile apps, admin-next plugin bundles | | **JWT Bearer token** (alt) | `Authorization: Bearer ` | Standard HTTP clients on hosts that don't strip the header | | **Session cookie** | `grav-site` cookie | Admin panel extensions | > Prefer `X-API-Token` over `Authorization: Bearer` when portability matters: > PHP under FastCGI / CGI / PHP-FPM (notably MAMP's `mod_fastcgi`) silently > strips `Authorization` before it reaches the application. A custom `X-*` > header bypasses this entirely. The server accepts either transport for the > same JWT. **API keys** are long-lived credentials that don't expire by default. Generate them via `bin/plugin api keys:generate` or the admin panel. Best for any server-side or automated integration. **JWT tokens** are short-lived (1 hour default). Obtain them by posting credentials to `/auth/token`, then refresh before expiry via `/auth/refresh`. The refresh flow uses token rotation — each refresh revokes the previous refresh token. Best for client-side apps where long-lived secrets shouldn't be stored. ## Response Envelope All successful responses use a standard JSON envelope: ```json { "data": { ... }, "meta": { ... }, "links": { ... } } ``` `meta` and `links` are present only on paginated responses. ## Error Format (RFC 7807) Error responses use `application/problem+json`: ```json { "status": 422, "title": "Unprocessable Entity", "detail": "Missing required fields: title", "errors": [ { "field": "title", "message": "The 'title' field is required." } ] } ``` ## Environments Send the optional `X-Grav-Environment` header to target a specific Grav environment. If omitted, defaults to the auto-detected environment (from hostname). Use `GET /system/environments` to discover available environments. ## Rate Limiting When rate limiting is enabled, every response includes: - `X-RateLimit-Limit` -- maximum requests per window - `X-RateLimit-Remaining` -- requests remaining - `X-RateLimit-Reset` -- UTC epoch when the window resets ## Optimistic Concurrency (ETags) Single-resource GET responses include an `ETag` header. For PATCH requests, send the same value in `If-Match` to detect mid-air collisions. A `409 Conflict` response is returned when the resource has changed since the last GET. version: 1.0.0-beta.1 license: name: MIT url: https://opensource.org/licenses/MIT contact: name: Team Grav url: https://getgrav.org email: devs@getgrav.org servers: - url: "{baseUrl}/api/v1" description: Default Grav CMS API server variables: baseUrl: default: "" description: > The base URL of the Grav installation. Leave empty when using relative paths, or set to a full origin such as https://example.com. tags: - name: Auth description: JWT token management -- obtain, refresh, and revoke tokens. - name: Pages description: Create, read, update, delete, move, and copy Grav pages. - name: Media description: Manage media files attached to pages or the site-level images folder. - name: Config description: Read and update Grav configuration (system, site, plugins, themes). - name: Users description: User account and API key management. - name: System description: System information, cache management, logs, and backups. - name: Languages description: Site language configuration and page translations. - name: GPM description: Package management -- install, remove, update plugins, themes, and Grav core. - name: Scheduler description: Job scheduling, status, and execution history. - name: Sidebar description: Admin sidebar navigation items contributed by plugins. - name: Dashboard description: Notifications, news feed, and statistics. - name: Webhooks description: Outbound webhook configuration and delivery management. - name: Blueprints description: Form schema definitions for pages, plugins, themes, and system configuration. Blueprints define the fields, types, validation, and layout used to render editing interfaces. - name: Data description: Resolve blueprint data-options@ directives and other dynamic data lookups. - name: Thumbnails description: Serve cached image thumbnails. security: - apiToken: [] - apiKey: [] - bearerAuth: [] - sessionAuth: [] paths: # --------------------------------------------------------------------------- # Auth # --------------------------------------------------------------------------- /auth/token: post: operationId: createToken tags: [Auth] summary: Obtain a JWT token pair description: | Authenticate with username and password to receive an access token and a refresh token. The access token is short-lived; use the refresh token to obtain a new pair without re-authenticating. security: [] requestBody: required: true content: application/json: schema: type: object required: [username, password] properties: username: type: string example: admin password: type: string format: password example: Pa$$w0rd! example: username: admin password: Pa$$w0rd! responses: "200": description: Token pair returned successfully. content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/TokenResponse" example: data: access_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... refresh_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... token_type: Bearer expires_in: 3600 "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /auth/refresh: post: operationId: refreshToken tags: [Auth] summary: Refresh a JWT token pair description: | Exchange a valid refresh token for a new access/refresh pair. The provided refresh token is automatically revoked (token rotation). security: [] requestBody: required: true content: application/json: schema: type: object required: [refresh_token] properties: refresh_token: type: string description: The refresh token obtained from `/auth/token` or a previous refresh. example: refresh_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... responses: "200": description: New token pair returned successfully. content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/TokenResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /auth/revoke: post: operationId: revokeToken tags: [Auth] summary: Revoke a refresh token description: | Explicitly revoke a refresh token so it can no longer be used. This is the recommended way to implement logout on the client side. security: [] requestBody: required: true content: application/json: schema: type: object required: [refresh_token] properties: refresh_token: type: string example: refresh_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... responses: "204": description: Token revoked successfully. No content returned. "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # Ping # --------------------------------------------------------------------------- /ping: get: operationId: ping tags: [System] summary: Health check / keep-alive description: | Simple health check endpoint. Returns a static response to confirm the API is reachable and the caller is authenticated. responses: "200": description: API is reachable. content: application/json: schema: type: object properties: data: type: object properties: pong: type: boolean example: true example: data: pong: true "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # Pages # --------------------------------------------------------------------------- /pages: get: operationId: listPages tags: [Pages] summary: List pages description: | Retrieve a paginated list of all pages. Supports filtering by published state, template, routable/visible flags, and parent route. Results can be sorted by date, title, slug, modified time, or order. The listing uses a lightweight serialization that excludes content, media, and children for performance. parameters: - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" - $ref: "#/components/parameters/lang" - name: sort in: query description: | Field to sort by. When omitted the default sort is `date` descending. schema: type: string enum: [date, title, slug, modified, order] - name: order in: query description: Sort direction. schema: type: string enum: [asc, desc] default: asc - name: published in: query description: Filter by published state. schema: type: boolean - name: template in: query description: Filter by page template name (e.g. `blog`, `default`). schema: type: string example: blog - name: routable in: query description: Filter by routable flag. schema: type: boolean - name: visible in: query description: Filter by visible flag. schema: type: boolean - name: parent in: query description: | Filter pages whose route starts with this parent path (e.g. `/blog` returns `/blog/post-1`, `/blog/post-2`, etc.). Returns all descendants. schema: type: string - name: children_of in: query description: | Return only **direct children** of the given route (one level deep). Unlike `parent`, this does not return deeper descendants. Use `children_of=/` for root-level pages. Ideal for lazy-loading tree views and miller column interfaces. schema: type: string example: /blog - name: root in: query description: Filter to top-level (root) pages only. schema: type: boolean example: /blog responses: "200": description: Paginated list of pages. content: application/json: schema: allOf: - $ref: "#/components/schemas/PaginatedEnvelope" - type: object properties: data: type: array items: $ref: "#/components/schemas/PageSummary" example: data: - route: /blog/my-first-post slug: my-first-post title: My First Post template: blog header: title: My First Post taxonomy: category: [blog] tag: [intro, grav] taxonomy: category: [blog] tag: [intro, grav] published: true visible: true routable: true date: "2026-03-20T12:00:00+00:00" modified: "2026-03-22T09:30:00+00:00" order: 1 has_children: true meta: pagination: page: 1 per_page: 20 total: 42 total_pages: 3 links: self: "/api/v1/pages?page=1&per_page=20" next: "/api/v1/pages?page=2&per_page=20" last: "/api/v1/pages?page=3&per_page=20" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" post: operationId: createPage tags: [Pages] summary: Create a new page description: | Create a new page at the given route. The parent page must already exist (except when creating top-level pages). Supply an optional `order` to create an ordered (visible) page, or pass `"auto"` to let the server pick the next free prefix based on existing siblings. Include `lang` in the body to create the page in a specific language. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreatePageRequest" example: route: /blog/my-new-post title: My New Post template: blog content: | # Hello World This is the body of my new blog post. header: taxonomy: category: [blog] tag: [new, grav] order: 3 responses: "201": description: Page created successfully. headers: Location: description: URL of the newly created page resource. schema: type: string example: /api/v1/pages/blog/my-new-post content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/Page" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /pages/{route}: get: operationId: getPage tags: [Pages] summary: Get a single page description: | Retrieve a single page by its route. Returns the full page object including raw Markdown content and media. Optionally render the content to HTML and/or include child pages. The response includes an `ETag` header for use with optimistic concurrency on subsequent PATCH requests. parameters: - $ref: "#/components/parameters/pageRoute" - $ref: "#/components/parameters/lang" - name: render in: query description: When `true`, include the rendered HTML in `content_html`. schema: type: boolean default: false - name: children in: query description: When `true`, include child pages in the response. schema: type: boolean default: false - name: children_depth in: query description: Maximum depth for recursive child inclusion. Only effective when `children=true`. schema: type: integer minimum: 1 default: 1 - name: translations in: query description: | When `true`, include `translated_languages` and `untranslated_languages` arrays in the response. schema: type: boolean default: false responses: "200": description: Page returned successfully. headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/Page" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" patch: operationId: updatePage tags: [Pages] summary: Partially update a page description: | Apply a partial update to an existing page. Only the fields included in the request body are modified; all others are left unchanged. **Optimistic concurrency:** If the `If-Match` header is provided, it is compared against the current ETag. A `409 Conflict` is returned if the resource has been modified since the client last fetched it. parameters: - $ref: "#/components/parameters/pageRoute" - $ref: "#/components/parameters/lang" - $ref: "#/components/parameters/ifMatch" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdatePageRequest" example: title: Updated Post Title content: | # Updated Content The body has been revised. header: taxonomy: tag: [updated] published: true responses: "200": description: Page updated successfully. headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/Page" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" delete: operationId: deletePage tags: [Pages] summary: Delete a page description: | Delete a page and its directory from disk. By default, child pages are deleted as well. Set `children=false` to prevent deletion of a page that has children (a 422 error will be returned instead). parameters: - $ref: "#/components/parameters/pageRoute" - $ref: "#/components/parameters/lang" - name: children in: query description: | Whether to also delete child pages. Set to `false` to prevent accidental deletion of pages with children. schema: type: boolean default: true responses: "204": description: Page deleted successfully. No content returned. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /pages/{route}/move: post: operationId: movePage tags: [Pages] summary: Move a page to a new location description: | Move an existing page to a different parent and/or change its slug or ordering prefix. The destination parent must exist and the target path must not already be occupied. parameters: - $ref: "#/components/parameters/pageRoute" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MovePageRequest" example: parent: /tutorials slug: renamed-post order: 5 responses: "200": description: Page moved successfully. headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/Page" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /pages/{route}/copy: post: operationId: copyPage tags: [Pages] summary: Copy a page to a new location description: | Create a duplicate of an existing page (including all of its files) at a new route. The destination parent must exist and the target route must not already be occupied. parameters: - $ref: "#/components/parameters/pageRoute" requestBody: required: true content: application/json: schema: type: object required: [route] properties: route: type: string description: The full route for the copied page. example: /blog/my-post-copy example: route: /blog/my-post-copy responses: "201": description: Page copied successfully. headers: Location: description: URL of the newly created page resource. schema: type: string content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/Page" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /pages/{route}/languages: get: operationId: listPageTranslations tags: [Pages] summary: List translations for a page description: | Return the list of languages the page has been translated into and those still missing. Useful for building translation management UIs. parameters: - $ref: "#/components/parameters/pageRoute" responses: "200": description: Translation status for the page. content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/TranslationInfo" example: data: route: /blog/my-first-post default_language: en translated: en: title: My First Post last_modified: "2026-03-22T09:30:00+00:00" fr: title: Mon Premier Article last_modified: "2026-03-23T11:00:00+00:00" untranslated: - de - es "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /pages/{route}/translate: post: operationId: createPageTranslation tags: [Pages] summary: Create a translation for a page description: | Create a new language translation for an existing page. The `lang` field specifies the target language code. If a translation already exists for that language, a `409 Conflict` is returned. parameters: - $ref: "#/components/parameters/pageRoute" requestBody: required: true content: application/json: schema: type: object required: [lang] properties: lang: type: string description: Two-letter language code for the translation. example: fr title: type: string description: Page title in the target language. example: Mon Premier Article content: type: string description: Raw Markdown body in the target language. example: | # Bonjour le Monde Ceci est le corps de l'article. header: type: object additionalProperties: true description: Additional frontmatter fields for the translation. example: lang: fr title: Mon Premier Article content: | # Bonjour le Monde Ceci est le corps de l'article. responses: "201": description: Translation created successfully. headers: Location: description: URL of the page in the new language. schema: type: string example: /api/v1/pages/blog/my-first-post?lang=fr content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/Page" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /pages/batch: post: operationId: batchPages tags: [Pages] summary: Batch operations on multiple pages description: | Apply the same operation to multiple pages at once. Supported operations are `publish`, `unpublish`, `delete`, and `copy`. Each route is processed independently; partial failures are reported in the results array. requestBody: required: true content: application/json: schema: type: object required: [operation, routes] properties: operation: type: string enum: [publish, unpublish, delete, copy] description: The operation to apply to all specified pages. example: publish routes: type: array items: type: string description: Array of page routes to operate on. example: [/blog/draft-one, /blog/draft-two] options: type: object additionalProperties: true description: | Optional parameters for the operation. For `copy`, you may provide `{parent: "/target"}` to copy all pages under a new parent. example: operation: publish routes: - /blog/draft-one - /blog/draft-two responses: "200": description: Batch operation completed. Check individual results for status. content: application/json: schema: type: object properties: data: type: array items: $ref: "#/components/schemas/BatchResult" example: data: - route: /blog/draft-one status: success - route: /blog/draft-two status: success "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /pages/reorganize: post: operationId: reorganizePages tags: [Pages] summary: Reorganize multiple pages (move and reorder atomically) description: | Move and/or reorder multiple pages in a single atomic operation. Each operation specifies a page route and optionally a new parent and/or position. All operations are validated before any changes are applied. Use this endpoint for drag-and-drop tree reorganization where pages may be moved across parents and reordered within the same request. requestBody: required: true content: application/json: schema: type: object required: [operations] properties: operations: type: array items: type: object required: [route] properties: route: type: string description: Current route of the page to reorganize. example: /blog/post-a parent: type: string description: | New parent route. Use `/` for root. Omit to keep the page under its current parent. example: /tutorials position: type: integer minimum: 1 description: | 1-based ordering position under the target parent. Omit to keep the current order value. example: 1 description: | Array of reorganize operations to apply atomically. All are validated before any filesystem changes are made. example: operations: - route: /blog/post-a parent: /tutorials position: 1 - route: /blog/post-b position: 2 - route: /tutorials/old-guide parent: /blog position: 3 responses: "200": description: Pages reorganized successfully. content: application/json: schema: type: object properties: data: type: array items: type: object properties: route: type: string example: /tutorials/post-a slug: type: string example: post-a title: type: string example: Post A order: type: integer nullable: true example: 1 parent: type: string example: /tutorials example: data: - route: /tutorials/post-a slug: post-a title: Post A order: 1 parent: /tutorials - route: /blog/post-b slug: post-b title: Post B order: 2 parent: /blog "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /pages/{route}/reorder: post: operationId: reorderPages tags: [Pages] summary: Reorder children of a page description: | Set the ordering of child pages under the specified parent route. The `order` array must contain the slugs of all immediate children in the desired order. Children not listed retain their current position. parameters: - $ref: "#/components/parameters/pageRoute" requestBody: required: true content: application/json: schema: type: object required: [order] properties: order: type: array items: type: string description: Ordered list of child slugs. example: [third-post, first-post, second-post] example: order: - third-post - first-post - second-post responses: "200": description: Children reordered successfully. content: application/json: schema: type: object properties: data: type: array items: $ref: "#/components/schemas/PageSummary" example: data: - route: /blog/third-post slug: third-post title: Third Post template: blog header: {} taxonomy: {} published: true visible: true routable: true date: "2026-03-20T12:00:00+00:00" modified: "2026-03-22T09:30:00+00:00" order: 1 language: en "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # Page Media # --------------------------------------------------------------------------- /pages/{route}/media: get: operationId: listPageMedia tags: [Media] summary: List media for a page description: Return all media files attached to the specified page. parameters: - $ref: "#/components/parameters/pageRoute" responses: "200": description: List of media items for the page. content: application/json: schema: type: object properties: data: type: array items: $ref: "#/components/schemas/MediaItem" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" post: operationId: uploadPageMedia tags: [Media] summary: Upload media to a page description: | Upload one or more files to the specified page's directory. Files are sent as `multipart/form-data`. The maximum upload size per file is 64 MB. Dangerous file extensions (as configured in Grav's security settings) are rejected. parameters: - $ref: "#/components/parameters/pageRoute" requestBody: required: true content: multipart/form-data: schema: type: object properties: file: type: string format: binary description: One or more files to upload. files: type: array items: type: string format: binary description: Alternative field name for uploading multiple files. responses: "201": description: Files uploaded successfully. headers: Location: description: URL of the page media collection. schema: type: string content: application/json: schema: type: object properties: data: type: array items: $ref: "#/components/schemas/MediaItem" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /pages/{route}/media/{filename}: delete: operationId: deletePageMedia tags: [Media] summary: Delete a media file from a page description: | Remove a single media file (and its `.meta.yaml` sidecar, if present) from the specified page. parameters: - $ref: "#/components/parameters/pageRoute" - name: filename in: path required: true description: Name of the media file to delete. schema: type: string example: hero-image.jpg responses: "204": description: Media file deleted successfully. No content returned. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # Site-level Media # --------------------------------------------------------------------------- /media: get: operationId: listSiteMedia tags: [Media] summary: List site-level media description: | Return a paginated list of files in the site-level media directory (`user/media`). parameters: - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" - name: path in: query description: Subfolder path relative to the media root directory. schema: type: string example: blog/2026 - name: search in: query description: Search term for recursive filename matching across all subfolders. schema: type: string example: hero - name: type in: query description: Filter by media type category. schema: type: string enum: [image, video, audio, document] example: image responses: "200": description: Paginated list of site media files. content: application/json: schema: allOf: - $ref: "#/components/schemas/PaginatedEnvelope" - type: object properties: data: type: array items: $ref: "#/components/schemas/MediaItem" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" post: operationId: uploadSiteMedia tags: [Media] summary: Upload site-level media description: | Upload one or more files to the site-level media directory (`user/media`). The maximum upload size per file is 64 MB. parameters: - name: path in: query description: Subfolder path to upload into. Created automatically if it does not exist. schema: type: string example: blog/2026 requestBody: required: true content: multipart/form-data: schema: type: object properties: file: type: string format: binary description: One or more files to upload. files: type: array items: type: string format: binary description: Alternative field name for uploading multiple files. responses: "201": description: Files uploaded successfully. headers: Location: description: URL of the site media collection. schema: type: string content: application/json: schema: type: object properties: data: type: array items: $ref: "#/components/schemas/MediaItem" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /media/{filename}: delete: operationId: deleteSiteMedia tags: [Media] summary: Delete a site-level media file description: | Remove a file (and its `.meta.yaml` sidecar, if present) from the site-level media directory. parameters: - name: filename in: path required: true description: Name of the file to delete (may include subdirectory path). schema: type: string example: hero-banner.jpg responses: "204": description: File deleted successfully. No content returned. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /media/folders: post: operationId: createMediaFolder tags: [Media] summary: Create a media folder description: | Create a new folder in the site-level media directory (`user/media`). Parent directories are created automatically if they do not exist. requestBody: required: true content: application/json: schema: type: object required: [path] properties: path: type: string description: Folder path to create, relative to the media root. example: blog/2026/april responses: "201": description: Folder created successfully. content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/FolderInfo" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /media/folders/{path}: delete: operationId: deleteMediaFolder tags: [Media] summary: Delete an empty media folder description: | Remove an empty folder from the site-level media directory. The folder must be empty (contain no files or subdirectories). parameters: - name: path in: path required: true description: Folder path relative to the media root. schema: type: string example: blog/2026 responses: "204": description: Folder deleted successfully. No content returned. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /media/rename: post: operationId: renameSiteMedia tags: [Media] summary: Rename or move a media file description: | Rename a file or move it to a different folder within the site-level media directory. The destination directory is created automatically if it does not exist. Any `.meta.yaml` sidecar is also moved. requestBody: required: true content: application/json: schema: type: object required: [from, to] properties: from: type: string description: Current file path relative to the media root. example: blog/hero.jpg to: type: string description: New file path relative to the media root. example: blog/banner.jpg responses: "200": description: File renamed successfully. content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/MediaItem" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /media/folders/rename: post: operationId: renameMediaFolder tags: [Media] summary: Rename a media folder description: | Rename a folder within the site-level media directory. All contents of the folder are preserved. requestBody: required: true content: application/json: schema: type: object required: [from, to] properties: from: type: string description: Current folder path relative to the media root. example: blog/old-name to: type: string description: New folder path relative to the media root. example: blog/new-name responses: "200": description: Folder renamed successfully. content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/FolderInfo" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # Taxonomy # --------------------------------------------------------------------------- /taxonomy: get: operationId: listTaxonomy tags: [Pages] summary: List all taxonomy types and values description: | Return every taxonomy type (e.g. `category`, `tag`) and the set of values currently in use across all pages. responses: "200": description: Taxonomy map returned successfully. content: application/json: schema: type: object properties: data: type: object additionalProperties: type: object description: | Keys are taxonomy values; each value is an array of page routes that use that taxonomy value. additionalProperties: type: array items: type: string example: category: blog: - /blog/post-one - /blog/post-two tutorials: - /tutorials/getting-started tag: grav: - /blog/post-one php: - /tutorials/getting-started "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # Config # --------------------------------------------------------------------------- /config/{scope}: get: operationId: getConfig tags: [Config] summary: Read configuration for a scope description: | Retrieve the current configuration for the given scope. Supported scopes are: | Scope | Maps to | |---|---| | `system` | `system` config | | `site` | `site` config | | `plugins/{name}` | `plugins.{name}` config | | `themes/{name}` | `themes.{name}` config | The response includes an `ETag` header for optimistic concurrency on subsequent PATCH requests. parameters: - $ref: "#/components/parameters/configScope" responses: "200": description: Configuration returned successfully. headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/ConfigObject" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" patch: operationId: updateConfig tags: [Config] summary: Partially update configuration description: | Deep-merge the provided values into the existing configuration for the given scope. The merged result is persisted to the corresponding YAML file and the config cache is cleared. Send the current `ETag` in `If-Match` to detect concurrent edits. parameters: - $ref: "#/components/parameters/configScope" - $ref: "#/components/parameters/ifMatch" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ConfigObject" example: pages: theme: quark debugger: enabled: false responses: "200": description: Configuration updated successfully. headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/ConfigObject" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # Users # --------------------------------------------------------------------------- /users: get: operationId: listUsers tags: [Users] summary: List all users description: Retrieve a paginated list of all user accounts. parameters: - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" responses: "200": description: Paginated list of users. content: application/json: schema: allOf: - $ref: "#/components/schemas/PaginatedEnvelope" - type: object properties: data: type: array items: $ref: "#/components/schemas/User" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" post: operationId: createUser tags: [Users] summary: Create a new user description: | Create a new user account. The username must be 3-64 characters and contain only letters, numbers, hyphens, and underscores. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateUserRequest" example: username: johndoe password: s3cureP@ss email: john@example.com fullname: John Doe title: Editor state: enabled access: api: access: true pages: read: true write: true responses: "201": description: User created successfully. headers: Location: description: URL of the newly created user resource. schema: type: string example: /api/v1/users/johndoe content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/User" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "409": $ref: "#/components/responses/Conflict" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /users/{username}: get: operationId: getUser tags: [Users] summary: Get a single user description: | Retrieve a user account by username. The response includes an `ETag` header for optimistic concurrency on PATCH requests. parameters: - $ref: "#/components/parameters/username" responses: "200": description: User returned successfully. headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/User" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" patch: operationId: updateUser tags: [Users] summary: Partially update a user description: | Apply a partial update to an existing user account. Users can update their own account with the basic `api.access` permission; updating other users requires `api.users.write`. Send the current `ETag` in `If-Match` to detect concurrent edits. parameters: - $ref: "#/components/parameters/username" - $ref: "#/components/parameters/ifMatch" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateUserRequest" example: fullname: Jane Doe email: jane@example.com state: enabled responses: "200": description: User updated successfully. headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/User" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" delete: operationId: deleteUser tags: [Users] summary: Delete a user description: | Permanently remove a user account. You cannot delete your own account; a `403 Forbidden` is returned if you try. parameters: - $ref: "#/components/parameters/username" responses: "204": description: User deleted successfully. No content returned. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # User Avatar # --------------------------------------------------------------------------- /users/{username}/avatar: post: operationId: uploadUserAvatar tags: [Users] summary: Upload user avatar description: | Upload or replace the avatar image for the specified user. The image is sent as `multipart/form-data` with a single `avatar` field. Users can update their own avatar with `api.access`; updating another user's avatar requires `api.users.write`. parameters: - $ref: "#/components/parameters/username" requestBody: required: true content: multipart/form-data: schema: type: object required: [avatar] properties: avatar: type: string format: binary description: Avatar image file to upload. responses: "200": description: Avatar uploaded successfully. Returns the full user object. content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/User" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" delete: operationId: deleteUserAvatar tags: [Users] summary: Remove user avatar description: | Remove the avatar image for the specified user. Users can remove their own avatar with `api.access`; removing another user's avatar requires `api.users.write`. parameters: - $ref: "#/components/parameters/username" responses: "200": description: Avatar removed successfully. Returns the full user object. content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/User" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # User 2FA # --------------------------------------------------------------------------- /users/{username}/2fa: post: operationId: generateUser2FA tags: [Users] summary: Generate 2FA secret and QR code description: | Generate a new two-factor authentication secret and QR code data URI for the specified user. The client should display the QR code for the user to scan with their authenticator app, then confirm activation via a PATCH to the user resource. parameters: - $ref: "#/components/parameters/username" responses: "200": description: 2FA secret and QR code generated. content: application/json: schema: type: object properties: data: type: object properties: secret: type: string description: The TOTP secret key. example: JBSWY3DPEHPK3PXP qr_code: type: string description: QR code as a data URI (PNG image encoded as base64). example: "data:image/png;base64,iVBORw0KGgo..." example: data: secret: JBSWY3DPEHPK3PXP qr_code: "data:image/png;base64,iVBORw0KGgo..." "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # API Keys # --------------------------------------------------------------------------- /users/{username}/api-keys: get: operationId: listApiKeys tags: [Users] summary: List API keys for a user description: | Return all API keys for the specified user. The response includes key metadata but never the key hash. Users can list their own keys with `api.access`; listing another user's keys requires `api.users.read`. parameters: - $ref: "#/components/parameters/username" responses: "200": description: List of API keys. content: application/json: schema: type: object properties: data: type: array items: $ref: "#/components/schemas/ApiKey" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" post: operationId: createApiKey tags: [Users] summary: Create a new API key description: | Generate a new API key for the specified user. The raw key value is returned **only once** in the `api_key` field of the response -- it cannot be retrieved again. Users can create keys for their own account with `api.access`; creating keys for other users requires `api.users.write`. parameters: - $ref: "#/components/parameters/username" requestBody: required: false content: application/json: schema: type: object properties: name: type: string description: A human-readable label for the key. default: API Key example: CI/CD Pipeline scopes: type: array items: type: string description: Optional list of permission scopes to restrict this key. example: [api.pages.read, api.media.read] expiry_days: type: integer nullable: true description: Number of days until the key expires. Omit or set to null for no expiry. example: 90 example: name: CI/CD Pipeline scopes: [api.pages.read, api.media.read] expiry_days: 90 responses: "201": description: API key created. The raw key is returned only in this response. headers: Location: description: URL of the user's API keys collection. schema: type: string content: application/json: schema: type: object properties: data: allOf: - $ref: "#/components/schemas/ApiKey" - type: object properties: api_key: type: string description: | The raw API key. Store it securely -- it is shown only once and cannot be retrieved later. example: grav_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6 "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /users/{username}/api-keys/{keyId}: delete: operationId: deleteApiKey tags: [Users] summary: Revoke an API key description: | Permanently delete an API key. Any future requests using this key will be rejected. parameters: - $ref: "#/components/parameters/username" - name: keyId in: path required: true description: Unique identifier of the API key. schema: type: string example: a1b2c3d4e5f6a7b8 responses: "204": description: API key revoked. No content returned. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # System # --------------------------------------------------------------------------- /system/environments: get: operationId: getEnvironments summary: List available environments tags: [System] description: | Returns the current Grav environment and all environment-specific overrides found in `user/env/`. responses: '200': description: Environment list content: application/json: schema: type: object properties: data: type: object properties: current: type: string example: localhost environments: type: array items: type: object properties: name: type: string example: mysite.com active: type: boolean '401': $ref: '#/components/responses/Unauthorized' /system/info: get: operationId: getSystemInfo tags: [System] summary: Get system information description: | Return runtime information about the Grav installation including Grav version, PHP version, loaded extensions, installed plugins and themes. responses: "200": description: System information returned successfully. content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/SystemInfo" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /cache: delete: operationId: clearCache tags: [System] summary: Clear the cache description: | Clear the Grav cache. The `scope` query parameter controls which cache pool is purged. parameters: - name: scope in: query description: Cache scope to clear. schema: type: string enum: [all, standard, images, assets, tmp] default: standard responses: "200": description: Cache cleared successfully. content: application/json: schema: type: object properties: data: type: object properties: scope: type: string example: standard message: type: string example: "Cache cleared successfully (scope: standard)." details: type: array items: type: string description: Per-directory results from the cache clear operation. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /system/logs: get: operationId: getSystemLogs tags: [System] summary: Read log entries description: | Return paginated entries from the Grav log file (`grav.log`). Entries are returned newest-first. Optionally filter by log level. parameters: - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" - name: level in: query description: Filter entries by log level (case-insensitive). schema: type: string enum: [DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL, ALERT, EMERGENCY] example: ERROR responses: "200": description: Paginated list of log entries. content: application/json: schema: allOf: - $ref: "#/components/schemas/PaginatedEnvelope" - type: object properties: data: type: array items: $ref: "#/components/schemas/LogEntry" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /system/backup: post: operationId: createBackup tags: [System] summary: Create a backup description: Trigger a full site backup and return metadata about the generated archive. responses: "201": description: Backup created successfully. headers: Location: description: URL of the backups list. schema: type: string content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/Backup" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /system/backups: get: operationId: listBackups tags: [System] summary: List existing backups description: Return metadata for all backup archives found on disk. responses: "200": description: List of backups. content: application/json: schema: type: object properties: data: type: array items: $ref: "#/components/schemas/Backup" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # Languages # --------------------------------------------------------------------------- /languages: get: operationId: listLanguages tags: [Languages] summary: List configured site languages description: | Return the language configuration for the site, including which languages are enabled, the default language, and which language is currently active based on the request context. responses: "200": description: Language configuration returned successfully. content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/LanguageInfo" example: data: enabled: true languages: - en - fr - de default: en active: en "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # GPM # --------------------------------------------------------------------------- /gpm/plugins: get: operationId: listInstalledPlugins tags: [GPM] summary: List installed plugins description: | Return a list of all installed plugins with their version, enabled state, and whether an update is available. responses: "200": description: List of installed plugins. content: application/json: schema: type: object properties: data: type: array items: $ref: "#/components/schemas/Package" example: data: - slug: api name: API version: 1.0.0-beta.1 type: plugin enabled: true update_available: false available_version: null author: Team Grav description: RESTful API for Grav CMS. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /gpm/plugins/{slug}: get: operationId: getInstalledPlugin tags: [GPM] summary: Get installed plugin details description: | Return detailed information about a specific installed plugin including its configuration blueprint, README, and changelog. parameters: - name: slug in: path required: true description: Plugin slug (e.g. `admin`, `api`). schema: type: string example: api responses: "200": description: Plugin details returned successfully. content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/Package" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /gpm/themes: get: operationId: listInstalledThemes tags: [GPM] summary: List installed themes description: | Return a list of all installed themes with their version, active state, and whether an update is available. responses: "200": description: List of installed themes. content: application/json: schema: type: object properties: data: type: array items: $ref: "#/components/schemas/Package" example: data: - slug: quark name: Quark version: 2.0.4 type: theme enabled: true update_available: false available_version: null author: Team Grav description: Quark is the default theme for Grav CMS. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /gpm/themes/{slug}: get: operationId: getInstalledTheme tags: [GPM] summary: Get installed theme details description: | Return detailed information about a specific installed theme including its configuration blueprint and README. parameters: - name: slug in: path required: true description: Theme slug (e.g. `quark`). schema: type: string example: quark responses: "200": description: Theme details returned successfully. content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/Package" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /gpm/plugins/{slug}/readme: get: operationId: getPluginReadme tags: [GPM] summary: Get plugin README content description: | Return the raw Markdown content of the specified plugin's README.md file. parameters: - name: slug in: path required: true description: Plugin slug (e.g. `admin`, `api`). schema: type: string example: api responses: "200": description: Plugin README content returned. content: application/json: schema: type: object properties: data: type: object properties: content: type: string description: Raw Markdown content of the README file. example: "# API Plugin\n\nRESTful API for Grav CMS." example: data: content: "# API Plugin\n\nRESTful API for Grav CMS." "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /gpm/plugins/{slug}/changelog: get: operationId: getPluginChangelog tags: [GPM] summary: Get plugin changelog content description: | Return the raw Markdown content of the specified plugin's CHANGELOG.md file. parameters: - name: slug in: path required: true description: Plugin slug (e.g. `admin`, `api`). schema: type: string example: api responses: "200": description: Plugin changelog content returned. content: application/json: schema: type: object properties: data: type: object properties: content: type: string description: Raw Markdown content of the CHANGELOG file. example: "# v1.0.0\n\n- Initial release" example: data: content: "# v1.0.0\n\n- Initial release" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /gpm/themes/{slug}/readme: get: operationId: getThemeReadme tags: [GPM] summary: Get theme README content description: | Return the raw Markdown content of the specified theme's README.md file. parameters: - name: slug in: path required: true description: Theme slug (e.g. `quark`). schema: type: string example: quark responses: "200": description: Theme README content returned. content: application/json: schema: type: object properties: data: type: object properties: content: type: string description: Raw Markdown content of the README file. example: "# Quark Theme\n\nDefault theme for Grav CMS." example: data: content: "# Quark Theme\n\nDefault theme for Grav CMS." "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /gpm/themes/{slug}/changelog: get: operationId: getThemeChangelog tags: [GPM] summary: Get theme changelog content description: | Return the raw Markdown content of the specified theme's CHANGELOG.md file. parameters: - name: slug in: path required: true description: Theme slug (e.g. `quark`). schema: type: string example: quark responses: "200": description: Theme changelog content returned. content: application/json: schema: type: object properties: data: type: object properties: content: type: string description: Raw Markdown content of the CHANGELOG file. example: "# v2.0.4\n\n- Bug fixes" example: data: content: "# v2.0.4\n\n- Bug fixes" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /gpm/updates: get: operationId: checkUpdates tags: [GPM] summary: Check for updates description: | Check the GPM repository for available updates to Grav core, installed plugins, and installed themes. responses: "200": description: Update check completed. content: application/json: schema: type: object properties: data: type: object properties: grav: type: object nullable: true description: Grav core update info, or `null` if up to date. properties: current: type: string example: 1.7.48 available: type: string example: 1.7.49 plugins: type: array items: type: object properties: slug: type: string example: admin current: type: string example: 1.10.44 available: type: string example: 1.10.45 themes: type: array items: type: object properties: slug: type: string example: quark current: type: string example: 2.0.4 available: type: string example: 2.0.5 total: type: integer description: Total number of available updates. example: 3 installed: type: object properties: plugins: type: integer example: 12 themes: type: integer example: 2 example: data: grav: current: 1.7.48 available: 1.7.49 plugins: - slug: admin current: 1.10.44 available: 1.10.45 themes: [] total: 2 installed: plugins: 12 themes: 2 "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /gpm/install: post: operationId: installPackage tags: [GPM] summary: Install a package description: | Install a plugin or theme from the GPM repository. Requires `api.gpm.write` permission. For premium packages, include a `license` field or ensure the license has already been registered via the license-manager plugin/API. requestBody: required: true content: application/json: schema: type: object required: [package, type] properties: package: type: string description: Package slug to install. example: shortcode-core type: type: string enum: [plugin, theme] description: Package type. example: plugin license: type: string description: | License key for premium packages. Format: `XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX`. If omitted, the system checks `user://data/licenses.yaml` for an existing license. example: A1B2C3D4-E5F6A7B8-C9D0E1F2-A3B4C5D6 example: package: shortcode-core type: plugin responses: "200": description: Package installed successfully. content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/Package" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /gpm/remove: post: operationId: removePackage tags: [GPM] summary: Remove a package description: | Remove an installed plugin or theme. Requires super admin permissions. requestBody: required: true content: application/json: schema: type: object required: [package] properties: package: type: string description: Package slug to remove. example: shortcode-core example: package: shortcode-core responses: "200": description: Package removed successfully. content: application/json: schema: type: object properties: data: type: object properties: package: type: string example: shortcode-core message: type: string example: Plugin shortcode-core removed successfully. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /gpm/update: post: operationId: updatePackage tags: [GPM] summary: Update a package description: | Update a single installed plugin or theme to the latest version. Requires super admin permissions. requestBody: required: true content: application/json: schema: type: object required: [package] properties: package: type: string description: Package slug to update. example: admin example: package: admin responses: "200": description: Package updated successfully. content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/Package" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /gpm/update-all: post: operationId: updateAllPackages tags: [GPM] summary: Update all packages description: | Update all installed plugins and themes that have available updates. Requires super admin permissions. responses: "200": description: All packages updated. content: application/json: schema: type: object properties: data: type: object properties: updated: type: array items: type: string description: Slugs of packages that were updated. example: [admin, form, email] failed: type: array items: type: string description: Slugs of packages that failed to update. example: [] total: type: integer example: 3 "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /gpm/upgrade: post: operationId: upgradeGrav tags: [GPM] summary: Self-upgrade Grav core description: | Upgrade Grav to the latest available version. Requires super admin permissions. responses: "200": description: Grav upgraded successfully. content: application/json: schema: type: object properties: data: type: object properties: previous_version: type: string example: 1.7.48 current_version: type: string example: 1.7.49 message: type: string example: Grav upgraded successfully to 1.7.49. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /gpm/direct-install: post: operationId: directInstallPackage tags: [GPM] summary: Install from URL or file upload description: | Install a plugin or theme directly from a URL or an uploaded ZIP file. This bypasses the GPM repository and allows installing custom or development packages. Requires super admin permissions. requestBody: required: true content: application/json: schema: type: object required: [url] properties: url: type: string format: uri description: URL pointing to a ZIP archive of the package. example: https://github.com/user/grav-plugin-custom/archive/main.zip example: url: https://github.com/user/grav-plugin-custom/archive/main.zip multipart/form-data: schema: type: object properties: file: type: string format: binary description: ZIP archive to install. responses: "200": description: Package installed from direct source. content: application/json: schema: type: object properties: data: type: object properties: package: type: string example: custom-plugin type: type: string example: plugin message: type: string example: Package installed successfully from URL. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /gpm/search: get: operationId: searchPackages tags: [GPM] summary: Search GPM repository description: | Search across all plugins and themes in the GPM repository by keyword. Matches against slug, name, description, author, and keywords. parameters: - name: q in: query required: true description: Search query string. schema: type: string example: email - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" responses: "200": description: Paginated search results across plugins and themes. content: application/json: schema: allOf: - $ref: "#/components/schemas/PaginatedEnvelope" - type: object properties: data: type: array items: $ref: "#/components/schemas/Package" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /gpm/repository/plugins: get: operationId: browseRepositoryPlugins tags: [GPM] summary: Browse available plugins description: | Return a paginated list of plugins available in the GPM repository. Includes metadata about each package and whether it is already installed. Use the `q` parameter to filter by keyword. parameters: - name: q in: query required: false description: Filter by keyword (matches slug, name, description, author, keywords). schema: type: string - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" responses: "200": description: Paginated list of available plugins. content: application/json: schema: allOf: - $ref: "#/components/schemas/PaginatedEnvelope" - type: object properties: data: type: array items: $ref: "#/components/schemas/Package" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /gpm/repository/themes: get: operationId: browseRepositoryThemes tags: [GPM] summary: Browse available themes description: | Return a paginated list of themes available in the GPM repository. Includes metadata about each theme and whether it is already installed. Use the `q` parameter to filter by keyword. parameters: - name: q in: query required: false description: Filter by keyword (matches slug, name, description, author, keywords). schema: type: string - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" responses: "200": description: Paginated list of available themes. content: application/json: schema: allOf: - $ref: "#/components/schemas/PaginatedEnvelope" - type: object properties: data: type: array items: $ref: "#/components/schemas/Package" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /gpm/repository/{slug}: get: operationId: getRepositoryPackage tags: [GPM] summary: Get repository package details description: | Return detailed information about a package available in the GPM repository, including its README, changelog, and screenshots. parameters: - name: slug in: path required: true description: Package slug in the repository. schema: type: string example: admin responses: "200": description: Repository package details returned successfully. content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/Package" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /gpm/plugins/{slug}/page: get: operationId: getPluginPage tags: [GPM] summary: Get plugin admin page definition description: | Returns the admin page definition for a plugin that registers a custom page in the admin sidebar. The response includes the page metadata, blueprint reference, data and save endpoints, available actions, and whether the plugin provides a custom web component. parameters: - name: slug in: path required: true description: Plugin slug (e.g. `admin-next`, `api`). schema: type: string example: api responses: "200": description: Plugin page definition returned. content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/PluginPageDefinition" example: data: id: api-settings plugin: api title: API Settings icon: plug page_type: blueprint-form blueprint: /blueprints/plugins/api/pages/settings data_endpoint: /config/plugins/api save_endpoint: /config/plugins/api actions: - save has_custom_component: false "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": description: No admin page found for this plugin. content: application/problem+json: schema: $ref: "#/components/schemas/ErrorResponse" example: status: 404 title: Not Found detail: "No admin page registered for plugin: api" "429": $ref: "#/components/responses/TooManyRequests" /gpm/plugins/{slug}/page-script: get: operationId: getPluginPageScript tags: [GPM] summary: Get plugin page web component script description: | Serves the JavaScript file containing the plugin's page-level web component. This is used by the admin panel to dynamically load plugin-provided custom page UIs. Returns `application/javascript` content type. parameters: - name: slug in: path required: true description: Plugin slug (e.g. `admin-next`). schema: type: string example: admin-next responses: "200": description: JavaScript file returned. content: application/javascript: schema: type: string description: Raw JavaScript source for the plugin's web component. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": description: No page script found for this plugin. content: application/problem+json: schema: $ref: "#/components/schemas/ErrorResponse" example: status: 404 title: Not Found detail: "No page script found for plugin: admin-next" "429": $ref: "#/components/responses/TooManyRequests" /gpm/plugins/{slug}/report-script/{reportId}: get: operationId: getPluginReportScript tags: [GPM] summary: Get plugin report web component script description: | Serves the JavaScript file containing a plugin's report web component. Used by the admin panel to dynamically load plugin-provided report UIs in the Tools > Reports tab. Returns `application/javascript` content. Convention: plugins place report scripts at `admin-next/reports/{reportId}.js` within their plugin directory. parameters: - name: slug in: path required: true description: Plugin slug (e.g. `problems`). schema: type: string example: problems - name: reportId in: path required: true description: Report component identifier matching the `component` field from `/reports`. schema: type: string example: problems-report responses: "200": description: JavaScript file returned. content: application/javascript: schema: type: string description: Raw JavaScript source for the report web component. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": description: No report script found. content: application/problem+json: schema: $ref: "#/components/schemas/ErrorResponse" example: status: 404 title: Not Found detail: "Report component 'problems-report' not found for plugin 'problems'." "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # Scheduler # --------------------------------------------------------------------------- /scheduler/jobs: get: operationId: listSchedulerJobs tags: [Scheduler] summary: List scheduler jobs description: | Return all configured scheduler jobs with their cron expressions, enabled state, last run time, and error status. responses: "200": description: List of scheduler jobs. content: application/json: schema: type: object properties: data: type: array items: $ref: "#/components/schemas/Job" example: data: - id: default-site-backup command: Grav\Common\Backup\Backups::backup expression: "0 3 * * *" enabled: true status: success last_run: "2026-03-26T03:00:00+00:00" error: null "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /scheduler/status: get: operationId: getSchedulerStatus tags: [Scheduler] summary: Get cron status description: | Return the current status of the system cron integration, including whether the cron tab entry is properly installed and the commands used. responses: "200": description: Scheduler status returned. content: application/json: schema: type: object properties: data: type: object properties: crontab_status: type: string description: Whether the crontab entry is installed. enum: [installed, not_installed, error] example: installed cron_command: type: string description: The cron command as it should appear in the crontab. example: "* * * * * cd /var/www/grav && bin/grav scheduler 1>> /dev/null 2>&1" scheduler_command: type: string description: The Grav scheduler CLI command. example: bin/grav scheduler whoami: type: string description: System user running the cron process. example: www-data "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /scheduler/history: get: operationId: getSchedulerHistory tags: [Scheduler] summary: Get job execution history description: | Return a paginated list of past scheduler job runs, ordered by execution time (newest first). parameters: - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" responses: "200": description: Paginated job execution history. content: application/json: schema: allOf: - $ref: "#/components/schemas/PaginatedEnvelope" - type: object properties: data: type: array items: type: object properties: id: type: string example: default-site-backup command: type: string example: Grav\Common\Backup\Backups::backup started: type: string format: date-time example: "2026-03-26T03:00:00+00:00" completed: type: string format: date-time example: "2026-03-26T03:00:45+00:00" status: type: string enum: [success, error] example: success output: type: string nullable: true example: Backup created successfully. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /scheduler/run: post: operationId: triggerSchedulerRun tags: [Scheduler] summary: Trigger a scheduler run description: | Trigger the scheduler to process all due jobs immediately. Optionally force all jobs to run regardless of their schedule. Requires super admin permissions. requestBody: required: false content: application/json: schema: type: object properties: force: type: boolean description: When `true`, run all jobs regardless of schedule. default: false example: force: false responses: "200": description: Scheduler run completed. content: application/json: schema: type: object properties: data: type: object properties: jobs_run: type: integer description: Number of jobs that were executed. example: 2 results: type: array items: type: object properties: id: type: string example: default-site-backup status: type: string enum: [success, error, skipped] example: success message: type: string nullable: true example: null "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /systeminfo: get: operationId: getSystemInfo tags: [System] summary: Get system info overview description: | Return a system info overview including PHP info, Grav status, disk usage, plugin health, and cache status. responses: "200": description: System info returned. content: application/json: schema: type: object properties: data: type: object properties: php: type: object description: PHP runtime details and extension status. additionalProperties: true grav: type: object description: Grav version and health info. additionalProperties: true disk: type: object description: Disk usage information. properties: total: type: integer description: Total disk space in bytes. example: 53687091200 free: type: integer description: Free disk space in bytes. example: 21474836480 used_percent: type: number format: float example: 60.0 plugins: type: object description: Plugin health summary. additionalProperties: true cache: type: object description: Cache driver and status. additionalProperties: true "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # Reports # --------------------------------------------------------------------------- /reports: get: operationId: getReports tags: [Reports] summary: Get plugin-extensible reports description: | Returns an array of diagnostic reports. Built-in reports include a Security Check (XSS scan) and a YAML Linter. Plugins can add their own reports via the `onApiGenerateReports` event, optionally specifying a web component for custom rendering. responses: "200": description: Reports returned. content: application/json: schema: type: object properties: data: type: array items: type: object properties: id: type: string description: Unique report identifier. example: security-check title: type: string description: Human-readable report title. example: Grav Security Check provider: type: string description: Source of the report (core or plugin name). example: core component: type: string nullable: true description: Optional web component tag name for custom rendering. example: null status: type: string enum: [success, warning, error] description: Overall report status. example: success message: type: string description: Summary message for the report. example: "Security Scan complete: No issues found." items: type: array description: Report-specific detail items. items: type: object additionalProperties: true "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # Sidebar # --------------------------------------------------------------------------- /sidebar/items: get: operationId: getSidebarItems tags: [Sidebar] summary: Get admin sidebar navigation items description: | Returns an array of sidebar navigation items contributed by installed plugins. Each item defines a route, label, icon, and optional badge for rendering in the admin sidebar. responses: "200": description: Sidebar items returned successfully. content: application/json: schema: type: object properties: data: type: array items: $ref: "#/components/schemas/SidebarItem" example: data: - id: dashboard plugin: admin-next label: Dashboard icon: gauge route: /dashboard priority: 100 badge: null - id: pages plugin: admin-next label: Pages icon: file-text route: /pages priority: 90 badge: null "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # Dashboard # --------------------------------------------------------------------------- /dashboard/notifications: get: operationId: getDashboardNotifications tags: [Dashboard] summary: Get notifications description: | Return current notifications for the dashboard. Notifications may come from Grav core, plugin updates, or security advisories. parameters: - name: location in: query description: Filter notifications by location (e.g. `dashboard`, `plugins`). schema: type: string example: dashboard - name: force in: query description: When `true`, bypass the notification cache and fetch fresh data. schema: type: boolean default: false responses: "200": description: Notifications returned. content: application/json: schema: type: object properties: data: type: array items: $ref: "#/components/schemas/Notification" example: data: - id: grav-update-1.7.49 type: info message: Grav 1.7.49 is now available. date: "2026-03-25T10:00:00+00:00" location: dashboard link: https://getgrav.org/blog/grav-1.7.49 read: false "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /dashboard/notifications/{id}/hide: post: operationId: hideNotification tags: [Dashboard] summary: Dismiss a notification description: | Mark a notification as hidden/dismissed so it no longer appears in the dashboard feed. parameters: - name: id in: path required: true description: Notification ID. schema: type: string example: grav-update-1.7.49 responses: "200": description: Notification dismissed. content: application/json: schema: type: object properties: data: type: object properties: id: type: string example: grav-update-1.7.49 hidden: type: boolean example: true "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /dashboard/feed: get: operationId: getDashboardFeed tags: [Dashboard] summary: Get news feed description: | Return the latest news feed items from the Grav blog and community. parameters: - name: force in: query description: When `true`, bypass the feed cache and fetch fresh data. schema: type: boolean default: false responses: "200": description: Feed items returned. content: application/json: schema: type: object properties: data: type: array items: $ref: "#/components/schemas/FeedItem" example: data: - title: Grav 1.7.49 Released url: https://getgrav.org/blog/grav-1.7.49 date: "2026-03-25T10:00:00+00:00" excerpt: This release includes performance improvements and bug fixes. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /dashboard/stats: get: operationId: getDashboardStats tags: [Dashboard] summary: Get statistics snapshot description: | Return a summary of site statistics suitable for rendering a dashboard overview. responses: "200": description: Dashboard stats returned. content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/DashboardStats" example: data: pages: 42 users: 5 plugins: 12 theme: quark grav_version: 1.7.49 php_version: 8.3.14 last_backup: "2026-03-26T03:00:00+00:00" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" /dashboard/popularity: get: operationId: getDashboardPopularity tags: [Dashboard] summary: Get page view statistics description: | Return page view statistics from the admin popularity tracker. Includes daily chart data (last 14 days), summary counters (today, this week, this month), and top pages by total views. responses: "200": description: Page view statistics. content: application/json: schema: type: object properties: data: type: object properties: summary: type: object properties: today: type: integer example: 12 week: type: integer example: 84 month: type: integer example: 312 chart: type: array items: type: object properties: date: type: string example: "Mar 28" views: type: integer example: 27 top_pages: type: array items: type: object properties: route: type: string example: "/blog" views: type: integer example: 30 "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # Webhooks # --------------------------------------------------------------------------- /webhooks: get: operationId: listWebhooks tags: [Webhooks] summary: List webhooks description: | Return all configured outbound webhooks with their event subscriptions and enabled state. responses: "200": description: List of webhooks. content: application/json: schema: type: object properties: data: type: array items: $ref: "#/components/schemas/Webhook" example: data: - id: wh_abc123 url: https://example.com/hooks/grav events: - pages.created - pages.updated enabled: true headers: {} created: "2026-03-20T12:00:00+00:00" last_delivery: "2026-03-25T14:30:00+00:00" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "429": $ref: "#/components/responses/TooManyRequests" post: operationId: createWebhook tags: [Webhooks] summary: Create a webhook description: | Register a new outbound webhook. The response includes a `secret` field that can be used to verify delivery signatures. The secret is only returned once at creation time. requestBody: required: true content: application/json: schema: type: object required: [url] properties: url: type: string format: uri description: Destination URL for webhook deliveries. example: https://example.com/hooks/grav events: type: array items: type: string description: | Event types to subscribe to. If empty or omitted, the webhook receives all events. example: [pages.created, pages.updated, pages.deleted] enabled: type: boolean description: Whether the webhook is active. default: true headers: type: object additionalProperties: type: string description: Custom HTTP headers to include in deliveries. example: {} example: url: https://example.com/hooks/grav events: [pages.created, pages.updated] enabled: true responses: "201": description: Webhook created. The `secret` is returned only once. headers: Location: description: URL of the newly created webhook resource. schema: type: string example: /api/v1/webhooks/wh_abc123 content: application/json: schema: type: object properties: data: allOf: - $ref: "#/components/schemas/Webhook" - type: object properties: secret: type: string description: | HMAC secret for verifying delivery signatures. Store it securely -- it is shown only once. example: whsec_a1b2c3d4e5f6g7h8i9j0 example: data: id: wh_abc123 url: https://example.com/hooks/grav events: [pages.created, pages.updated] enabled: true headers: {} created: "2026-03-26T12:00:00+00:00" last_delivery: null secret: whsec_a1b2c3d4e5f6g7h8i9j0 "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" /webhooks/{id}: get: operationId: getWebhook tags: [Webhooks] summary: Get a webhook description: Retrieve a single webhook by its ID. parameters: - $ref: "#/components/parameters/webhookId" responses: "200": description: Webhook returned. content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/Webhook" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" patch: operationId: updateWebhook tags: [Webhooks] summary: Update a webhook description: | Partially update a webhook. Only the fields included in the request body are modified. parameters: - $ref: "#/components/parameters/webhookId" requestBody: required: true content: application/json: schema: type: object properties: url: type: string format: uri example: https://example.com/hooks/grav-v2 events: type: array items: type: string example: [pages.created, pages.updated, pages.deleted] enabled: type: boolean example: true headers: type: object additionalProperties: type: string example: url: https://example.com/hooks/grav-v2 events: [pages.created, pages.updated, pages.deleted] responses: "200": description: Webhook updated. content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/Webhook" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" delete: operationId: deleteWebhook tags: [Webhooks] summary: Delete a webhook description: Permanently remove a webhook and all its delivery history. parameters: - $ref: "#/components/parameters/webhookId" responses: "204": description: Webhook deleted. No content returned. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /webhooks/{id}/deliveries: get: operationId: listWebhookDeliveries tags: [Webhooks] summary: List webhook deliveries description: | Return a paginated log of deliveries for a specific webhook, ordered newest first. parameters: - $ref: "#/components/parameters/webhookId" - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" responses: "200": description: Paginated delivery log. content: application/json: schema: allOf: - $ref: "#/components/schemas/PaginatedEnvelope" - type: object properties: data: type: array items: $ref: "#/components/schemas/WebhookDelivery" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" /webhooks/{id}/test: post: operationId: testWebhook tags: [Webhooks] summary: Send a test delivery description: | Send a test payload to the webhook's URL to verify connectivity. The test event type is `webhook.test`. parameters: - $ref: "#/components/parameters/webhookId" responses: "200": description: Test delivery sent. content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/WebhookDelivery" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # Blueprints # --------------------------------------------------------------------------- /blueprints/pages: get: operationId: listPageTypes tags: [Blueprints] summary: List available page templates description: Returns all page template types available in the active theme. responses: "200": description: List of page types. content: application/json: schema: type: object properties: data: type: array items: type: object properties: type: type: string example: blog label: type: string example: Blog "401": $ref: "#/components/responses/Unauthorized" /blueprints/pages/{template}: get: operationId: getPageBlueprint tags: [Blueprints] summary: Get resolved blueprint for a page template description: | Returns the fully resolved blueprint for the given page template. Inheritance via `extends@` and `import@` directives is resolved server-side, producing a complete field tree ready for client-side form rendering. parameters: - name: template in: path required: true schema: type: string description: Page template name (e.g., `default`, `blog`, `item`) example: blog responses: "200": description: Resolved blueprint. content: application/json: schema: $ref: "#/components/schemas/BlueprintResponse" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /blueprints/plugins/{plugin}: get: operationId: getPluginBlueprint tags: [Blueprints] summary: Get blueprint for a plugin description: Returns the configuration blueprint for the specified plugin. parameters: - name: plugin in: path required: true schema: type: string description: Plugin slug (directory name) example: email responses: "200": description: Plugin blueprint. content: application/json: schema: $ref: "#/components/schemas/BlueprintResponse" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /blueprints/themes/{theme}: get: operationId: getThemeBlueprint tags: [Blueprints] summary: Get blueprint for a theme description: Returns the configuration blueprint for the specified theme. parameters: - name: theme in: path required: true schema: type: string description: Theme slug (directory name) example: quark responses: "200": description: Theme blueprint. content: application/json: schema: $ref: "#/components/schemas/BlueprintResponse" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /blueprints/config/{scope}: get: operationId: getConfigBlueprint tags: [Blueprints] summary: Get blueprint for system configuration description: Returns the configuration blueprint for the specified scope. parameters: - name: scope in: path required: true schema: type: string enum: [system, site, media] description: Configuration scope example: system responses: "200": description: Config blueprint. content: application/json: schema: $ref: "#/components/schemas/BlueprintResponse" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /blueprints/users: get: operationId: getUserBlueprint tags: [Blueprints] summary: Get user account blueprint description: | Returns the blueprint defining the form fields for user account editing. Requires `api.users.read` permission. responses: "200": description: User account blueprint. content: application/json: schema: $ref: "#/components/schemas/BlueprintResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /blueprints/users/permissions: get: operationId: getUserPermissions tags: [Blueprints] summary: Get all registered permission actions description: | Returns all registered permission actions as a nested tree structure. This is used to render the permissions editor in the admin panel. Requires `api.users.read` permission. responses: "200": description: Permission actions tree. content: application/json: schema: type: object properties: data: type: object additionalProperties: true description: | Nested tree of permission actions. Keys are permission group names and values are objects with further nesting or boolean leaf values. example: admin: login: true super: true api: access: true pages: read: true write: true media: read: true write: true users: read: true write: true example: data: admin: login: true super: true api: access: true pages: read: true write: true "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /blueprints/plugins/{plugin}/pages/{pageId}: get: operationId: getPluginPageBlueprint tags: [Blueprints] summary: Get blueprint for a plugin's custom page description: | Returns the fully resolved blueprint for a plugin's custom admin page. This is used to render blueprint-form type pages that plugins register in the admin sidebar. Inheritance via `extends@` and `import@` directives is resolved server-side. parameters: - name: plugin in: path required: true schema: type: string description: Plugin slug (directory name) example: api - name: pageId in: path required: true schema: type: string description: Page identifier registered by the plugin example: settings responses: "200": description: Plugin page blueprint returned. content: application/json: schema: $ref: "#/components/schemas/BlueprintResponse" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" # --------------------------------------------------------------------------- # Data # --------------------------------------------------------------------------- /data/resolve: get: operationId: resolveData tags: [Data] summary: Resolve blueprint data-options@ directives description: | Resolve a `data-options@` directive from a blueprint field definition. The `callable` parameter specifies the PHP callable or data path to evaluate. Results are suitable for populating select fields and other option-based inputs. Requires `api.pages.read` permission. parameters: - name: callable in: query required: true description: | The callable string from a blueprint `data-options@` directive (e.g. `\Grav\Common\Page\Pages::types`). schema: type: string example: '\Grav\Common\Page\Pages::types' responses: "200": description: Resolved data returned. content: application/json: schema: type: object properties: data: description: | The resolved data. Shape depends on the callable -- typically a key-value object or array suitable for option lists. oneOf: - type: object additionalProperties: true - type: array items: {} example: data: default: Default blog: Blog item: Item "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/TooManyRequests" # --------------------------------------------------------------------------- # Thumbnails # --------------------------------------------------------------------------- /thumbnails/{file}: get: operationId: getThumbnail tags: [Thumbnails] summary: Serve cached thumbnail description: | Serve a cached thumbnail image. This endpoint is public and does not require authentication. The `file` parameter is the hashed filename generated by the thumbnail cache system. security: [] parameters: - name: file in: path required: true description: Cached thumbnail filename (e.g. a hashed filename with extension). schema: type: string example: a1b2c3d4e5f6.jpg responses: "200": description: Thumbnail image binary. content: image/jpeg: schema: type: string format: binary image/png: schema: type: string format: binary image/gif: schema: type: string format: binary image/webp: schema: type: string format: binary "404": $ref: "#/components/responses/NotFound" # --------------------------------------------------------------------------- # Translations # --------------------------------------------------------------------------- /translations/{lang}: get: operationId: getTranslations tags: [System] summary: Get translation strings for a language description: | Returns all translation strings for the specified language as a flat key-value object. Supports optional prefix filtering for partial loads. This endpoint does not require authentication. security: [] parameters: - name: lang in: path required: true schema: type: string description: Language code (e.g., `en`, `fr`, `de`) example: en - name: prefix in: query required: false schema: type: string description: Filter strings by key prefix (e.g., `PLUGIN_ADMIN`) example: PLUGIN_ADMIN responses: "200": description: Translation strings. content: application/json: schema: type: object properties: data: type: object properties: lang: type: string example: en count: type: integer example: 1868 checksum: type: string description: MD5 hash for cache invalidation. example: ca482ce9db905019b7f2be290f018553 strings: type: object additionalProperties: type: string description: Flat key-value map of all translation strings. example: PLUGIN_ADMIN.TITLE: Title PLUGIN_ADMIN.CONTENT: Content # ============================================================================= # Components # ============================================================================= components: # --------------------------------------------------------------------------- # Security Schemes # --------------------------------------------------------------------------- securitySchemes: apiToken: type: apiKey in: header name: X-API-Token description: | Recommended transport for JWT access tokens obtained from `POST /auth/token`. Uses a custom header so the token survives FastCGI / PHP-FPM / CGI environments (MAMP, shared hosts) that strip the standard `Authorization` header before it reaches PHP. Accepts either a bare JWT or the `Bearer ` form. bearerAuth: type: http scheme: bearer bearerFormat: JWT description: | Alternative transport for JWT access tokens — standard HTTP Bearer semantics. Works on most hosts but may be stripped by some FastCGI / CGI setups; prefer `X-API-Token` when portability across hosts matters. apiKey: type: apiKey in: header name: X-API-Key description: API key generated via `POST /users/{username}/api-keys`. sessionAuth: type: apiKey in: cookie name: grav-site description: Session cookie set by the Grav admin panel login. # --------------------------------------------------------------------------- # Reusable Parameters # --------------------------------------------------------------------------- parameters: page: name: page in: query description: Page number for pagination (1-based). schema: type: integer minimum: 1 default: 1 per_page: name: per_page in: query description: Number of items per page. Capped at the server's configured maximum (default 100). schema: type: integer minimum: 1 maximum: 100 default: 20 pageRoute: name: route in: path required: true description: | The page route without leading slash (e.g. `blog/my-post`). Supports nested routes with `/` separators. schema: type: string example: blog/my-first-post configScope: name: scope in: path required: true description: | Configuration scope. One of: `system`, `site`, `plugins/{name}`, `themes/{name}`. schema: type: string examples: system: value: system summary: System configuration site: value: site summary: Site configuration plugin: value: plugins/api summary: Plugin configuration theme: value: themes/quark summary: Theme configuration username: name: username in: path required: true description: The username of the user account. schema: type: string pattern: "^[a-zA-Z0-9_-]{3,64}$" example: admin ifMatch: name: If-Match in: header required: false description: | ETag value from the most recent GET response. When provided, the server returns `409 Conflict` if the resource has changed since the ETag was generated. schema: type: string example: '"a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5"' lang: name: lang in: query required: false description: | Two-letter language code. When provided, the request operates on the page translation for that language. If omitted, the default site language is used. schema: type: string example: en webhookId: name: id in: path required: true description: Unique identifier of the webhook. schema: type: string example: wh_abc123 gravEnvironment: name: X-Grav-Environment in: header required: false description: | Target Grav environment. Defaults to the auto-detected environment (from hostname) if omitted. Use `GET /system/environments` to discover available environments. schema: type: string example: localhost # --------------------------------------------------------------------------- # Reusable Headers # --------------------------------------------------------------------------- headers: ETag: description: Entity tag for optimistic concurrency control. schema: type: string example: '"a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5"' # --------------------------------------------------------------------------- # Reusable Responses # --------------------------------------------------------------------------- responses: Unauthorized: description: Authentication is required or the provided credentials are invalid. content: application/problem+json: schema: $ref: "#/components/schemas/ErrorResponse" example: status: 401 title: Unauthorized detail: Invalid username or password. Forbidden: description: The authenticated user does not have the required permission. content: application/problem+json: schema: $ref: "#/components/schemas/ErrorResponse" example: status: 403 title: Forbidden detail: "Missing required permission: api.pages.write" NotFound: description: The requested resource was not found. content: application/problem+json: schema: $ref: "#/components/schemas/ErrorResponse" example: status: 404 title: Not Found detail: "Page not found at route: /nonexistent" Conflict: description: | The resource has been modified since the provided ETag was generated. Fetch the latest version and retry. content: application/problem+json: schema: $ref: "#/components/schemas/ErrorResponse" example: status: 409 title: Conflict detail: The resource has been modified since you last retrieved it. Please fetch the latest version and try again. ValidationError: description: The request body failed validation. content: application/problem+json: schema: allOf: - $ref: "#/components/schemas/ErrorResponse" - type: object properties: errors: type: array items: $ref: "#/components/schemas/ValidationError" example: status: 422 title: Unprocessable Entity detail: "Missing required fields: title, route" errors: - field: title message: "The 'title' field is required." - field: route message: "The 'route' field is required." TooManyRequests: description: Rate limit exceeded. headers: X-RateLimit-Limit: description: Maximum requests allowed per window. schema: type: integer X-RateLimit-Remaining: description: Requests remaining in the current window. schema: type: integer X-RateLimit-Reset: description: UTC epoch timestamp when the rate limit window resets. schema: type: integer content: application/problem+json: schema: $ref: "#/components/schemas/ErrorResponse" example: status: 429 title: Too Many Requests detail: Rate limit exceeded. Try again later. # --------------------------------------------------------------------------- # Schemas # --------------------------------------------------------------------------- schemas: # -- Blueprint ------------------------------------------------------------- BlueprintResponse: type: object properties: data: $ref: "#/components/schemas/Blueprint" Blueprint: type: object description: A resolved blueprint schema defining form fields for content or configuration editing. properties: name: type: string description: Blueprint identifier (template name, plugin slug, etc.) example: blog title: type: string description: Human-readable title. example: Blog type: type: string nullable: true description: Blueprint type (plugin, theme, etc.) child_type: type: string nullable: true description: Default child page type for this template. example: item validation: type: string description: Validation mode (`strict` or `loose`). example: loose fields: type: array description: Top-level field definitions (typically a `tabs` container). items: $ref: "#/components/schemas/BlueprintField" BlueprintField: type: object description: A single form field definition. required: [name, type] properties: name: type: string description: Dot-notation field path (e.g. `header.content.limit`). example: header.title type: type: string description: | Field type. Standard types include: `text`, `textarea`, `select`, `toggle`, `checkbox`, `radio`, `markdown`, `editor`, `filepicker`, `pagemedia`, `taxonomy`, `list`, `array`, `tabs`, `tab`, `section`, `fieldset`, `columns`, `column`, `spacer`, `display`, `hidden`. example: text label: type: string description: Display label (may contain translation keys like `PLUGIN_ADMIN.TITLE`). help: type: string description: Help text shown below the field. placeholder: type: string default: description: Default value for the field. description: type: string description: Extended description (may contain HTML if `markdown` is true). title: type: string description: Section or spacer title. options: type: object additionalProperties: type: string description: Key-value options for select, radio, toggle fields. example: { "asc": "Ascending", "desc": "Descending" } validate: type: object description: Validation rules. properties: type: type: string example: int required: type: boolean pattern: type: string min: type: number max: type: number fields: type: array description: Nested child fields (for tabs, tab, section, fieldset, columns, list). items: $ref: "#/components/schemas/BlueprintField" toggleable: type: boolean multiple: type: boolean markdown: type: boolean description: If true, the description contains HTML/markdown. underline: type: boolean size: type: string enum: [small, medium, large] rows: type: integer description: Number of rows for textarea fields. # -- Sidebar -------------------------------------------------------------- SidebarItem: type: object description: A sidebar navigation item contributed by a plugin. properties: id: type: string description: Unique identifier for the sidebar item. example: dashboard plugin: type: string description: Plugin slug that registered this item. example: admin-next label: type: string description: Display label for the sidebar item. example: Dashboard icon: type: string description: Icon identifier (e.g. Lucide icon name). example: gauge route: type: string description: Admin route this item navigates to. example: /dashboard priority: type: integer description: Sort priority (higher values appear first). example: 100 badge: type: string nullable: true description: Optional badge text (e.g. a count or status indicator). example: null # -- Plugin Page Definition ------------------------------------------------ PluginPageDefinition: type: object description: Admin page definition registered by a plugin. properties: id: type: string description: Unique page identifier. example: api-settings plugin: type: string description: Plugin slug that registered this page. example: api title: type: string description: Page title. example: API Settings icon: type: string description: Icon identifier for the page. example: plug page_type: type: string description: | Page rendering type. `blueprint-form` renders a standard blueprint form; `custom` uses a plugin-provided web component. example: blueprint-form blueprint: type: string nullable: true description: API path to the blueprint for this page (if page_type is blueprint-form). example: /blueprints/plugins/api/pages/settings data_endpoint: type: string nullable: true description: API path to fetch current data for the form. example: /config/plugins/api save_endpoint: type: string nullable: true description: API path to save form data. example: /config/plugins/api actions: type: array items: type: string description: Available actions for the page toolbar. example: [save] has_custom_component: type: boolean description: Whether the plugin provides a custom web component for this page. example: false # -- Page ----------------------------------------------------------------- Page: type: object description: Full page object including content, media, and optional children. properties: route: type: string description: The page route (e.g. `/blog/my-post`). example: /blog/my-first-post slug: type: string description: URL-safe slug derived from the directory name. example: my-first-post title: type: string description: The page title from the header. example: My First Post template: type: string description: The Twig template used to render this page. example: blog header: type: object additionalProperties: true description: The full frontmatter header as a key-value object. example: title: My First Post taxonomy: category: [blog] tag: [intro, grav] taxonomy: type: object additionalProperties: type: array items: type: string description: Taxonomy types and their values for this page. example: category: [blog] tag: [intro, grav] published: type: boolean description: Whether the page is published. example: true visible: type: boolean description: Whether the page is visible in navigation menus. example: true routable: type: boolean description: Whether the page is routable (accessible via URL). example: true date: type: string format: date-time nullable: true description: Page date as ISO 8601. example: "2026-03-20T12:00:00+00:00" modified: type: string format: date-time nullable: true description: Last modification time as ISO 8601. example: "2026-03-22T09:30:00+00:00" order: type: integer nullable: true description: Numeric ordering prefix, or `null` for unordered pages. example: 1 has_children: type: boolean description: Whether this page has child pages. Useful for tree/column UIs to show expand indicators without loading children. example: true content: type: string description: Raw Markdown content of the page body. example: | # Hello World This is the body of the post. content_html: type: string description: | Rendered HTML content. Only present when `render=true` is requested. example: "

Hello World

\n

This is the body of the post.

" media: type: array description: Media files attached to this page. items: type: object properties: filename: type: string example: hero.jpg type: type: string description: MIME type of the file. example: image/jpeg size: type: integer description: File size in bytes. example: 204800 children: type: array description: | Child pages. Only present when `children=true` is requested. Each child is a full Page object (recursion depth controlled by `children_depth`). items: $ref: "#/components/schemas/Page" language: type: string description: Language code of the page content (e.g. `en`, `fr`). example: en translated_languages: type: array description: | Languages this page has been translated into. Only present when `translations=true` is requested. items: type: string example: [en, fr] untranslated_languages: type: array description: | Configured languages that this page has not been translated into. Only present when `translations=true` is requested. items: type: string example: [de, es] required: - route - slug - title - template - header - taxonomy - published - visible - routable - date - modified - order PageSummary: type: object description: | Lightweight page representation used in list responses. Excludes content, media, and children for performance. properties: route: type: string example: /blog/my-first-post slug: type: string example: my-first-post title: type: string example: My First Post template: type: string example: blog header: type: object additionalProperties: true taxonomy: type: object additionalProperties: type: array items: type: string published: type: boolean example: true visible: type: boolean example: true routable: type: boolean example: true date: type: string format: date-time nullable: true example: "2026-03-20T12:00:00+00:00" modified: type: string format: date-time nullable: true example: "2026-03-22T09:30:00+00:00" order: type: integer nullable: true example: 1 language: type: string description: Language code of the page content (e.g. `en`, `fr`). example: en required: - route - slug - title - template - header - taxonomy - published - visible - routable - date - modified - order # -- Media ---------------------------------------------------------------- MediaItem: type: object description: A media file, either from a page or the site-level media folder. properties: filename: type: string description: The file name. example: hero-image.jpg path: type: string description: Relative path from the media root, including the filename. example: blog/2026/hero-image.jpg url: type: string description: Relative URL path to the file. example: /user/images/hero-image.jpg type: type: string description: MIME type of the file. example: image/jpeg size: type: integer description: File size in bytes. example: 204800 dimensions: type: object nullable: true description: Image dimensions. Only present for image files. properties: width: type: integer example: 1920 height: type: integer example: 1080 modified: type: string format: date-time description: Last modification time as ISO 8601. example: "2026-03-22T09:30:00+00:00" required: - filename - url - type - size - modified FolderInfo: type: object properties: name: type: string description: Folder name. example: 2026 path: type: string description: Full relative path from the media root. example: blog/2026 children_count: type: integer description: Number of immediate subdirectories. example: 1 file_count: type: integer description: Number of files in this folder (excluding metadata files). example: 5 # -- User ----------------------------------------------------------------- User: type: object description: A Grav user account. properties: username: type: string example: admin email: type: string format: email nullable: true example: admin@example.com fullname: type: string nullable: true example: Site Administrator title: type: string nullable: true description: Job title or role label. example: Administrator state: type: string enum: [enabled, disabled] description: Whether the account is active. example: enabled avatar_url: type: string nullable: true description: URL to the user's avatar image, or `null` if no avatar is set. example: /user/accounts/admin/avatar.png twofa_enabled: type: boolean description: Whether two-factor authentication is currently enabled for this user. example: false twofa_secret: type: boolean description: Whether a 2FA secret exists for this user (does not expose the actual secret). example: false access: type: object additionalProperties: true description: | Permission tree. Keys are dot-separated permission groups, values are booleans or nested objects. example: admin: super: true api: access: true created: type: string format: date-time nullable: true example: "2026-01-15T10:00:00+00:00" modified: type: string format: date-time nullable: true example: "2026-03-22T09:30:00+00:00" required: - username - state - access # -- Config --------------------------------------------------------------- ConfigObject: type: object additionalProperties: true description: | An arbitrary configuration object. The shape depends on the scope. For scalar values, the response wraps the value as `{"value": ...}`. example: pages: theme: quark debugger: enabled: false # -- System --------------------------------------------------------------- SystemInfo: type: object description: Runtime information about the Grav installation. properties: grav_version: type: string description: The installed Grav core version. example: 1.7.48 php_version: type: string description: PHP runtime version. example: 8.3.14 php_extensions: type: array items: type: string description: List of loaded PHP extensions. example: [curl, gd, intl, json, mbstring, openssl, pdo, zip] server_software: type: string description: Web server identification string. example: Apache/2.4.58 (Unix) environment: type: string description: The active Grav environment name. example: localhost plugins: type: array items: type: object properties: name: type: string example: API version: type: string example: 1.0.0-beta.1 enabled: type: boolean example: true description: Installed plugins with version and enabled state. themes: type: array items: type: object properties: name: type: string example: Quark version: type: string example: 2.0.4 active: type: boolean example: true description: Installed themes with version and active state. required: - grav_version - php_version - php_extensions - server_software - environment - plugins - themes LogEntry: type: object description: A single log file entry from `grav.log`. properties: date: type: string nullable: true description: Timestamp of the log entry. example: "2026-03-22 09:30:15" level: type: string description: Log level. enum: [DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL, ALERT, EMERGENCY] example: ERROR message: type: string description: The log message. example: "Class 'Grav\\Plugin\\FooPlugin' not found" context: type: string description: Additional context or stack trace information. example: "" required: - date - level - message Backup: type: object description: Metadata about a backup archive. properties: filename: type: string description: Name of the backup archive file. example: grav-backup-2026-03-22T09-30-00.zip path: type: string description: Absolute filesystem path. Only present on creation responses. example: /var/www/grav/backup/grav-backup-2026-03-22T09-30-00.zip size: type: integer description: Archive file size in bytes. example: 52428800 date: type: string format: date-time nullable: true description: Creation timestamp as ISO 8601. example: "2026-03-22T09:30:00+00:00" required: - filename - size ApiKey: type: object description: | API key metadata. The key hash is never exposed; only the prefix (first 12 characters plus `...`) is shown. properties: id: type: string description: Unique identifier for the key. example: a1b2c3d4e5f6a7b8 name: type: string description: Human-readable label for the key. example: CI/CD Pipeline prefix: type: string description: First 12 characters of the key plus `...` for identification. example: grav_a1b2c3d4... scopes: type: array items: type: string description: Permission scopes this key is restricted to. Empty means unrestricted. example: [api.pages.read, api.media.read] active: type: boolean description: Whether the key is currently active. example: true created: type: integer nullable: true description: Unix timestamp when the key was created. example: 1711094400 last_used: type: integer nullable: true description: Unix timestamp when the key was last used, or `null` if never used. example: 1711180800 expires: type: integer nullable: true description: Unix timestamp when the key expires, or `null` for no expiry. example: null required: - id - name - prefix - scopes - active # -- Auth ----------------------------------------------------------------- TokenResponse: type: object description: JWT token pair returned by the auth endpoints. properties: access_token: type: string description: Short-lived JWT access token. example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... refresh_token: type: string description: Long-lived refresh token for obtaining new access tokens. example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... token_type: type: string description: Token type. Always `Bearer`. example: Bearer enum: [Bearer] expires_in: type: integer description: Access token lifetime in seconds. example: 3600 required: - access_token - refresh_token - token_type - expires_in # -- Pagination Envelope -------------------------------------------------- PaginatedEnvelope: type: object description: Standard paginated response wrapper. properties: data: type: array items: {} meta: type: object properties: pagination: type: object properties: page: type: integer description: Current page number (1-based). example: 1 per_page: type: integer description: Items per page. example: 20 total: type: integer description: Total number of items across all pages. example: 42 total_pages: type: integer description: Total number of pages. example: 3 required: [page, per_page, total, total_pages] required: [pagination] links: type: object properties: self: type: string description: URL of the current page. example: "/api/v1/pages?page=1&per_page=20" first: type: string description: URL of the first page. Present when `page > 1`. example: "/api/v1/pages?page=1&per_page=20" prev: type: string description: URL of the previous page. Present when `page > 1`. example: "/api/v1/pages?page=1&per_page=20" next: type: string description: URL of the next page. Present when more pages exist. example: "/api/v1/pages?page=2&per_page=20" last: type: string description: URL of the last page. Present when more pages exist. example: "/api/v1/pages?page=3&per_page=20" required: [self] required: [data, meta, links] # -- Error ---------------------------------------------------------------- ErrorResponse: type: object description: | Error response following RFC 7807 Problem Details. Returned with content type `application/problem+json`. properties: status: type: integer description: HTTP status code. example: 422 title: type: string description: Short, human-readable summary of the problem type. example: Unprocessable Entity detail: type: string description: Human-readable explanation specific to this occurrence. example: "Missing required fields: title" errors: type: array description: | Field-level validation errors. Only present on 422 responses when individual field errors are available. items: $ref: "#/components/schemas/ValidationError" required: - status - title - detail ValidationError: type: object description: A single field-level validation error. properties: field: type: string description: The name of the field that failed validation. example: title message: type: string description: Human-readable description of the validation failure. example: "The 'title' field is required." required: - field - message # -- Request Bodies ------------------------------------------------------- CreatePageRequest: type: object description: Request body for creating a new page. required: [route, title] properties: route: type: string description: Full route for the new page (e.g. `/blog/my-new-post`). example: /blog/my-new-post title: type: string description: Page title. example: My New Post template: type: string description: Template name. Defaults to `default` if omitted. default: default example: blog content: type: string description: Raw Markdown body content. default: "" example: | # Hello World This is the body of my new post. header: type: object additionalProperties: true description: | Additional frontmatter fields to merge into the page header. The `title` from the top-level field is always included. example: taxonomy: category: [blog] tag: [new] order: oneOf: - type: integer - type: string enum: [auto] nullable: true description: | Numeric ordering prefix. When set to an integer, creates a visible (ordered) page directory (e.g. `03.my-new-post`). When `null`, creates an unordered page. Pass the string `"auto"` to mirror the admin-classic add-page behavior: the API scans the parent's direct children and assigns the next available number when any sibling already uses numeric prefixes; otherwise the new page is created without a numeric prefix. example: 3 lang: type: string description: | Two-letter language code. When provided, creates the page in the specified language. If omitted, uses the default site language. example: en UpdatePageRequest: type: object description: | Request body for partially updating a page. All fields are optional; only provided fields are changed. properties: title: type: string description: New page title. example: Updated Post Title content: type: string description: New raw Markdown body content. example: | # Updated New content here. template: type: string description: Change the page template. example: blog header: type: object additionalProperties: true description: | Header fields to merge into the existing frontmatter. Existing fields not included here are preserved. example: taxonomy: tag: [updated] published: type: boolean description: Set the published state. example: true visible: type: boolean description: Set the visible state. example: true MovePageRequest: type: object description: Request body for moving a page. required: [parent] properties: parent: type: string description: Route of the new parent page (use `/` for root). example: /tutorials slug: type: string description: New slug. Defaults to the current slug if omitted. example: renamed-post order: type: integer nullable: true description: New ordering prefix. Defaults to the current order if omitted. example: 5 CreateUserRequest: type: object description: Request body for creating a new user account. required: [username, password, email] properties: username: type: string description: | Unique username. Must be 3-64 characters containing only letters, numbers, hyphens, and underscores. pattern: "^[a-zA-Z0-9_-]{3,64}$" example: johndoe password: type: string format: password description: Password for the new account. example: s3cureP@ss email: type: string format: email description: Email address. example: john@example.com fullname: type: string description: Full display name. default: "" example: John Doe title: type: string description: Job title or role label. default: "" example: Editor state: type: string enum: [enabled, disabled] description: Initial account state. default: enabled example: enabled access: type: object additionalProperties: true description: Permission tree for the new user. example: api: access: true pages: read: true write: true UpdateUserRequest: type: object description: | Request body for partially updating a user. All fields are optional; only provided fields are changed. properties: email: type: string format: email example: jane@example.com fullname: type: string example: Jane Doe title: type: string example: Senior Editor state: type: string enum: [enabled, disabled] example: enabled password: type: string format: password description: New password. Only set if a non-empty value is provided. access: type: object additionalProperties: true description: Updated permission tree. # -- Languages ------------------------------------------------------------ LanguageInfo: type: object description: Site language configuration. properties: enabled: type: boolean description: Whether multi-language support is enabled. example: true languages: type: array items: type: string description: List of configured language codes. example: [en, fr, de] default: type: string description: The default language code. example: en active: type: string description: The currently active language based on the request context. example: en required: - enabled - languages - default - active TranslationInfo: type: object description: Translation status for a page across configured languages. properties: route: type: string description: The page route. example: /blog/my-first-post default_language: type: string description: The default language code. example: en translated: type: object additionalProperties: type: object properties: title: type: string description: Page title in that language. last_modified: type: string format: date-time description: Last modification time of the translation. description: | Map of language code to translation metadata for each language that has a translation. example: en: title: My First Post last_modified: "2026-03-22T09:30:00+00:00" fr: title: Mon Premier Article last_modified: "2026-03-23T11:00:00+00:00" untranslated: type: array items: type: string description: Language codes that do not yet have a translation. example: [de, es] required: - route - default_language - translated - untranslated # -- GPM ------------------------------------------------------------------ Package: type: object description: A Grav plugin or theme package. properties: slug: type: string description: Package slug identifier. example: admin name: type: string description: Human-readable package name. example: Admin Panel version: type: string description: Installed version, or latest available version for repository packages. example: 1.10.44 type: type: string enum: [plugin, theme] description: Package type. example: plugin enabled: type: boolean description: Whether the package is currently enabled. example: true update_available: type: boolean description: Whether a newer version is available in the repository. example: false available_version: type: string nullable: true description: The latest available version, or `null` if up to date. example: null author: type: string description: Package author name. example: Team Grav description: type: string description: Short description of the package. example: Provides an admin panel for Grav CMS. homepage: type: string nullable: true description: URL to the package homepage or repository. example: https://github.com/getgrav/grav-plugin-admin keywords: type: array items: type: string description: Tags or keywords associated with the package. example: [admin, panel, manager] installed: type: boolean description: Whether the package is installed locally. Used in repository listings. example: true required: - slug - name - version - type # -- Scheduler ------------------------------------------------------------ Job: type: object description: A configured scheduler job. properties: id: type: string description: Unique job identifier. example: default-site-backup command: type: string description: The command or callable to execute. example: Grav\Common\Backup\Backups::backup expression: type: string description: Cron expression for the schedule. example: "0 3 * * *" enabled: type: boolean description: Whether the job is enabled. example: true status: type: string enum: [success, error, pending] description: Status of the last run. example: success last_run: type: string format: date-time nullable: true description: Timestamp of the last execution. example: "2026-03-26T03:00:00+00:00" error: type: string nullable: true description: Error message from the last run, or `null` if successful. example: null required: - id - command - expression - enabled - status # -- Webhooks ------------------------------------------------------------- Webhook: type: object description: An outbound webhook configuration. properties: id: type: string description: Unique webhook identifier. example: wh_abc123 url: type: string format: uri description: Destination URL for deliveries. example: https://example.com/hooks/grav events: type: array items: type: string description: Event types subscribed to. Empty means all events. example: [pages.created, pages.updated] enabled: type: boolean description: Whether the webhook is active. example: true headers: type: object additionalProperties: type: string description: Custom HTTP headers included in deliveries. example: {} created: type: string format: date-time description: Creation timestamp. example: "2026-03-20T12:00:00+00:00" last_delivery: type: string format: date-time nullable: true description: Timestamp of the most recent delivery, or `null` if never delivered. example: "2026-03-25T14:30:00+00:00" required: - id - url - events - enabled - created WebhookDelivery: type: object description: A record of a single webhook delivery attempt. properties: id: type: string description: Unique delivery identifier. example: del_xyz789 webhook_id: type: string description: ID of the parent webhook. example: wh_abc123 event: type: string description: The event type that triggered this delivery. example: pages.updated url: type: string format: uri description: The URL the payload was delivered to. example: https://example.com/hooks/grav request_headers: type: object additionalProperties: type: string description: HTTP headers sent with the delivery. request_body: type: string description: JSON payload sent in the request body. response_status: type: integer nullable: true description: HTTP status code returned by the receiver, or `null` if the request failed. example: 200 response_body: type: string nullable: true description: Response body from the receiver (truncated to 10 KB). success: type: boolean description: Whether the delivery was successful (2xx response). example: true duration_ms: type: integer description: Request duration in milliseconds. example: 234 delivered_at: type: string format: date-time description: Timestamp of the delivery attempt. example: "2026-03-25T14:30:00+00:00" required: - id - webhook_id - event - url - success - delivered_at # -- Dashboard ------------------------------------------------------------ Notification: type: object description: A dashboard notification. properties: id: type: string description: Unique notification identifier. example: grav-update-1.7.49 type: type: string enum: [info, warning, error, success] description: Notification severity level. example: info message: type: string description: Notification message text. example: Grav 1.7.49 is now available. date: type: string format: date-time description: When the notification was created. example: "2026-03-25T10:00:00+00:00" location: type: string description: Where the notification should appear. example: dashboard link: type: string nullable: true description: Optional URL for more information. example: https://getgrav.org/blog/grav-1.7.49 read: type: boolean description: Whether the notification has been read/dismissed. example: false required: - id - type - message FeedItem: type: object description: A news feed item from the Grav blog or community. properties: title: type: string description: Article title. example: Grav 1.7.49 Released url: type: string format: uri description: URL to the full article. example: https://getgrav.org/blog/grav-1.7.49 date: type: string format: date-time description: Publication date. example: "2026-03-25T10:00:00+00:00" excerpt: type: string description: Short excerpt or summary. example: This release includes performance improvements and bug fixes. required: - title - url - date DashboardStats: type: object description: Summary statistics for the dashboard overview. properties: pages: type: integer description: Total number of pages. example: 42 users: type: integer description: Total number of user accounts. example: 5 plugins: type: integer description: Number of installed plugins. example: 12 theme: type: string description: Currently active theme slug. example: quark grav_version: type: string description: Installed Grav version. example: 1.7.49 php_version: type: string description: PHP runtime version. example: 8.3.14 last_backup: type: string format: date-time nullable: true description: Timestamp of the most recent backup, or `null` if no backups exist. example: "2026-03-26T03:00:00+00:00" required: - pages - users - plugins - theme - grav_version - php_version # -- Batch ---------------------------------------------------------------- BatchResult: type: object description: Result of a single operation within a batch request. properties: route: type: string description: The page route that was operated on. example: /blog/draft-one status: type: string enum: [success, error] description: Whether this individual operation succeeded. example: success message: type: string nullable: true description: Error message if the operation failed, or `null` on success. example: null required: - route - status