Agent API.
Authenticate external AI agents and automation tools to manage your VoxelSite pages, settings, assets, and publishing via REST endpoints. Includes OpenAPI 3.0 schema for tool calling.
Agent API
⚠️ Beta — The Agent API is in beta. The endpoints are stable and production-safe, but we're actively collecting feedback to refine the developer experience. If you encounter unexpected behavior, check the Logging & Debugging section below and reach out to support with your log file. Breaking changes, if any, will be communicated before release.
The Agent API lets external tools manage your VoxelSite installation programmatically. Use it to connect AI agents, automation platforms (Zapier, Make.com, n8n), or custom scripts to your site.
💡 Machine-readable schema available: The API publishes an OpenAPI 3.0 specification at
/_studio/api/agent/v1/schema— no authentication required. Import it into any agent framework, Postman, or code generator. See the Tool Calling & Schema section below for integration examples. Live demo: demo.voxelsite.com/_studio/api/agent/v1/schema
How It Works
sequenceDiagram
participant Agent as Your Agent / Tool
participant API as Agent API
participant Site as VoxelSite
Note over Agent: 1. Discover capabilities
Agent->>API: GET /schema (public, no auth)
API-->>Agent: OpenAPI 3.0 schema (JSON)
Note over Agent: 2. Authenticate
Agent->>API: GET /pages (Authorization: Bearer vxs_...)
Note over API: Validate key → check scope → route
API-->>Agent: { data: { pages: [...] } }
Note over Agent: 3. Make changes
Agent->>API: POST /pages { slug, title, content }
API->>Site: Create page, update nav, create revision
API-->>Agent: { data: { page: {...} } }
Agent->>API: POST /compile
API->>Site: Recompile Tailwind CSS
Agent->>API: POST /publish
API->>Site: Publish preview → production
Agent API vs MCP
VoxelSite exposes two machine-readable interfaces. They serve different purposes:
| Agent API | MCP Endpoint | |
|---|---|---|
| Purpose | Manage the site (create pages, change settings, publish) | Query the site (business info, menus, forms) |
| Auth | Bearer token (API key) | None (public) |
| Audience | Site owner's tools and automations | Any AI agent on the internet |
| Endpoint | /_studio/api/agent/v1/ |
/mcp.php |
| Write access | Yes | Limited (form submissions only) |
| Schema | OpenAPI 3.0 at /schema |
MCP tool definitions |
The MCP endpoint serves public, read-only data. The Agent API is a private management surface behind authentication.
Enabling the Agent API
The Agent API is disabled by default. To enable it:
- Open Settings → API Access
- Toggle Enable Agent API on
- Click Save
The Agent API is blocked in Demo Mode. Remove the
.demofile first if you need to enable it.
Creating API Keys
- In Settings → API Access, click Generate Key
- Enter a label (e.g. "My Website Automation", "Zapier Production")
- Select a role — this sets the maximum permissions:
| Role | Default scopes |
|---|---|
| Agent | Pages (read/write), assets (read/write), compile, publish, submissions, settings (read), tools |
| Editor | Pages (read/write), assets (read/write), compile, submissions, tools |
| Viewer | Pages (read), settings (read), submissions, assets (read) |
- Click Generate
Your API key is shown once. Copy it immediately — it cannot be viewed again.
Keys start with
vxs_and are stored as SHA-256 hashes. VoxelSite never stores the plaintext key.
Authentication
Include your API key as a Bearer token in the Authorization header:
curl -H "Authorization: Bearer vxs_your_key_here" \
https://yourdomain.com/_studio/api/agent/v1/pages
Invalid or missing keys return 401 (codes: no_header, malformed, invalid_key). Rate-limited keys return 429 (code: rate_limited).
Every response includes rate limit headers:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 998
X-RateLimit-Reset: 1710612000
Complete Endpoint Reference
All endpoints are prefixed with /_studio/api/agent/v1/. 14 operations total across 7 resource groups.
Pages
Full CRUD for site pages. Renaming automatically rewrites all internal links and navigation references across the entire site. Deleting cleans up nav items and link references.
| Method | Path | Scope | Description |
|---|---|---|---|
GET |
/pages |
pages:read |
List all pages (paginated) |
GET |
/pages/:slug |
pages:read |
Get a single page with content |
POST |
/pages |
pages:write |
Create a new page |
PUT |
/pages/:slug |
pages:write |
Update page content or rename |
DELETE |
/pages/:slug |
pages:write |
Delete a page |
POST /pages — Create
| Field | Type | Required | Description |
|---|---|---|---|
slug |
string | ✅ | URL slug (auto-normalized: lowercased, special chars removed) |
title |
string | ✅ | Human-readable page title |
content |
string | — | Full PHP/HTML page content |
Returns: 201 with { data: { page: { slug, title, file_path } } }
PUT /pages/:slug — Update
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | — | New page title |
content |
string | — | New PHP/HTML content |
slug |
string | — | New slug (triggers site-wide link rewrite) |
Returns: 200 with { data: { page: { slug, title, file_path }, renamed: bool, suggested_prompt?: string } }
Error codes
| HTTP | Code | When |
|---|---|---|
404 |
not_found |
Page slug doesn't exist |
409 |
conflict |
Slug already taken (create or rename) |
422 |
validation_error |
Empty slug, invalid characters |
422 |
invalid_path |
Path traversal attempt (../) |
Build & Publish
| Method | Path | Scope | Description |
|---|---|---|---|
POST |
/compile |
compile:trigger |
Recompile Tailwind CSS |
POST |
/publish |
publish:trigger |
Publish preview → production |
POST /publish
| Field | Type | Default | Description |
|---|---|---|---|
create_snapshot |
boolean | true |
Create a snapshot backup before publishing |
Returns: { data: { published: true, snapshot_id, files_copied, published_at } }
| HTTP | Code | When |
|---|---|---|
422 |
nothing_to_publish |
No changes since last publish |
500 |
publish_failed |
File system or pipeline error |
Settings
| Method | Path | Scope | Description |
|---|---|---|---|
GET |
/settings |
settings:read |
Read settings (sensitive values redacted) |
PUT |
/settings |
settings:write |
Update settings (whitelisted keys only) |
Readable settings (GET)
site_name, site_tagline, site_language, site_url, site_favicon, ai_provider, nav_style, mobile_nav_style, footer_style, auto_snapshot, max_snapshots, max_revisions, last_published_at, publish_count, agent_api_enabled, agent_api_allowed_origins
Writable settings (PUT)
site_name, site_tagline, site_language, site_url, site_favicon, nav_style, mobile_nav_style, footer_style, auto_snapshot, max_snapshots, max_revisions
Security: AI API keys,
agent_api_enabled, andagent_api_allowed_originscannot be changed via the Agent API. These are management-plane settings that require human access through the Studio UI.
Response: { data: { updated: ["site_name"], rejected: ["agent_api_enabled"] } }
Submissions
| Method | Path | Scope | Description |
|---|---|---|---|
GET |
/submissions |
submissions:read |
List form and action submissions |
Query parameters
| Parameter | Type | Description |
|---|---|---|
form_id |
string | Filter by form ID. Prefix with action_ for action submissions |
status |
string | Filter by status (new, read, etc.) |
source |
string | form, action, or omit for both |
page |
integer | Page number (default: 1) |
per_page |
integer | Results per page (1–100, default: 50) |
Pagination strategy
When filtering by a single source (?source=form), pagination is applied at the database level for efficiency. When mixing sources (default), all matching rows from both databases are collected, globally sorted by created_at DESC, then paginated once — ensuring stable, duplicate-free page boundaries across sources.
Assets
| Method | Path | Scope | Description |
|---|---|---|---|
GET |
/assets |
assets:read |
List uploaded assets |
POST |
/assets |
assets:write |
Upload a file (max 10 MB) |
GET /assets parameters
| Parameter | Type | Description |
|---|---|---|
category |
string | Filter: images, css, js, fonts, files |
page |
integer | Page number |
per_page |
integer | Results per page (1–100) |
POST /assets (multipart/form-data)
| Field | Type | Required | Description |
|---|---|---|---|
file |
binary | ✅ | The file to upload (max 10 MB) |
category |
string | — | Target category (auto-detected from extension if omitted) |
Blocked types: .php, .phtml, .phar, .exe, .bat, .sh, .py, .rb, .htaccess, etc.
Returns: 201 with { data: { path, filename, original, extension, category, size, width?, height? } }
Tools
| Method | Path | Scope | Description |
|---|---|---|---|
GET |
/tools |
tools:invoke |
List available tools |
POST |
/tools/invoke |
tools:invoke |
Invoke a tool by name |
Built-in tools
| Tool | Description | Parameters |
|---|---|---|
get_business_info |
Business name, contact, hours | None |
get_menu |
Restaurant menu data | category (optional filter) |
get_services |
Service listings with pricing | None |
get_faq |
FAQ entries | query (optional search) |
list_forms |
All forms with field summaries | None |
get_form_schema |
Full field definitions for a form | form_id (required) |
submit_form |
Submit data, validated against schema | form_id, data (required) |
Plus any custom Actions configured in the Studio (booking, reservation, etc.).
POST /tools/invoke
{
"name": "submit_form",
"arguments": {
"form_id": "contact",
"data": {
"name": "Jane Doe",
"email": "[email protected]",
"message": "Hello!"
}
}
}
Response Format
Success:
{
"data": {
"pages": [...],
"total": 5,
"page": 1,
"per_page": 50
}
}
Error:
{
"error": {
"code": "invalid_key",
"message": "Invalid API key. The key may have been revoked or never existed."
}
}
Error Codes
| HTTP | Code | Meaning |
|---|---|---|
401 |
no_header |
No Authorization header was sent |
401 |
malformed |
Header present but not in Bearer vxs_... format |
401 |
invalid_key |
Key not found or revoked |
403 |
feature_disabled |
Agent API is disabled or demo mode is active |
403 |
insufficient_scope |
Key lacks the required scope for this endpoint |
404 |
not_found |
No endpoint matches the request method and path |
409 |
conflict |
Resource already exists (e.g. page slug taken) |
422 |
validation_error |
Invalid input data |
429 |
rate_limited |
Hourly rate limit exceeded (check X-RateLimit-Reset) |
500 |
publish_failed / server_error |
Internal error |
503 |
service_unavailable |
Auth service could not initialize |
Tool Calling & Schema
The Agent API publishes an OpenAPI 3.0.3 schema that any AI agent framework can consume for tool calling. This is the machine-readable contract that makes the API agent-compatible.
Schema Endpoint
# Public endpoint — no authentication required
curl https://yourdomain.com/_studio/api/agent/v1/schema
Returns the complete OpenAPI specification in JSON, describing every endpoint, parameter, request body, response shape, and error code.
Integration with Agent Frameworks
flowchart LR
subgraph setup ["One-time Setup"]
A["Agent starts"] --> B["GET /schema"]
B --> C["Parse OpenAPI spec"]
C --> D["Register as tools"]
end
subgraph runtime ["Runtime"]
E["User prompt"] --> F["LLM selects tool"]
F --> G["Agent calls API"]
G --> H["API executes & returns"]
H --> I["LLM processes result"]
end
setup --> runtime
OpenAI Function Calling
import openai, requests, json
BASE = "https://yourdomain.com/_studio/api/agent/v1"
KEY = "vxs_your_key_here"
# 1. Fetch the schema (once at startup)
spec = requests.get(f"{BASE}/schema").json()
# 2. Convert to OpenAI tools format
tools = []
for path, methods in spec["paths"].items():
for method, op in methods.items():
params = {}
if "requestBody" in op:
body_schema = op["requestBody"]["content"]["application/json"]["schema"]
params = body_schema.get("properties", {})
tools.append({
"type": "function",
"function": {
"name": op["operationId"],
"description": f"{op['summary']}\n\nHTTP: {method.upper()} {path}",
"parameters": {
"type": "object",
"properties": params,
"required": body_schema.get("required", []) if "requestBody" in op else [],
},
},
})
# 3. Pass to the LLM
response = openai.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "Create an About page"}],
tools=tools,
)
# 4. Execute the tool call
call = response.choices[0].message.tool_calls[0]
method_path = call.function.description.split("HTTP: ")[1]
method, api_path = method_path.split(" ", 1)
result = requests.request(
method=method,
url=f"{BASE}{api_path}",
headers={"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"},
json=json.loads(call.function.arguments),
)
print(result.json())
Anthropic/Claude Tool Use
import anthropic, requests, json
BASE = "https://yourdomain.com/_studio/api/agent/v1"
KEY = "vxs_your_key_here"
# Convert schema to Claude tools
spec = requests.get(f"{BASE}/schema").json()
tools = []
for path, methods in spec["paths"].items():
for method, op in methods.items():
tool = {
"name": op["operationId"],
"description": f"{op['summary']}\n\nHTTP: {method.upper()} {path}",
"input_schema": {"type": "object", "properties": {}, "required": []},
}
if "requestBody" in op and "application/json" in op["requestBody"].get("content", {}):
schema = op["requestBody"]["content"]["application/json"]["schema"]
tool["input_schema"]["properties"] = schema.get("properties", {})
tool["input_schema"]["required"] = schema.get("required", [])
tools.append(tool)
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": "List all pages on my site"}],
)
What the Schema Contains
| Section | Contents |
|---|---|
info |
API name, version, description |
servers |
Base URL (/_studio/api/agent/v1) |
security |
Bearer authentication scheme |
components.schemas |
Reusable data models: Page, Asset, Submission, Tool, ErrorResponse |
paths |
Every endpoint with method, parameters, request body schema, response schema, and error codes |
Scopes Reference
| Scope | Grants |
|---|---|
pages:read |
Read page list and content |
pages:write |
Create, update, and delete pages |
settings:read |
Read settings (redacted) |
settings:write |
Update whitelisted settings |
compile:trigger |
Trigger Tailwind CSS compilation |
publish:trigger |
Publish preview to production |
submissions:read |
Read form and action submissions |
assets:read |
List uploaded assets |
assets:write |
Upload new assets |
tools:invoke |
List and invoke tools |
CORS Origins
By default, the Agent API accepts requests from any origin (*). To restrict access:
- Open Settings → API Access
- In the Allowed Origins textarea, enter one origin per line:
https://yourdomain.com
https://app.zapier.com
https://hook.eu1.make.com
- Click Save
Nginx Configuration
On Apache, the Agent API works out of the box. On Nginx (Forge, RunCloud, Ploi), routing works automatically via the Studio router fallback — no rewrite rule is required. You only need one addition:
Forward the Authorization header (required)
Add this inside your location ~ \.php$ block:
fastcgi_param HTTP_AUTHORIZATION $http_authorization;
Without this line, Nginx strips the Authorization header and all Agent API requests fail with 401.
Route Agent API requests directly (optional)
For direct routing — bypassing the Studio router for better performance — add this before the location ~ \.php$ block:
location ~ ^/_studio/api/agent/v1/(?!.*\.php)(.*)$ {
rewrite ^/_studio/api/agent/v1/(.*)$ /_studio/api/agent/v1/router.php?_path=$1&$args last;
}
This is a performance optimization. The Studio router fallback handles the same routing automatically if this rule is not present.
See Nginx Configuration for the complete config example.
Examples
List all pages
curl -H "Authorization: Bearer vxs_abc123..." \
https://yourdomain.com/_studio/api/agent/v1/pages
Create a page
curl -X POST \
-H "Authorization: Bearer vxs_abc123..." \
-H "Content-Type: application/json" \
-d '{"title": "About Us", "slug": "about", "content": "<?php ..."}' \
https://yourdomain.com/_studio/api/agent/v1/pages
Compile and publish
# Recompile Tailwind CSS
curl -X POST \
-H "Authorization: Bearer vxs_abc123..." \
https://yourdomain.com/_studio/api/agent/v1/compile
# Publish to production
curl -X POST \
-H "Authorization: Bearer vxs_abc123..." \
https://yourdomain.com/_studio/api/agent/v1/publish
Upload an image
curl -X POST \
-H "Authorization: Bearer vxs_abc123..." \
-F "[email protected]" \
-F "category=images" \
https://yourdomain.com/_studio/api/agent/v1/assets
Invoke a tool
curl -X POST \
-H "Authorization: Bearer vxs_abc123..." \
-H "Content-Type: application/json" \
-d '{"name": "get_business_info", "arguments": {}}' \
https://yourdomain.com/_studio/api/agent/v1/tools/invoke
Logging & Debugging
Every Agent API request is logged to help you diagnose issues, especially on servers you can't access directly.
Log Location
Logs are stored in _studio/logs/ as daily JSON-line files:
_studio/logs/2026-03-16.log
_studio/logs/2026-03-15.log
Each line is valid JSON, making logs easy to parse with jq, grep, or any log viewer. Log files are automatically rotated — files older than 30 days are pruned.
Log Format
Every Agent API log entry uses the agent-api channel and includes:
{
"ts": "2026-03-16T14:23:01.123Z",
"level": "INFO",
"ch": "agent-api",
"rid": "a1b2c3d4",
"msg": "POST /pages",
"ctx": {
"handler": "pages.php",
"scope": "pages:write",
"key_label": "My Website Automation",
"key_role": "agent",
"key_pre": "vxs_61b90f0b",
"ip": "192.168.1.100"
}
}
| Field | Description |
|---|---|
ts |
UTC timestamp (millisecond precision) |
level |
INFO, WARNING, or ERROR |
ch |
Always agent-api for Agent API entries |
rid |
Request ID — same for all log entries within a single request |
msg |
What happened — e.g. POST /pages, Page created, Auth failed: invalid_key |
ctx |
Structured metadata — key label, role, IP, error details |
What Gets Logged
| Event | Level | Example message |
|---|---|---|
| Request dispatched | INFO |
POST /pages |
| Page created/updated/deleted | INFO |
Page created, Page updated, Page deleted |
| Site published | INFO |
Site published |
| CSS compiled | INFO |
CSS compiled |
| Settings updated | INFO |
Settings updated |
| Asset uploaded | INFO |
Asset uploaded |
| Tool invoked | INFO |
Tool invoked: data, Tool invoked: form, Tool invoked: action |
| Auth failure | WARNING |
Auth failed: invalid_key, Auth failed: rate_limited |
| Scope denied | WARNING |
Scope denied |
| Route not found | WARNING |
Route not found |
| Page not found | WARNING |
Page not found, Page not found for update |
| Asset validation | WARNING |
Asset upload: no file, Asset upload: file too large, Asset upload: invalid category, Asset blocked extension |
| Settings rejected | WARNING |
Settings update: all keys rejected |
| Tool validation | WARNING |
Tool invoke: missing name, Tool invoke: form not found, Tool invoke: form validation failed, Tool invoke: action failed |
| Feature/demo blocked | INFO |
Request blocked: feature disabled, Request blocked: demo mode |
| Operation error | ERROR |
Page create failed, Publish failed, Asset file save failed, CSS compile failed |
Filtering Logs
Grep for Agent API entries only:
grep '"ch":"agent-api"' _studio/logs/2026-03-16.log
Filter by level:
grep '"ch":"agent-api"' _studio/logs/2026-03-16.log | grep '"level":"ERROR"'
Filter by key label:
grep '"ch":"agent-api"' _studio/logs/2026-03-16.log | grep '"key_label":"My Website Automation"'
Trace a complete request by request ID:
grep '"rid":"a1b2c3d4"' _studio/logs/2026-03-16.log | jq .
Sending Logs for Support
If you encounter an issue with the Agent API:
- Note the approximate time the error occurred
- Find the matching log file in
_studio/logs/(named by date) - Filter for
agent-apientries around that time - Send the relevant log lines to support — they contain:
- Request ID (
rid) to correlate all entries from a single request - Key label and role to identify which integration failed
- IP address and user agent for network-level context
- Error details: validation warnings include the specific error code and rejected values; exception errors include the exception class, file, line number, and a truncated stack trace
- Request ID (
Security note: Log files contain key prefixes (first 12 characters) but never the full API key. IP addresses and key labels are included for debugging. Log files are protected from web access by
.htaccessand should not be publicly accessible.
Related
- Settings → API Access — enable the API and manage keys
- Nginx Configuration — required Nginx setup for the Agent API
- SEO & AI Discovery — the public MCP endpoint and
llms.txt
Ready to build?
One-time purchase. Self-hosted. Own every file forever.