Getting Started with the Grav API
The Grav API Plugin adds a RESTful API to your Grav site, providing full headless access to pages, media, configuration, users, and system management.
Requirements
- Grav CMS 2.1.5+
- PHP 8.3+
- Login Plugin 3.9.2+
Installation
Install via GPM:
bin/grav install api
Or manually: download the plugin into user/plugins/api and run composer install.
Quick Setup
1. Enable the Plugin
# user/config/plugins/api.yaml
enabled: true
2. Generate an API Key
Via CLI (recommended for initial setup):
bin/plugin api keys:generate --user=admin --name="My First Key"
Via Admin Panel: Navigate to a user's profile and use the API Keys section.
The key is shown once — save it immediately.
3. Make Your First Request
curl https://my-site.example/api/v1/pages \
-H "X-API-Key: grav_abc123..."
Configuration
The API is configured via user/config/plugins/api.yaml:
enabled: true
route: /api # Base route for all API endpoints
version_prefix: v1 # Version prefix
auth:
api_keys_enabled: true # Enable API key authentication
jwt_enabled: true # Enable JWT token authentication
session_enabled: true # Enable session passthrough
cors:
enabled: true # Enable CORS headers
origins: [] # Extra origins allowed cross-origin (same-origin only by default)
credentials: false # Allow credentials
rate_limit:
enabled: true # Enable rate limiting
requests: 120 # Requests allowed per window, per user (or per IP when anonymous)
window: 60 # Window length in seconds
# excluded_paths: ['/sync/'] # Route prefixes that skip the limit (default: collaboration polling only)
pagination:
default_per_page: 20 # Default items per page
max_per_page: 1000 # Maximum items per page
Environments
Grav supports multiple environments. The API respects this via the X-Grav-Environment header:
curl -H "X-Grav-Environment: my-site.example" \
-H "X-API-Key: ..." \
https://my-site.example/api/v1/pages
Response Format
All API responses follow a consistent structure:
{
"data": { ... }
}
Paginated responses include metadata:
{
"data": [ ... ],
"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"
}
}
The links keep any other query parameters you sent (filters, search, sort), so following next on a filtered list stays within that filter.
Errors use RFC 7807 format, with the application/problem+json content type. A 422 validation error can also carry an errors list of {field, message} entries:
{
"status": 404,
"title": "Not Found",
"detail": "Page '/missing' not found."
}
Concurrency Control
The API uses ETags for optimistic concurrency. When updating resources, include the If-Match header with the ETag from the GET response:
# Fetch with ETag
curl -H "X-API-Key: ..." https://my-site.example/api/v1/pages/blog
# Response includes: ETag: "abc123"
# Update with If-Match
curl -X PATCH \
-H "X-API-Key: ..." \
-H "If-Match: \"abc123\"" \
-H "Content-Type: application/json" \
-d '{"title": "Updated Title"}' \
https://my-site.example/api/v1/pages/blog
If the resource was modified since your last fetch, you'll receive a 409 Conflict response.
Using the API with AI
The API is what powers AI integration in Grav 2.0. The first-party Grav MCP Server connects agentic AI clients (Claude Code, Claude Desktop, Cursor, and other Model Context Protocol clients) to your site, exposing pages, media, configuration, users, and system management as AI-callable tools. It authenticates with the same API keys described here and respects the same permission model. See the MCP Server documentation to get started.