VerSketch Developers
Agent API v0
A REST surface at /api/agent/v0 that lets an external agent read a plan, validate work through the same gates the editor uses, and propose changes. The write policy is proposal_only: an agent may propose, never commit. Every proposal waits as a ghost overlay until a human accepts it on the canvas, so nothing on this surface can overwrite or delete a user's work. Everything speaks JSON.
Get a token. A signed-in user can now issue a one-time token pair for their local agent at
Connect an agent. The token grants read, validate, and propose access; every proposal still needs a human to accept it in VerSketch. Each connection is its own AIVerID grant (hub Phase B1): revoking one agent cuts only that agent — other agents and the web session stay signed in. The hub's Connected Agents dashboard screen is on its way. The
BYO prompt pack remains the zero-auth on-ramp.
One pair, one agent instance. Do not copy a token pair to two machines. Their refreshes can collide outside AIVerID's 30-second grace window and the hub cuts off the token family of that agent — other agents and the web session are unaffected. Connect the second machine again at
Connect an agent to issue its own pair.
Refreshing. Access tokens live one hour. Renew by POSTing exactly
{"grant_type":"refresh_token","refresh_token":"..."} (no other keys) to
/api/agent/token/refresh; the response is AIVerID's token JSON passed through verbatim, and each refresh rotates the pair — always store the new one. Raw REST clients should follow that flow directly. MCP users can set both
VERSKETCH_TOKEN and
VERSKETCH_REFRESH_TOKEN; the
versketch-mcp shim refreshes automatically and stores the rotated pair in
~/.versketch/agent-token.json (or
VERSKETCH_TOKEN_STORE), using a local lock to avoid a same-machine rotation collision. A
400 invalid_grant carries
"hint":"reconnect": the family is revoked, so send the user back to
Connect an agent instead of retrying.
Endpoints
The table below renders from the same declaration GET /api/agent/v0/capabilities serves, so it cannot drift from what the API declares about itself.
GET/api/agent/v0/capabilitiesno auth
This document: endpoints, required scopes, export formats.
- No authentication. Cached for 5 minutes (Cache-Control: public, max-age=300).
200 — the machine-readable version of this page:
{
"version": "v0",
"base": "/api/agent/v0",
"auth": { "type": "oauth2_introspection", "issuer": "AIVerID", ... },
"writePolicy": "proposal_only",
"endpoints": [ { "method", "path", "scope", "description", "annotations" }, ... ],
"exportFormats": ["versketch.json", "dxf", "ifc", "boq.csv"],
"unavailableFormats": ["gltf", "step", "dae", "usdz", "pdf"],
"exportAccess": { "freeFormats": ["versketch.json"], "paidFormats": [...], ... },
"errorShape": "{ \"error\": string, \"message\": string }",
"rateLimits": {
"perSubject": "120/minute",
"validate": "30/minute",
"propose": "10/minute",
"ifcExport": "4/minute",
"ifcConcurrentPerSubject": 1,
"ifcConcurrentGlobal": 4,
"ifcMaxExecutionSeconds": 240, ...
}
}GET/api/agent/v0/projectsversketch:read
List projects owned by the token subject.
- Lists the projects owned by the token subject. No parameters.
200
{ "projects": [ { "id": string, "name": string, "created_at": string, "updated_at": string } ] }- 503 db_error — project store unavailable; retry shortly.
GET/api/agent/v0/projects/:idversketch:read
Full PlanSpec + revision of one owned project.
- Returns the full PlanSpec and the current document revision.
200
{ "project": { "id", "name", "created_at", "updated_at" }, "spec": PlanSpec, "revision": number }- 404 not_found — no such project, or the token subject does not own it (ownership is never revealed as 403).
- 503 db_error — store unavailable.
GET/api/agent/v0/projects/:id/working-set?query=versketch:read
Bounded working set in the exact shape the patch prompt consumes (64KB cap).
- Optional ?query= (at most 4,000 characters) focuses the working set on the elements relevant to that request.
- Returns the bounded context the AI patch prompt consumes (64 KB cap), so an agent sees exactly what a model would see.
200
{
"mode": "full" | "compacted",
"text": string,
"editableIds": string[],
"includedElements": number,
"totalElements": number,
"contextJsonLength": number,
"caps": { "full": number, "compact": number },
"revision": number
}- 400 query_too_long — the query exceeds 4,000 characters.
- 404 not_found — unknown or unowned project.
- 503 db_error — store unavailable or the stored spec could not be read.
POST/api/agent/v0/validateversketch:validate
Stateless document validation: {spec} or {spec, patch} → draft/deliverable/completeness verdicts. The verdict covers those document gates, not a building-code result; code-check runs only with an explicit body.region or spec.meta.regionHint.
- Body: { "spec": PlanSpec } or { "spec": PlanSpec, "patch": PlanPatch }, optional "region" (US, CA, UK, EU, AU, TH) — 8 MB ceiling.
- Stateless: nothing is stored; the verdict depends only on the body.
- A broken spec is a 200 with a failing verdict — "your spec is invalid" is the answer, never a 500. Only a malformed envelope is a 400.
- The code-check summary runs only when the spec is draft-valid and a jurisdiction is known (explicit "region" or a persisted spec.meta.regionHint). VerSketch never guesses a jurisdiction.
200
{
"verdict": "pass" | "fail",
"gates": {
"draft": { "ok": boolean, "errors": string[] },
"deliverable": { "ok", "errors" } | null, // null when an earlier gate failed
"completeness": { "ok", "errors" } | null
},
"patch": { "requested": boolean, "applied": boolean, "error": string | null },
"codeCheck": {
"region", "regionSupported", "codeAuthority",
"summary": { "errors", "warnings", "info", "total" },
"levels": [ { "levelId", "skippedReason": "pool" | null, "skipIneligibilityReason": "pool_has_spaces" | "pool_has_openings" | null, "errors", "warnings", "info", "total" } ],
"disclaimer": string
} | null,
"codeCheckSkippedReason": "region_required" | "draft_invalid" | null,
"validatedSpec": PlanSpec | null // the merged spec when a patch applied
}- 400 bad_body / bad_json — the envelope is not { spec } or { spec, patch }.
- 400 bad_region — region is not one of US, CA, UK, EU, AU, TH.
- 400 spec_too_deep / spec_too_large / spec_too_complex — JSON or polygon budget exceeded.
- 413 payload_too_large — body exceeds the 8 MB ceiling.
GET/api/agent/v0/projects/:id/boq?format=json|csvversketch:read
Bill of quantities derived from the PlanSpec. Requires a plan with export (402 export_not_included otherwise).
- Optional ?format=json|csv (default json). The bill of quantities is derived from the stored PlanSpec — the same numbers the in-app BOQ panel shows.
- Requires a plan with export: the quantities are the export, so this endpoint enforces the same entitlement as /export.
200 (json) { "items": VbmBoqItem[], "revision": number }
200 (csv) { "csv": string, "revision": number }- 400 bad_format — format is not "json" or "csv".
- 402 export_not_included — the subject plan has no export; the body carries "tier".
- 404 not_found — unknown or unowned project.
- 422 export_invalid — a specific element cannot be quantified; the message names it.
- 503 entitlement_unavailable — the plan could not be confirmed (Retry-After: 30). 503 db_error — store unavailable.
GET/api/agent/v0/projects/:id/code-check?region=versketch:read
Informational code-check report per level (with the legal disclaimer). Requires ?region= or a persisted spec.meta.regionHint; VerSketch does not guess a jurisdiction.
- Jurisdiction is required: pass ?region=US|CA|UK|EU|AU|TH or persist spec.meta.regionHint. Neither present is a 400 — building rules are never guessed.
- Informational report per level, with the mandatory legal disclaimer. A pool is returned with skippedReason: "pool" only when it has neither rooms nor door/window openings; otherwise skipIneligibilityReason names why building-room checks still run. AI flags, a licensed professional verifies, the authority having jurisdiction approves.
200
{
"region", "regionSupported", "codeAuthority", "supportedRegions",
"levels": [ {
"levelId", "skippedReason": "pool" | null, "skipIneligibilityReason": "pool_has_spaces" | "pool_has_openings" | null, "violations": CodeViolation[],
"summary": { "errors", "warnings", "info", "total" },
"roomCoverage": "none" | "partial" | "complete",
"roomDetectionTruncated": boolean
} ],
"summary": { "errors", "warnings", "info", "total" },
"disclaimer": string,
"revision": number
}- 400 region_required — no ?region= and no persisted spec.meta.regionHint.
- 400 bad_region — unsupported region value.
- 404 not_found — unknown or unowned project.
- 503 db_error — store unavailable or the engine failed.
GET/api/agent/v0/projects/:id/export?format=versketch.json|dxf|ifc|boq.csvversketch:read
Server-side export. versketch.json is the free user-owned project file; dxf, ifc, and boq.csv require a plan with export (402 export_not_included otherwise). Mesh-kernel formats (gltf/step/dae/usdz/pdf) answer 501.
- Optional ?format=versketch.json|dxf|ifc|boq.csv (default versketch.json). DXF also accepts ?level= (defaults to the first level).
- versketch.json is the free, user-owned project file on every plan. dxf, ifc and boq.csv require a plan with export.
- Mesh-kernel formats (gltf, step, dae, usdz, pdf) need the browser kernel and answer 501 in v0 — export those from the VerSketch UI.
200 (versketch.json) { "format": "versketch.json", "spec": PlanSpec, "revision": number }
200 (dxf | boq.csv) { "format", "content": string, "contentType", "skipped": SkippedElement[], "revision" }
200 (ifc) { "format": "ifc", "content": base64 string, "contentType": "application/x-step", "skipped", "revision" }- 400 bad_format — unknown format. 400 no_levels / bad_level — DXF level selection failed.
- 402 export_not_included — paid format on a plan without export; the body carries "tier".
- 404 not_found — unknown or unowned project.
- 422 export_invalid — a specific element cannot be exported; the message names it.
- 501 format_not_available_v0 — mesh-kernel format; use the UI export.
- 503 entitlement_unavailable (Retry-After: 30) / db_error.
POST/api/agent/v0/projects/:id/proposalsversketch:propose
Submit {patch, baseRevision, summary?} against an owned project. Validated through the same gates as the editor, then held as a pending proposal for a human to accept on the canvas. Never modifies the plan.
- Body: { "patch": PlanPatch, "baseRevision": integer, "summary"?: string } — 8 MB ceiling, summary at most 2,000 characters.
- "baseRevision" must equal the current server revision (read it from GET /projects/:id); a mismatch is a 409 and the agent re-reads and re-proposes.
- The patch is applied with the same grammar the in-app AI rail uses, and the merged spec must pass every validation gate before the proposal is stored.
- A proposal never touches the document. It waits as a ghost overlay until a human accepts or rejects it on the canvas. At most 10 pending proposals per project.
201
{ "proposalId": string, "status": "pending", "baseRevision": number }- 400 bad_json / bad_body — unparseable body, missing patch, or a non-integer baseRevision.
- 400 proposal_invalid — the patch failed to apply, or the merged spec failed a gate; the message carries the per-gate errors.
- 400 spec_too_deep / spec_too_large / spec_too_complex — JSON or polygon budget exceeded.
- 409 revision_conflict — the document moved past baseRevision.
- 409 too_many_pending — the project already holds 10 pending proposals.
- 413 body_too_large — body exceeds the 8 MB ceiling.
- 404 not_found / 503 db_error.
GET/api/agent/v0/projects/:id/proposalsversketch:read
List this subject's proposals on a project with their status.
- Lists this subject’s proposals on the project — metadata and status, without the spec payloads.
200
{ "proposals": [ {
"id", "project_id", "aiverid", "base_revision", "summary",
"status": "pending" | "accepted" | "rejected" | "superseded" | "expired",
"created_at", "resolved_at", "resolved_revision"
} ] }- 404 not_found — unknown or unowned project.
- 503 db_error — store unavailable.
GET/api/agent/v0/projects/:id/proposals/:pidversketch:read
One proposal: status, base and proposed specs, resolved revision once accepted.
- One proposal with its base and proposed specs — this is how an agent learns whether the human accepted its work and at which revision it landed (resolved_revision).
- "expired" means the pending proposal aged out before anyone resolved it; nothing was merged.
200
{ ...proposal row as above, "base_spec": PlanSpec, "proposal_spec": PlanSpec }- 404 not_found — unknown project or proposal.
- 503 db_error — store unavailable.
POST/api/agent/v0/projectsversketch:propose
Create an empty project and attach {name, spec, summary?} to it as its first pending proposal. The plan itself lands only when a human accepts.
- Body: { "name": string, "spec": PlanSpec, "summary"?: string } — 8 MB ceiling.
- "name" is required, at most 200 characters. "summary" is optional, at most 2,000 characters.
- The spec runs the full gate chain (draft structure, deliverable, completeness) before anything is stored.
- Creates an empty project and attaches the spec as its first pending proposal. The plan itself lands only when a human accepts it on the canvas.
201
{ "projectId": string, "proposalId": string }- 400 bad_json / bad_body — unparseable body, missing name or spec, or an invalid summary.
- 400 spec_too_deep / spec_too_large / spec_too_complex — JSON or polygon budget exceeded.
- 400 proposal_invalid — the spec failed to parse or failed a validation gate; the message carries the per-gate errors.
- 409 project_limit — the plan tier project ceiling is reached.
- 413 body_too_large — body exceeds the 8 MB ceiling.
- 503 db_error — store unavailable.
Worked examples
One request per scope tier, with placeholder tokens. Shapes match the responses documented above.
# No token needed — discover the surface
curl https://versketch.xyz/api/agent/v0/capabilities
# versketch:read — list the token subject's projects
curl -H "Authorization: Bearer $AIVERID_TOKEN" \
https://versketch.xyz/api/agent/v0/projects
# → 200
# { "projects": [ { "id": "8b1f…", "name": "Pool villa",
# "created_at": "2026-08-01T09:30:00Z", "updated_at": "2026-08-14T17:02:11Z" } ] }# versketch:validate — gate a spec without storing anything
curl -X POST https://versketch.xyz/api/agent/v0/validate \
-H "Authorization: Bearer $AIVERID_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "spec": { /* your PlanSpec — the "spec" payload of a .versketch.json file */ }, "region": "US" }'
# → 200 (abridged)
# { "verdict": "pass",
# "gates": { "draft": { "ok": true, "errors": [] },
# "deliverable": { "ok": true, "errors": [] },
# "completeness": { "ok": true, "errors": [] } },
# "patch": { "requested": false, "applied": false, "error": null },
# "codeCheck": { "region": "US", "summary": { "errors": 0, "warnings": 2, "info": 5, "total": 7 }, … },
# "codeCheckSkippedReason": null,
# "validatedSpec": null }# versketch:propose — submit a patch for a human to accept
curl -X POST https://versketch.xyz/api/agent/v0/projects/$PROJECT_ID/proposals \
-H "Authorization: Bearer $AIVERID_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "patch": { /* plan patch */ }, "baseRevision": 12,
"summary": "Widen the entry door to 900 mm" }'
# → 201
# { "proposalId": "6f0a…", "status": "pending", "baseRevision": 12 }
#
# The change now waits as a ghost on the owner's canvas. Poll
# GET /projects/$PROJECT_ID/proposals/6f0a… to see whether it was accepted.