VerSketch

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; theversketch-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.

Connect from Claude / ChatGPT

Add a remote MCP server at https://versketch.xyz/api/agent/mcp. The connection uses your AIVerID account and always keeps VerSketch's proposal-only write policy: an agent may propose, never commit.

  • Claude: client ID aiv_claude; leave the client secret blank.
  • ChatGPT: client ID aiv_chatgpt; leave the client secret blank.

If the host asks for scopes, request profile email versketch:read versketch:validate versketch:propose. The MCP server advertises all tools, but enforces each tool's required scope when it is called.

MCP validates each tool's envelope first. Envelope-shape errors use JSON-RPC -32602; once an input reaches a VerSketch core, its success or error body is the same REST body verbatim.

Authentication

Send an AIVerID access token on every request except GET /capabilities:

Authorization: Bearer <AIVerID access token>

VerSketch is introspection-based: each token is verified against the AIVerID hub per request, and VerSketch keeps no token store of its own. Authorization uses exact scope membership — the token's space-delimited scope string must contain the scope an endpoint requires.

  • versketch:read — read projects, working sets, BOQ, code-check reports and exports.
  • versketch:validate — stateless spec/patch validation.
  • versketch:propose — create proposals, and create new projects whose first content is a proposal.
  • Missing or malformed Authorization header → 401 invalid_token with WWW-Authenticate: Bearer (no error attribute, per RFC 6750 §3).
  • A presented token that is missing, revoked, expired, or not issued to VerSketch → 401 invalid_token with WWW-Authenticate: Bearer error="invalid_token".
  • A valid token without the required scope → 403 insufficient_scope with WWW-Authenticate: Bearer error="insufficient_scope", scope="…".

Agent requests never consume one of the account's device seats — the device ceiling applies to browser sessions only.

Conventions

  • One error shape across the whole surface: { "error": string, "message": string }, where error is the machine code. The 402 export_not_included refusal additionally carries tier, the plan the token subject is on.
  • Ownership is private: a project the token subject does not own answers 404 not_found, never 403 — the API does not reveal that someone else's project exists.
  • Request bodies are capped at 8 MB (413), JSON at 64 nesting levels and 200,000 nodes, polygons at 1,000 points each and 20,000 points per spec (400 spec_too_deep / spec_too_large / spec_too_complex).
  • Rate limiting is fail-closed: if the limiter itself is unavailable the request is refused with 429 rather than waved through. Every 429 carries a Retry-After header (seconds).

Rate limits

  • 240/minute per IP on every authenticated endpoint, consumed before token introspection.
  • 120/minute per token subject across the whole surface.
  • 30/minute per subject for POST /validate.
  • 10/minute per subject for the versketch:propose endpoints.
  • 4/minute per subject for server-side IFC export; concurrency is capped at 1 active job per subject and 4 globally.

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.

Zero-auth today: the BYO prompt pack

You do not need a token to put an external model to work. Inside the app — in the project gallery's import dialog and in the editor — the "use your own AI" button copies a prompt pack: the same system prompt and plan briefs the managed pipeline sends. Paste it into any assistant, then paste the JSON it returns back into VerSketch. The pasted plan passes through exactly the same validation gates as everything else on this page, and appears as a proposal for you to accept. No account is required to try it.