Agent API
Connect external agents and scripts to VoxelSite with authenticated REST endpoints. Check roles, scopes and beta limits before enabling access.
The Agent API is VoxelSite’s authenticated REST interface for scripts and integrations. It remains beta. Test an integration on a copy before granting write or publish access.
Read the installed OpenAPI specification at /_studio/api/agent/v1/schema. Current writes use stable identities and captured versions; older request examples without those fields are not valid for 1.33.0.
Agent API vs MCP
| Interface | Endpoint | Purpose and access |
|---|---|---|
| Agent REST API | /_studio/api/agent/v1/ |
Site management through HTTP, using an enabled REST credential and its scopes |
| Studio MCP | /_studio/mcp.php |
Private authoring, exact reviews, lifecycle and administration through scoped account-bound connections |
| Public MCP | /mcp.php |
Published business information and enabled visitor interactions; no private authoring access |
REST and Studio MCP credentials have separate audiences. Do not send one to the other endpoint. See Studio MCP overview for external-client setup.
Enabling the Agent API
The owner enables Settings → API Access → Enable Agent API, then saves API settings. Demo mode blocks access.
Create a REST key with the role and scopes the integration needs. Save the secret once in a private environment variable. Keys use the vxs_ prefix and are stored as hashes. Revocation prevents further use.
Existing keys need a current-account binding. In Settings → Connections, use Connect existing REST keys to an account when prompted. Choose the intended account explicitly. The credential’s audience and scopes remain unchanged; its account’s current role is an additional permission ceiling. An unbound key cannot be used as an implicit owner.
Roles and scopes
| Key role | Typical default scope ceiling |
|---|---|
| Agent | Pages, assets, compile, publish, submissions, safe settings reads and tools |
| Editor | Pages, assets, compile, submissions and tools |
| Viewer | Read-only pages, assets, submissions and safe settings |
The owner controls the granted scopes. Provider-backed prompt:execute is a separate opt-in grant and is unavailable to viewer keys. It also needs exec() and PHP CLI; see Prompt execution.
Authentication
Send the credential in the Authorization header over HTTPS. Example:
curl -H "Authorization: Bearer $VOXELSITE_API_KEY" \
https://example.com/_studio/api/agent/v1/pages
Missing, malformed or invalid credentials return HTTP 401. Permission failures return 403. Current account, role and scope checks also apply to retained retries. Authentication and rate-limit responses can include X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.
Versioned writes and retries
Read the current draft or record before writing. Keep its version and hash with the request you intend to send.
- Use a stable
operation_idfor page, compile, publish and settings writes. - Use
request_idfor asset uploads and business-tool mutations where required by their contract. - Reuse the same identity and original input after a lost response. A new identity requests a new operation.
- If an operation returns
applying, use its returned checkpoint to continue the original request. - A completed replay returns the retained result. It does not create another revision or publication.
- A source conflict requires a fresh read and a new intentional edit, not a guessed version or forced overwrite.
Page and compile operations share the private draft with Studio and Studio MCP. Draft CSS, scripts and mutable assets stay off the published site until publication. Business submissions are immediate live records. See Site lifecycle and Business workspaces.
Endpoint reference
Prefix these paths with /_studio/api/agent/v1. The installed schema provides the version-specific details.
Pages
| Method | Path | Scope | Result |
|---|---|---|---|
| GET | /pages |
pages:read |
Draft page list, source hashes and draft version |
| GET | /pages/:slug |
pages:read |
Page source, content hash and draft version |
| POST | /pages |
pages:write |
Create a draft page |
| PUT | /pages/:slug |
pages:write |
Update source, metadata or supported route fields |
| DELETE | /pages/:slug |
pages:write |
Delete a page with supported reference updates |
Use page and per_page for REST listing. Pass the captured draft_version when continuing the inventory. Read returns data.page.content_sha256 and data.draft_version.
All page writes require operation_id, base_draft_version and expected_content_sha256. Creation uses explicit null for the expected hash and also requires slug, title and content. Use a canonical lowercase slug with hyphens; it is validated rather than silently rewritten. Updates and deletion require the hash returned by the page read. A deletion can include replacementHref.
Example creation body, after replacing the illustrative version with the value you read:
{
"operation_id": "create-about-unique-request",
"base_draft_version": "draft:12",
"expected_content_sha256": null,
"slug": "about",
"title": "About us",
"content": "<?php $page = ['title' => 'About us']; include __DIR__ . '/_partials/header.php'; ?><h1>About us</h1><?php include __DIR__ . '/_partials/footer.php'; ?>"
}
The response includes a retained operation, resulting draft version and affected paths. Inspect operation.status; HTTP success alone does not mean bounded work has finished. Supported reference updates are reviewed by the shared service. Unsafe or ambiguous propagation can be refused.
Build & publish
| Method | Path | Scope | Request |
|---|---|---|---|
| POST | /compile |
compile:trigger |
operation_id, captured base_draft_version, optional returned checkpoint |
| POST | /publish |
publish:trigger |
operation_id, optional create_snapshot, optional returned checkpoint |
Compile changes private draft CSS. REST publication is an explicit command to seal and publish the current draft. For a separate prepare, inspect and apply interaction, use the Studio MCP publication tools.
Publication returns HTTP 202 while applying and HTTP 200 when succeeded. The data.operation object carries status and checkpoint. Re-submit the same operation ID with the returned checkpoint to continue. Repeating the ID without a checkpoint reads its retained result. create_snapshot: false does not disable the required recovery capture.
Example publish body:
{"operation_id":"publish-reviewed-site-unique-request","create_snapshot":true}
A stale candidate, changed live file or unresolved recovery blocks publication. It does not authorize overwriting unrelated content.
Settings
| Method | Path | Scope | Result |
|---|---|---|---|
| GET | /settings |
settings:read |
Safe settings, group versions and group keys |
| PUT | /settings |
settings:write |
Versioned change to allowed site settings |
Read returns data.settings, data.versions and data.group_keys. The writable groups are site identity, navigation and retention. PUT requires operation_id, a typed values object and the captured versions for the groups changed. Read the current response before assembling the request.
REST settings cannot write provider configuration, secrets, mail settings or the favicon. Use the owner controls or explicitly approved Studio MCP administration. Unknown fields are refused; they are not silently ignored. The result includes the retained operation and changed keys.
Submissions
GET /submissions requires submissions:read. Choose source=form, source=action or source=all. The /submissions/forms and /submissions/actions routes select their corresponding source.
Supported filters include form_id or entity_id, status, search, date_from, date_to and record_source. Use limit or per_page from 1 to 100. Continue with the returned next_cursor, keeping the same filter. Offset page numbers beyond the first page are refused.
The result contains submissions, next_cursor and consistency information. Form and Action records retain their own storage and source attribution. Website snapshots do not restore these records.
Assets
GET /assets requires assets:read and returns private draft assets. Continue listing with the captured draft_version.
POST /assets requires assets:write and multipart form data:
| Field | Meaning |
|---|---|
file |
File, up to 10 MiB |
request_id |
Stable identity for this exact upload |
base_draft_version |
Version captured from the asset list |
category |
Optional images, fonts, media or files |
path |
Optional allowed target path |
The adapter validates the file type and stages it privately before applying the draft change. Edit CSS and JavaScript through authoring tools. Uploading a file does not publish it. The response includes the path, review and retained operation.
Tools
GET /tools lists the business tools allowed for the credential. POST /tools/invoke uses the closed {name, arguments} envelope, plus the mutation fields returned by discovery.
| Tool | Purpose |
|---|---|
get_business_info |
Published site.json information |
get_menu |
Published menu, with optional category filter |
get_services |
Published services |
get_faq |
Published FAQ, with optional query filter |
list_forms |
Published form schemas |
get_form_schema |
A published form and its submission version |
submit_form |
Create a real form record |
Active Actions add their own tools. Discovery provides each Action’s ID, version and required request fields. These reads use published data, never private authoring memory or a draft fallback.
For submit_form, pass the schema’s version and a stable request_id at the envelope’s top level. Action mutations also require the discovered action_id. Reuse the same identity and exact input after a lost response. Notification sends need explicit mail access; do not interpret record storage as email delivery.
Example form submission envelope, using the actual version returned by get_form_schema:
{
"name": "submit_form",
"version": "REPLACE_WITH_RETURNED_VERSION",
"request_id": "contact-unique-request",
"arguments": {
"form_id": "contact",
"data": {"name": "Example visitor", "email": "[email protected]", "message": "Please contact me."}
}
}
Prompt execution
This beta endpoint calls the configured built-in provider and can incur provider charges. It is separate from provider-free Studio MCP authoring. Prompt execution shares the owner’s AI Router settings with Studio. Routing can select the writing model and prompt recipe; a routing failure uses the default generation path. Agent clients cannot change Router settings or read the TypeSafe key. Direct file and publish endpoints do not pass through the router.
Beta: Test prompt execution on a copy of the site. Check terminal status and modified files before submitting another prompt.
Run AI prompts programmatically through the Agent API. Prompts are executed asynchronously: you submit a prompt, receive a poll URL, and check back for results.
Prerequisites
| Requirement | Details |
|---|---|
| Provider | Configure built-in generation in Studio; an external-client login does not configure this endpoint. |
| Server | exec() must be enabled (not in disable_functions), and a working PHP command-line executable must be available to the web server. Hosting that blocks either requirement cannot run Agent prompts. |
| Scope | The API key must have the prompt:execute scope. This is opt-in - check AI Prompt Execution when generating the key. |
| Role | Only owner, editor, and agent keys are eligible. Viewer keys cannot get prompt:execute because the prompt engine has full workspace write access. |
VoxelSite verifies the PHP executable before creating a conversation or queued job. A failed check returns 503 php_cli_unavailable; disabled exec() returns 503 exec_unavailable. Accepted prompts use the same saved output ceiling and model limits as Studio generation.
Async flow
sequenceDiagram
participant Agent as Your Agent / Tool
participant API as Agent API
participant Worker as prompt-runner.php
Agent->>API: POST /prompt {prompt, page_scope?}
API->>API: Verify PHP command-line executable
API-->>Agent: 202 Accepted {id, status: "queued", poll_url}
API->>Worker: spawn background worker
Worker->>Worker: PromptEngine::execute()
loop Poll every 2-5 seconds
Agent->>API: GET /prompt/{id}
API-->>Agent: {status, files_modified?, tokens?}
end
Note over Agent: status: queued → streaming → success / partial / error
| Method | Path | Scope | Description |
|---|---|---|---|
POST |
/prompt |
prompt:execute |
Queue a prompt for execution |
GET |
/prompt/:id |
prompt:execute |
Poll prompt status and result |
POST /prompt - queue
| Field | Type | Required | Description |
|---|---|---|---|
prompt |
string | ✅ | The instruction for the AI (e.g. "Create an About page with our team info") |
action_type |
string | - | Prompt type. Default: free_prompt. Also: edit_page, change_design, add_section, section_edit, optimize_aeo, inline_edit |
action_data |
object | - | Action-specific parameters (e.g. target element for inline_edit) |
page_scope |
string | - | Focus context on a page slug; this does not restrict all writes to that page |
continue_from |
integer | - | A previous prompt's id to continue the same conversation. Must belong to the same API key. |
Returns: 202 with:
{
"data": {
"id": 42,
"conversation_id": "a1b2c3d4-...",
"status": "queued",
"poll_url": "/_studio/api/agent/v1/prompt/42"
}
}
GET /prompt/:id - poll
Returns: 200 with prompt status and - for terminal statuses - result details:
| Field | Type | Present | Description |
|---|---|---|---|
id |
integer | always | Prompt log ID |
conversation_id |
string | always | UUID of the conversation |
action_type |
string | always | The action type (free_prompt, etc.) |
prompt |
string | always | The original prompt text |
status |
string | always | queued, streaming, success, partial, or error |
status_message |
string | always | Human-readable status (e.g. "Waiting for worker...", "Generating...") |
ai_provider |
string | always | AI provider used (e.g. claude, openai) |
ai_model |
string | always | Model used (e.g. claude-sonnet-4-20250514). Shows pending while queued. |
created_at |
string | always | ISO timestamp of when the prompt was queued |
files_modified |
array | terminal | List of file paths modified by the AI |
tokens |
object | terminal | { input: int, output: int } - token counts |
cost_estimate |
float|null | terminal | Estimated cost in USD |
duration_ms |
integer|null | terminal | Execution time in milliseconds |
error_message |
string|null | terminal | Error details (for error or partial status) |
"terminal" means the field is only present when status is
success,partial, orerror.
Status lifecycle
| Status | Meaning |
|---|---|
queued |
Prompt is waiting for the background worker to start |
streaming |
The AI is actively generating - worker is running |
success |
Prompt completed successfully - check files_modified and tokens |
partial |
Incomplete execution, including usable output that reached the model's limit - check files_modified and error_message |
error |
Prompt failed, including an empty response or an output limit reached before usable changes - check error_message |
Stale detection:
- A prompt stuck in
queuedfor 60 seconds (worker failed to spawn) is markederror - A prompt stuck in
streamingfor 300 seconds (worker died) is markedpartial
Error codes
| HTTP | Code | When |
|---|---|---|
422 |
validation_error |
Empty or missing prompt, invalid action_type, or invalid action_data |
403 |
forbidden |
continue_from references a prompt from a different API key |
404 |
not_found |
Prompt ID doesn't exist or belongs to another key |
503 |
exec_unavailable |
exec() is disabled on this server |
503 |
php_cli_unavailable |
No working PHP command-line executable could be verified; no conversation or job was created |
Scope and isolation
- Shared private draft - prompts use the same private website files as Studio. Review the changed-file list; page scope is a context hint.
- Per-key isolation -
GET /prompt/:idreturns 404 for prompts created by a different key.continue_fromenforces the same key.
Response format
Successful REST responses use a data envelope. Errors use error.code, error.message and optional error.details. Mutation responses can include a retained operation even when further work or recovery is required.
| Status or code | What to do |
|---|---|
| HTTP 401 | Check the credential and Authorization forwarding |
HTTP 403 / permission_denied |
Check current account, role, audience and scopes |
credential_migration_required |
Ask the owner to bind the REST credential to its intended account |
invalid_schema |
Use the installed contract and required version/identity fields |
content_conflict |
Read current source and prepare a new intentional request |
idempotency_conflict |
Do not reuse an identity for different input |
operation_pending |
Inspect or continue the retained operation |
recovery_required |
Follow recovery before another website write |
| HTTP 429 | Respect the returned rate-limit reset |
host_capability_unavailable |
Check the host requirement reported in the error |
Tool calling & schema
GET /schema is public and describes the installed REST routes. Keep the HTTP method, path, parameters and request body together when mapping an operation into an integration. Studio MCP uses its own tool discovery and schema; the two protocols are not interchangeable.
The schema’s info.x-capabilities.prompt_execution.available flag checks exec(). PHP CLI is checked when a prompt is submitted. Do not infer prompt readiness from that flag alone.
Scopes reference
| Scope | Grants |
|---|---|
pages:read, pages:write |
Draft page reads and writes |
assets:read, assets:write |
Private asset reads and uploads |
compile:trigger |
Draft Tailwind compilation |
publish:trigger |
Explicit publication |
settings:read, settings:write |
Safe reads and allowed versioned settings changes |
submissions:read |
Business inbox reads |
tools:invoke |
Business tool discovery and invocation within account permissions |
prompt:execute |
Opt-in provider-backed prompt jobs |
CORS origins
The owner can restrict browser origins under Settings → API Access → Allowed Origins, one origin per line. * permits all origins. CORS is a browser policy; it does not replace credential or scope checks.
Nginx configuration
Forward Authorization to PHP-FPM:
fastcgi_param HTTP_AUTHORIZATION $http_authorization;
Use the Nginx guide for private-directory protection and API rewrites. If the Studio API router already receives /agent/v1/ requests, it forwards them to the REST router. A generic PHP host still needs its web-server routes configured.
Read-only example
import os
import requests
response = requests.get(
"https://example.com/_studio/api/agent/v1/pages",
headers={"Authorization": f"Bearer {os.environ['VOXELSITE_API_KEY']}"},
params={"page": 1, "per_page": 50},
timeout=30,
)
response.raise_for_status()
print(response.json())
Inspect errors and versions before creating a later write request. Store credentials outside source files.
Logging & debugging
Use Agent API logs to trace dispatched requests and authentication failures. Download them through Settings when you lack shell access.
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
Found a mistake in these instructions? Open an issue.
Download VoxelSite