Developer Documentation
PrecisionDocs public API reference: authentication, project lifecycle, agent questions, knowledge-base search, reports, MCP connectors, errors, and rate limits.
API Overview
What the PrecisionDocs public API is, the base URL, and the three ways to connect.
What is the PrecisionDocs public API?
The PrecisionDocs public API lets your own tools, agents, and scripts drive the same project workflow the app uses: create a project for a site, wait for the knowledge base to build, ask the project agent cited questions, search indexed sources, generate reports, and download artifacts.
All REST endpoints live under https://api.precisiondocs.ai/v1.
Two access surfaces, one API:
| Surface | How it connects | Best for |
|---|---|---|
| Remote MCP | https://mcp.precisiondocs.ai/mcp - PrecisionDocs OAuth, no key | Claude Code, OpenAI Codex, Gemini CLI, Cursor, the Claude app, any MCP client |
| REST | HTTPS + Authorization: Bearer pd_live_... header | Scripts, backends, CI, ChatGPT Actions |
The machine-readable spec is at https://api.precisiondocs.ai/v1/openapi.json.
For copy-paste connector setup per client, use Account -> Integrations or the Help Center. This page is the endpoint-level reference.
Creating a project from an agent (wizard parity). A connected agent walks the same steps a person does in the app: POST /v1/parcels/resolve for each address or APN (candidates carry ll_uuid, address, size, confidence and GeoJSON geometry), POST /v1/parcels/preview with the chosen geometries to show the user the highlighted parcels, POST /v1/plan-check to learn which goal questions still need asking, then POST /v1/projects with dry_run: true and setup_mode: "wizard". The dry-run response is the pre-Create screen: estimate, prep_summary.sources (every governing source discovery found, with status and URL) and gaps - each source discovery could not settle, naming the request field that fills it (ordinance_url, county_ordinance_url, pending_gis_sources, or a skip_* flag the user must confirm). A confirmation_required response is the wizard asking a question; answer it by re-submitting with the field its options map to. Then create for real with an Idempotency-Key and poll GET /v1/projects/{id} until chat_ready.
Authentication & API Keys
Bearer pd_live keys, where to create them, scopes, and key-handling rules.
How do I authenticate REST requests?
Every REST request sends an organization API key as a Bearer token:
Authorization: Bearer pd_live_...Create keys in Account -> API keys. Organization owners and active members can create keys; owner-created keys are org-wide, while member-created keys only authorize projects the member owns or is explicitly shared into. A connector (OAuth) session is always scoped to one person: it sees only projects that user owns or is shared into, whatever their role.
Key facts:
- Keys are shown once at creation. Copy the full
pd_live_secret while it is visible — only a hashed form is stored. - Keys can carry an optional expiry and optional daily/monthly credit caps.
- Rotate or revoke keys anytime from Account -> API keys.
- Test a key with
GET https://api.precisiondocs.ai/v1/wallet.
What scopes do API keys have?
Each key carries a scope list. Requests to an endpoint outside the key's scopes fail with 403 insufficient_scope.
| Scope | Grants | Default |
|---|---|---|
project:create | Create projects (POST /v1/projects) and run plan checks (POST /v1/plan-check) | Yes |
project:read | List/poll projects, site knowledge, messages, reports, search | Yes |
agent:ask | Ask the project agent (POST /v1/projects/{id}/messages) | Yes |
report:write | Generate reports (POST /v1/projects/{id}/reports) | Yes |
source:write | Add URL and GIS sources and upload files (POST .../sources, POST .../gis-sources, POST /v1/uploads/*) | Yes |
artifact:read | List artifacts and mint one-time download links | Yes |
wallet:read | Read the organization credit balance | Yes |
gis:export | GIS export artifacts (POST .../gis-exports) and map/aerial rendering (POST .../maps) | Yes |
siteplan:read | Site Plan Studio design data: concept geometry, design criteria, yield scenarios, CAD exports, proposals | Yes |
siteplan:write | Approve the development program, propose and decide concept proposals (POST .../site-plan/program/approve, POST .../site-plan/proposals, POST .../site-plan/proposals/{id}/approve or /reject). Also needs Site Plan Studio enabled for the organization | Yes |
There is no custom-scope picker: every new key carries the full set above, and a connector (OAuth) token carries the same set. A credential keeps the scopes it was minted with - they are never backfilled - so a key or connected app created before siteplan:write existed (September 2026) does not have it: mint a new key, or reconnect the app from its client, to pick it up.
Security rules:
- Never paste a
pd_live_key into a shared document, prompt, chat message, screenshot, or support ticket. - Put keys only in server-side config, sent as the
Authorizationheader. Never in a URL, and never in an MCP client config - MCP clients sign in with OAuth and hold no key. - Rotate a key immediately if it may have been exposed.
Projects Lifecycle
Create a project, poll readiness, and verify parcel zoning over REST.
How do I create and manage projects?
| Endpoint | What it does | Rate limit |
|---|---|---|
POST /v1/projects | Create a project (returns 202 — bootstrap runs async) | 8/min |
GET /v1/projects | List your projects, a page at a time: limit (1-100) and cursor, answered as data and next_cursor (null on the last page) | 60/min |
GET /v1/projects/{project_id} | Poll readiness + knowledge-base summary | 60/min |
PATCH /v1/projects/{project_id}/parcels/{parcel_index}/zoning | Verify or correct a parcel zoning designation | 30/min |
Creating a project (POST /v1/projects):
dry_run: truereturns a setup and cost estimate without creating anything.setup_mode: "wizard"(default) runs the same source-discovery gates as the in-app New Project wizard;setup_mode: "provided"preserves caller-supplied parcels and source decisions.- When the platform needs a decision (ambiguous parcels, source choices, duplicates), the API responds
422withconfirmation_requiredand the choices to present. Re-submit with the selections applied. - Send an
Idempotency-Keyheader so a retried create never makes a duplicate project.
Polling readiness (GET /v1/projects/{project_id}):
- Poll every 5–15 seconds until
chat_readyistrue. - The response carries a
readinessblock with per-target ordinance status, GIS layer states, supplemental source lanes, and advisories - use it to see exactly which lane is still open. - A ready bootstrap can still hold
chat_ready: falsewithzoning_verification_requireduntil every parcel's zoning is verified — confirm or correct each parcel via the zoning PATCH, then re-poll.
Plan Check, Sources & Uploads
Classify a goal before creation, then attach URL sources, GIS sources, and file uploads to a project.
How do I classify a goal and attach sources to a project?
All five endpoints require the source:write scope except plan check, which uses project:create. Source and upload writes accept an Idempotency-Key header so a retry never double-ingests.
| Endpoint | What it does | Scope | Rate limit |
|---|---|---|---|
POST /v1/plan-check | Classifies goal text into a typology and reports which follow-up values the goal already answers | project:create | 20/min |
POST /v1/projects/{project_id}/sources | Adds a URL source; the response names the lane that accepted it (document, supplemental ordinance, or GIS) | source:write | 10/min |
POST /v1/projects/{project_id}/gis-sources | Registers a user-verified GIS source by authority scope | source:write | 10/min |
POST /v1/uploads/initiate | Starts a file-upload job for a local document | source:write | 30/min |
POST /v1/uploads/{job_id}/finalize | Finalizes an initiated upload into the project knowledge base | source:write | 30/min |
Plan check (POST /v1/plan-check): send { goal_text, primary_typology?, has_existing_structure? }. The verdicts in the response tell your agent which follow-up values the goal already settled and which come back as ask. Ask the user only the ask items, then pass the settled answers as project_goal_context on POST /v1/projects.
URL sources (POST .../sources): send { url, title?, category? }. The response reports which ingest lane accepted the URL - a project document, a supplemental ordinance corpus, or a GIS source - so your client can say what will happen next.
GIS sources (POST .../gis-sources): send { url, scope, jurisdiction_name? } where scope is municipality, county, or state_reference. Use it for a viewer or ArcGIS REST endpoint the jurisdiction publishes that discovery did not find.
File uploads: local files never travel as URLs. Call POST /v1/uploads/initiate, PUT the raw bytes to the returned upload_url with the file's Content-Type, then call POST /v1/uploads/{job_id}/finalize. The upload_url is a one-time PrecisionDocs link on api.precisiondocs.ai (it works once and expires after 15 minutes; the storage URL behind it never leaves the server); if the PUT fails or finalize answers upload_object_missing, initiate again with the same body and Idempotency-Key for a fresh link to the same job, PUT the bytes, then finalize. Over MCP the same two steps are precisiondocs_start_upload and precisiondocs_finish_upload, which need a client that can read the file and make an HTTP request (Claude Code, Codex).
Sources at creation: POST /v1/projects also accepts pending_supplemental_sources (max 10: url, title, category, authority_scope) and pending_gis_sources (max 6: url, scope, jurisdiction_name) so a connector can hand over everything the user confirmed in one create call.
Ask the Agent
Agent questions with model choice, citations and recoverable turn history.
How do I ask the project agent a question?
POST /v1/projects/{project_id}/messages runs the same ChatAgent the app uses. Send wait: false to receive a pollable job handle, or use the default wait: true for a synchronous answer with citations, tool usage and generated artifacts.
Limits and behavior:
- Rate limit: 2/min per key, with a concurrency limit of 2 in-flight questions.
- Send an
Idempotency-Keyheader — a retried request returns the original turn instead of re-running (and re-billing) the agent. - Before the project is ready you get
409withbootstrap_not_ready; when parcels still need zoning confirmation you get409withzoning_verification_required. PollGET /v1/projects/{project_id}and retry.
Model selection: Read GET /v1/models for the same approved models, Auto default and estimated credit rates the app offers (requires agent:ask; no inference or charge). Pass an exact returned model_id with your message, or omit it, send null, or send auto to use Auto. Each API request defaults to Auto on its own.
A rejected, unavailable or disabled explicit selection fails before the turn starts. The answering model can be selected; report generation, classifiers and other tools retain their own task-specific models. Finished responses, and a turn read back by id, expose model_selection with requested/resolved IDs and observed served identity when available.
Reading turns back:
| Endpoint | What it returns |
|---|---|
GET /v1/projects/{project_id}/messages/{turn_id} | One prior turn (question, answer, citations, artifacts) |
GET /v1/projects/{project_id}/messages | Turn list for the project - API-surface turns only, not in-app chat history; data a page at a time (limit, cursor, next_cursor) plus running_turns |
Site Knowledge & Search
Read the structured site digest for free, or run hybrid knowledge-base search.
How do I read structured site knowledge?
GET /v1/projects/{project_id}/site-knowledge returns the structured, geometry-free digest of everything the foundation pipeline collected for the site: parcels and zoning, flood and wetlands screening, soils, terrain, utilities, roads, regulatory layers, and source readiness.
- It is a plain read — no credits are debited.
- Use it to hydrate your own tools without asking the agent, or to decide which agent questions are worth a metered turn.
- A large site's digest can run to ~100 KB. Add
?sections=zoning,floodto return only the parts you need — every response lists the valid names insections_available, an unknown name is refused with a 422 that names them, and omitting the parameter returns the whole digest. - Read
sections_returnedbefore treating an empty section as evidence: a section you did not request comes back at its default (nullor[]), which is not a claim that the site has none. - Geometry (GeoJSON/shapefiles) is not included; use the in-app GIS export (or the
gis:exportscope, which every key carries by default) for geometry deliverables.
How does knowledge-base search work?
POST /v1/projects/{project_id}/search runs the same hybrid (dense + BM25, reranked) search the agent uses over the project knowledge base — indexed ordinances plus your uploaded project documents.
- Returns matching chunks with citations: source document, section, page, and relevance score.
- Flat cost: 2 credits per call, regardless of result count.
- Rate limit: 30/min. Requires the project to be
chat_ready.
Use search when you want raw evidence to feed your own pipeline; use POST .../messages when you want a synthesized, cited answer.
How do summaries and GIS exports work?
| Endpoint | What it does | Scope | Rate limit |
|---|---|---|---|
POST /v1/projects/{project_id}/summaries | Creates a constrained text-only summary from geometry-free site knowledge plus top KB chunks | agent:ask | 5/min |
POST /v1/projects/{project_id}/gis-exports | Creates a stored GIS export artifact and returns a one-time download link | gis:export | 10/min |
GET /v1/projects/{project_id}/gis-exports/{artifact_id} | Mints a fresh one-time download link for a prior GIS export | gis:export | 30/min |
POST /v1/projects/{project_id}/maps | Renders a site-map exhibit PDF or a georeferenced aerial snapshot, or hands back the official FEMA flood map, the USFWS wetlands map or the 3DEP contour sheet recorded for the project, with a one-time download link | gis:export | 10/min |
POST /v1/parcels/resolve | The wizard parcel picker: address, APN + state, coordinates, or ll_uuid to ranked candidates with geometry | project:create | 20/min |
POST /v1/parcels/preview | The highlighted map: chosen parcel geometries rendered to a PNG (base64) for the user to confirm before creation; nothing stored | project:create | 10/min |
Summaries: send { focus, format: "paragraph" | "bullets", max_words? }. The endpoint is one constrained writing-model pass, not the full agent loop and not report generation.
GIS exports: send { format: "shapefile" | "kml" | "kmz" | "geojson" | "dxf", layers? }. The response contains artifact_id, filename, mime_type, download_url, expires_at, warnings, and metadata. It never returns raw GeoJSON, ZIP, KML, or DXF bytes inline.
DXF is the CAD handoff format for captured GIS layers. DWG is not supported on this public surface.
Maps: send { kind: "site_map" | "aerial" | "flood" | "wetlands" | "contours", buffer_meters?, latitude?, longitude? }. Omit buffer_meters to fit the project parcels; the response returns the drawn extent as extent_feet.
site_mapreturns a sheet-ready PDF locator exhibit: parcel boundaries over aerial imagery with street labels, scale bar and north arrow.road_labelsstates whether street names actually landed.aerialreturns the bare imagery as PNG plus ageoreferenceblock:crs,bbox_web_mercator,bbox_wgs84,image_size_px, andworld_filetext to save beside the image underworld_file_name. Insert it as a background raster and draw on top.
Both are screening exhibits under a display-only imagery licence. Print the returned attribution, never scale dimensions off them, never present traced linework as survey data, and never pass the imagery to an image or vision model. The response repeats these rules in usage.
floodreturns FEMA's own flood map: the FIRMette PDF, or the printed FIRM panel PNG where FIRMette does not answer.wetlandsreturns the USFWS National Wetlands Inventory map PDF.contoursreturns the parcel's USGS 3DEP contour sheet PDF: labelled index contours over the aerial, from the project's stored contours. A sheet already drawn from the same data is reused.
flood and wetlands are the official products recorded for the project at the centroid of its parcel geometry, the same files a report embeds. These three are framed by their own lane, so sending latitude, longitude or buffer_meters with them is refused with 400 invalid_map_request. extent_feet and parcels_drawn are null where the product does not state them. One not recorded yet is drawn on a worker: while it draws the call answers 202 with { status: "rendering", retry_after_seconds } and a Retry-After header, so call again after that many seconds with the same body and the recorded map is returned; concurrent first calls start one job, not two. The Retry-After header can be longer than the retry_after_seconds in the body, because the rate limiter writes the later of the two, in whole seconds; following either is safe, since a call made early joins the same render and answers 202 again. contours answers 409 map_layer_unavailable when the project has no stored contours and 409 bootstrap_not_ready while its site data is still being built. When the agency does not return a map the answer is a retryable 502 naming it (flood_map_unavailable, wetlands_map_unavailable, with Retry-After); 404 flood_map_not_published means FEMA answered that it prints no FIRMette or FIRM panel for the site, and retrying will not change it.
Every kind except contours needs parcel geometry only, so a map works before ordinance bootstrap finishes; contours needs the project's stored contours. The artifact_id remains downloadable through GET /v1/projects/{project_id}/artifacts/{artifact_id}.
Site Plan & CAD Handoff
Read Site Plan Studio design data, compile a concept from the interview, and export a concept picture or geometry for Civil 3D.
How do I read site-plan design data and export concept geometry?
All four endpoints require the siteplan:read scope. Every key and connector token carries the full scope set by default, so no separate grant step exists.
| Endpoint | What it does | Rate limit |
|---|---|---|
GET /v1/projects/{project_id}/site-plan | Latest compiled concept: object_counts (every object counted by type), metrics and scenario, plus WGS84-only GeoJSON features by default; include_geometry=false returns an empty FeatureCollection | 60/min |
GET /v1/projects/{project_id}/design-criteria | Resolved zoning standards per district with per-field provenance | 60/min |
GET /v1/projects/{project_id}/yield-scenario | Settled yield scenarios per district plus snapshot summaries | 60/min |
POST /v1/projects/{project_id}/site-plan-exports | Renders the concept to a picture (concept_png), LandXML, DXF, or GeoJSON and returns a one-time download link | 10/min |
Concept geometry: until a concept compiles in Site Plan Studio, the site-plan route returns has_concept: false with an empty feature collection. Features carry object_type, label, layer, status, editable, and measurements; projected coordinates are stripped, WGS84 only. measurements holds site numbers only - solver bookkeeping (planner strategy, fit ratios, discarded-geometry counters) is excluded, so nothing there should be read as an area or dimension on the ground.
For a summary, pass include_geometry=false: has_concept, revision, object_counts, metrics and scenario still describe the plan, while feature_count is 0 and truncated is false. An empty FeatureCollection in that response does not mean the concept is absent. object_counts counts every plan object, including those beyond the geometry response cap, in descending count order.
Design criteria: absent values are never bare nulls. A field reads Not provided in ordinance only when the extraction corroborated the absence; otherwise it reads Not located in the indexed ordinance, which claims nothing about the law. user_provided_fields lists the values the user themselves confirmed. citations are readable references such as Tangipahoa Parish Code § 36-91; retrieval chunk identifiers are never returned as citations. stormwater_context splits into known and not_in_code, so a requirement the code is silent on is named rather than read as a zero. governing_district is the district the concept is compiled and checked against, with its basis (current, or rezone_target once the user confirmed a rezone in Site Plan Studio) and the parcel district as current_zoning_code beside it; a rezone target is also listed in districts, so never take the parcel district for the governing one.
Yield scenarios: values the user settled under one district are inherited into another district's partition when missing, with the borrowing stated per key in inherited.
CAD handoff: landxml is the native Civil 3D import format; dxf covers generic CAD; concept_png is a picture of the concept plan for people, not CAD. The response is artifact_id, filename, mime_type, download_url, and expires_at (15 minutes); geometry travels as a file, never inline. Send an Idempotency-Key header so a retried request replays the original artifact. With no compiled concept the endpoint returns 404 (no_site_plan_concept).
Concept picture disclosures: the drawing carries recorded wetland exclusion, constraint override and floodplain retention caveats, including suspended decisions. When space is limited, optional notes give way first and a long stated override reason may be shortened with a marker pointing to the full project record; the caveats remain whole. If required disclosures still cannot fit, export returns 400 (invalid_site_plan_export_request) with the refusal message. Retrying the same revision cannot resolve that refusal; transient worker failures remain 502 (export_render_failed).
How do I walk the guided site-planning interview from my own agent?
Three endpoints walk the same interview the PrecisionDocs app walks. The engine is deterministic: it picks the next question and already knows what the project settled, so you never decide what to ask next.
| Endpoint | What it does | Rate limit |
|---|---|---|
GET /v1/projects/{project_id}/interview/workflows | Guided workflows this project can walk, which one it is on, and how far in (siteplan:read) | 60/min |
GET /v1/projects/{project_id}/interview/step | The next 1-3 questions as JSON Schema, plus what is already answered or inferred (siteplan:read) | 60/min |
POST /v1/projects/{project_id}/interview/answers | Submit typed answers, get the next step back (project:create) | 30/min |
Both reads are free. No LLM call and no credit spend, so poll them rather than guessing what comes next. Only the answer endpoint writes.
Questions arrive as JSON Schema mapped from the same control the in-app card renders: types, ranges, units, and options with their labels. A default on a field is the engine's recommendation, and recommended.reason says why - offer it as a suggestion, never as a requirement.
Nothing is asked twice. answered lists what the project settled and inferred lists what the engine derived, each with the statement behind it. remaining_question_ids is the rest of the walk; complete: true with no questions means the scripted interview is finished, and completeness.missing_required names anything a concept still needs.
A rejected batch records nothing. Values are validated against the same schema server-side; a 400 invalid_interview_answer names every bad field under errors, so fix those and resend the same batch. What is recorded is the user's own decision, on the same ledger the in-app card writes - so an interview can be started in the app and finished over the API, or the other way round.
Read standing, not the workflow rows. standing always describes the walk the project is on. selected_workflow_id is null both before a workflow is chosen and for a use that has no template lane (commercial, hotel), so a null there never means the interview has not started.
How do I compile the concept plan once the interview is complete?
When the interview returns complete: true with completeness.ready: true, four endpoints turn the settled program into a drawn concept. They call the same functions the in-app Site Plan Studio calls - one engine, no second compile path - so a walk can start in the app and finish here or the other way round.
| Endpoint | What it does | Rate limit |
|---|---|---|
POST /v1/projects/{project_id}/site-plan/program/approve | Approve the development program and advance the session to Generate Concept; idempotent once approved (siteplan:write) | 30/min |
POST /v1/projects/{project_id}/site-plan/proposals | Create the generate_concept proposal (or a one-zone regeneration with zone_id); returns notes, recap and conflicts, compiles nothing yet (siteplan:write) | 10/min |
GET /v1/projects/{project_id}/site-plan/proposals | Proposals on the active planning session, newest first, with limit/offset and status_filter (siteplan:read) | 60/min |
POST /v1/projects/{project_id}/site-plan/proposals/{proposal_id}/approve | Approve = compile. Returns the applied revision and the plan's scalar metrics (siteplan:write) | 10/min |
POST /v1/projects/{project_id}/site-plan/proposals/{proposal_id}/reject | Reject, recording the optional reason (siteplan:write) | 10/min |
Every write is fenced on the session. Pass the planning_session_id the interview responses carry; a write against a session that is no longer active is 409 planning_session_conflict. state_version is optional and, when given, is a compare-and-swap token against the last response you saw. An accepted proposal write consumes that token even if the later compile blocks or fails, and the proposal-create and decision responses return the session's new state_version - send that one on the next call, and re-read the interview state after a conflict. An already-decided proposal replay consumes nothing. Send an Idempotency-Key on each write so a retry replays rather than repeats.
The writes are gated; the reads are not. Besides siteplan:write, every write requires Site Plan Studio for the organization, which comes with every paid plan (a free workspace gets it by buying credits): without it every write answers 403 site_plan_studio_not_enabled while every read in this section keeps answering 200. Single-family and subdivision plans compile through their own Studio lanes and answer 409 typology_not_supported_via_api.
A flagged proposal needs the user's reason. conflicts on a proposal names the rules the plan would break. Approving one without a reason is 422 proposal_override_reason_required; the reason is recorded in the plan's decision ledger beside the overridden findings. It is the user's own words - a client must never supply it for them.
The compile runs inline. The engine is deterministic and a 16-acre multifamily concept compiles in seconds, so approve answers with applied_revision_id, zone_failures and metrics in one call, and GET .../site-plan reports has_concept: true right after. 409 proposal_expired means the program or plan changed since the proposal - propose again. 409 proposal_blocked names a prerequisite to fix first and leaves the proposal retryable; 409 proposal_failed is a domain rejection of the compile. Re-approving an applied proposal replays (replayed: true) and never compiles twice.
Reports & Artifacts
Generate due-diligence and feasibility reports, then download artifacts through one-time links.
How do I generate and download reports?
| Endpoint | What it does | Rate limit |
|---|---|---|
POST /v1/projects/{project_id}/reports | Start a report job (202 with a job_id) | 5/min |
GET /v1/projects/{project_id}/reports/{job_id} | Poll job status; completed jobs include artifact references | 60/min |
GET /v1/projects/{project_id}/reports | List report jobs for the project | 60/min |
GET /v1/projects/{project_id}/artifacts | List downloadable artifacts (reports, map bundles) | 60/min |
GET /v1/projects/{project_id}/artifacts/{artifact_id} | Mint a fresh one-time download link | 60/min |
GET /v1/downloads/{token} | Stream the file a one-time link names; the link is the credential | 60/min per IP |
Report generation:
report_typeisdue_diligenceorfeasibility.- Generation is asynchronous — poll the job until it is terminal.
- If a matching report job is already queued or running, the API attaches to the in-flight job instead of dispatching (and billing) a duplicate.
Artifacts:
- Every
download_urlis a one-time PrecisionDocs link onapi.precisiondocs.ai: it works once and expires after 15 minutes, and the file streams from our server, so no storage URL, bucket, path or token ever reaches your client. Re-request the artifact endpoint any time you need a fresh link; reminting is free. - Hand your user only these PrecisionDocs links. If your own code downloads the file, mint a fresh link for the user, because the one you used is spent.
Wallet & Billing
How API usage debits the organization credit wallet, and per-key and per-connection caps.
How is API usage billed?
GET /v1/wallet returns the organization's credit balance so your integration can check affordability before metered work.
The credit model:
- Reserved then settled: project creation (
POST /v1/projects) holds an estimate up front, then debits what bootstrap actually used and releases the remainder. - Metered: agent turns (
POST .../messages), summaries (POST .../summaries), and report generation (POST .../reports) debit credits based on the underlying work, exactly like in-app usage. - Flat: knowledge-base search (
POST .../search) costs 2 credits per call. - Free: reads — project polling, site knowledge, message history, report status, artifact listing, wallet checks.
Caps:
- Keys can carry optional daily and monthly credit caps (never mandatory). When a cap or the wallet balance is exhausted, metered endpoints return
402(api_key_credit_cap_exceededorinsufficient_credits). - A chat-agent (MCP) connection can carry the optional daily and monthly credit caps the person chose on the consent page; past one, metered endpoints return
402connection_credit_cap_exceeded, and the way past it is reconnecting with a higher cap. - A key's or connection's caps are read before every operation that spends credits, and count what that key or connection charged to the wallet.
- Owners top up or configure auto-refill from Account -> Billing.
Connector spend lands in the same workspace wallet as in-app work and is labelled API or MCP in usage views. For the customer-facing explanation of what spends credits and why amounts vary, see the Credits & Billing help articles.
Errors & Rate Limits
The error envelope, common error types, per-route limits, and the 202-then-poll pattern.
What does an error response look like?
Every non-2xx response is one RFC 9457 problem document, Content-Type: application/problem+json, whatever refused the call - a route, the scope check, request validation, the rate limiter or an unknown path:
{
"type": "https://precisiondocs.ai/developers/errors#bootstrap_not_ready",
"title": "Conflict",
"status": 409,
"code": "bootstrap_not_ready",
"detail": "Project knowledge base is still building.",
"request_id": "5d0c1c9e-7f1b-4f5e-9a57-2f6d3c1b8e44",
"retry_after_seconds": 15,
"poll": "/v1/projects/{project_id}"
}Branch on code. detail is human copy, request_id matches the X-Request-ID response header (quote it to support), and recovery.action (with recovery.url where there is one) says what to do next when the error has a known recovery. The context of each error rides beside them: required_scope, required_credits and available_credits, period and credit_cap, gaps, and for a request the schema refused, errors[] with each bad field, its code and a message.
A 429 from the per-route rate limiter is the same shape with code: "rate_limited", retry_after_seconds and a Retry-After header. It is always transient - wait the named seconds and retry the same call.
Common error codes:
| Code | Status | Meaning |
|---|---|---|
not_found | 404 | Unknown resource, or a project outside your organization |
insufficient_scope | 403 | Key lacks the scope for this endpoint |
bootstrap_not_ready | 409 | Knowledge base still building — poll and retry |
zoning_verification_required | 409 | Parcels need zoning confirmation before agent work |
goal_version_conflict | 409 | The project goal moved since the user_goal_revision / user_goal_hash you read - re-read GET /v1/projects/{id}, confirm the wording with the user, and send again |
goal_version_required | 428 | PATCH /v1/projects/{id} was sent without expected_goal_revision or expected_goal_hash; a goal is replaced whole, never overwritten blind |
invalid_interview_answer | 400 | One or more interview answers were rejected; nothing was recorded, and errors names each bad field |
site_plan_studio_not_enabled | 403 | Site Plan Studio is not enabled for the organization (it comes with every paid plan); the compile-lane writes are refused while every read still works |
planning_session_conflict | 409 | The planning_session_id is no longer the active session, or the state_version is stale - re-read and retry |
program_incomplete | 409 | The program still has required gaps; gaps names them per zone |
invalid_program | 422 | The program cannot be approved as it stands - a staged parking draft the studio could not confirm |
program_not_approved | 409 | A concept was proposed before the program was approved |
concept_proposal_rejected | 409 | The proposal could not be built as asked - an unknown zone_id, a zone with no sketched boundary, or no existing concept for a zone_id regeneration to start from |
typology_not_supported_via_api | 409 | Single-family and subdivision plans compile only in the in-app Site Plan Studio |
proposal_override_reason_required | 422 | The proposal carries conflicts; approving it needs the user's written reason |
proposal_expired | 409 | The program or plan changed since the proposal was made - propose again |
proposal_blocked | 409 | A prerequisite the compile needs is missing (block says which); the proposal stays retryable |
proposal_failed | 409 | The compile was refused on a domain rule (repair gate, conflict); the proposal is marked failed |
proposal_decided | 409 | The proposal was already approved, rejected or expired |
proposal_execution_failed | 500 | The compile faulted rather than refusing; the proposal is left retryable, so retry the same approve |
idempotency_key_reused | 409 | Same Idempotency-Key sent with a different payload |
idempotency_in_progress | 409 | A request with this Idempotency-Key is still running — retry |
concurrency_limit_exceeded | 429 | Too many concurrent agent turns for this key (limit 2) - retry_after_seconds in the body and a Retry-After header |
house_paid_daily_limit | 429 | This key or connection added 50 URL documents and GIS services today, the daily limit for sources PrecisionDocs processes at its own cost - Retry-After names the wait until 00:00 UTC; supplemental ordinance sources and duplicates are not counted |
insufficient_credits | 402 | Organization wallet is empty |
api_key_credit_cap_exceeded | 402 | This key hit its daily or monthly credit cap |
connection_credit_cap_exceeded | 402 | This chat-agent connection hit a daily or monthly credit cap chosen when it was connected - reconnect with a higher cap |
free_tier_limit | 402 | A free-plan limit: the feature (own documents, sources, GIS registration) is not on the free plan, or its allowance is used up - limit names which; add credits |
search_unavailable | 503 | Search backend temporarily unavailable — retry with backoff |
What are the rate limits?
Limits are per API key. Exceeding one returns 429 with a Retry-After header and a typed body naming the wait (type: "rate_limited" plus retry_after_seconds) - wait and retry the same call. The MCP client puts the wait in the error text as "Retry after Ns".
| Route | Limit |
|---|---|
POST /v1/projects | 8/min |
POST /v1/plan-check | 20/min |
POST .../sources, POST .../gis-sources | 10/min |
POST /v1/uploads/initiate, POST /v1/uploads/{job_id}/finalize | 30/min |
GET /v1/projects, GET /v1/projects/{id} | 60/min |
PATCH .../parcels/{i}/zoning | 30/min |
PATCH /v1/projects/{id} | 30/min |
GET .../site-knowledge | 60/min |
POST .../messages | 2/min (concurrency 2) |
GET .../messages, GET .../messages/{turn_id} | 60/min |
POST .../search | 30/min |
POST .../summaries | 5/min |
POST .../gis-exports | 10/min |
POST .../maps | 10/min |
POST /v1/parcels/resolve | 20/min |
POST /v1/parcels/preview | 10/min |
GET .../site-plan, GET .../design-criteria, GET .../yield-scenario | 60/min |
POST .../site-plan-exports | 10/min |
GET .../interview/workflows, GET .../interview/step | 60/min |
POST .../interview/answers | 30/min |
POST .../site-plan/program/approve | 30/min |
POST .../site-plan/proposals, POST .../site-plan/proposals/{id}/approve, POST .../site-plan/proposals/{id}/reject | 10/min |
GET .../site-plan/proposals | 60/min |
GET .../gis-exports/{artifact_id} | 30/min |
POST .../reports | 5/min |
GET .../reports, GET .../reports/{job_id}, GET .../artifacts | 60/min |
GET .../artifacts/{artifact_id} | 30/min |
GET /v1/wallet | 30/min |
The 202-then-poll pattern: long-running work (project creation, report generation) returns 202 immediately. Poll the corresponding read endpoint every 5–15 seconds until the resource is terminal — never spin faster than the read limits.
MCP & Connectors
Remote and local MCP servers, and how MCP tools map to REST operations.
How do I connect over MCP?
One MCP endpoint wraps the whole API: https://mcp.precisiondocs.ai/mcp, Streamable HTTP with OAuth 2.1 (dynamic client registration, PKCE S256, refresh-token rotation). The client opens your browser, you approve on the PrecisionDocs consent page, and the client holds a token scoped to your user and organization. No pd_live_ key is involved; the endpoint rejects one.
| Client | Setup |
|---|---|
| Claude Code | claude mcp add --scope user --transport http precisiondocs https://mcp.precisiondocs.ai/mcp, then /mcp -> precisiondocs -> Authenticate |
| OpenAI Codex | codex mcp add precisiondocs --url https://mcp.precisiondocs.ai/mcp, then codex mcp login precisiondocs |
| Gemini CLI | gemini mcp add --scope user --transport http precisiondocs https://mcp.precisiondocs.ai/mcp, then /mcp auth precisiondocs (the command writes {"url": "https://mcp.precisiondocs.ai/mcp", "type": "http"} under mcpServers.precisiondocs in settings.json) |
| Cursor | {"mcpServers":{"precisiondocs":{"url":"https://mcp.precisiondocs.ai/mcp"}}} in mcp.json, then sign in when Cursor asks |
| Claude app (claude.ai, Claude Desktop) | Customize -> Connectors -> Add custom connector -> paste the URL -> Add -> Connect (Team and Enterprise: an Owner adds it in Organization settings -> Connectors first) |
| ChatGPT | Plugins -> + -> Add custom MCP server -> paste the URL -> OAuth -> Create as a plugin (no such option: Developer mode under Settings -> Security and login) |
| Any other MCP client | The URL alone. A client that only runs local stdio servers: npx -y mcp-remote https://mcp.precisiondocs.ai/mcp (the standard mcp-remote bridge, same browser sign-in) |
Discovery for a custom client: /.well-known/oauth-protected-resource/mcp names the authorization server, /.well-known/oauth-authorization-server lists authorization_endpoint, token_endpoint, registration_endpoint and revocation_endpoint. Disconnect a client any time from Account -> Integrations -> Connected apps; that revokes its token and refresh token immediately.
A long agent turn never times out. precisiondocs_ask_agent submits the turn asynchronously (wait: false): the API answers 202 with a job_id, the turn runs on a worker, and the tool polls GET /v1/projects/{id}/messages/{job_id} for up to 45 seconds before handing the model that handle to keep polling - always under the proxy read timeout, so no more opaque 524s. REST callers get the same contract by sending wait: false; wait: true still runs the turn synchronously in one request. If a transport error ever still says the turn may be running, do not re-ask (a second ask would charge a second turn): list the project messages and read the newest turn.
Switch models by asking: Tell your connected assistant "use Claude Sonnet 5.5" or "switch to Auto". It reads precisiondocs_get_models and passes the approved model_id on subsequent precisiondocs_ask_agent calls. Auto is the default whenever no explicit choice is sent. A preference change itself does not run a paid turn; the assistant keeps it within your current conversation.
MCP tools and their REST equivalents:
| MCP tool | REST operation |
|---|---|
precisiondocs_plan_check | POST /v1/plan-check |
precisiondocs_resolve_parcels | POST /v1/parcels/resolve |
precisiondocs_preview_parcels | POST /v1/parcels/preview (returned to the client as an MCP image block) |
precisiondocs_create_project | POST /v1/projects |
precisiondocs_add_source | POST /v1/projects/{id}/sources, or POST /v1/projects/{id}/gis-sources when scope is given |
precisiondocs_start_upload | POST /v1/uploads/initiate |
precisiondocs_finish_upload | POST /v1/uploads/{job_id}/finalize |
precisiondocs_dismiss_advisory | DELETE /v1/projects/{id}/advisories/{advisory_id} |
precisiondocs_list_projects | GET /v1/projects |
precisiondocs_get_project | GET /v1/projects/{id} |
precisiondocs_update_project_goal | PATCH /v1/projects/{id} |
precisiondocs_verify_zoning | PATCH /v1/projects/{id}/parcels/{i}/zoning |
precisiondocs_get_models | GET /v1/models |
precisiondocs_ask_agent | POST /v1/projects/{id}/messages |
precisiondocs_get_message | GET /v1/projects/{id}/messages/{turn_id} |
precisiondocs_list_messages | GET /v1/projects/{id}/messages |
precisiondocs_get_site_knowledge | GET /v1/projects/{id}/site-knowledge |
precisiondocs_get_study_area | GET /v1/projects/{id}/study-area |
precisiondocs_get_parcel_geometry | GET /v1/projects/{id}/parcels/geometry |
precisiondocs_set_study_area | POST /v1/projects/{id}/study-area |
precisiondocs_clear_study_area | DELETE /v1/projects/{id}/study-area |
precisiondocs_reapply_study_area | POST /v1/projects/{id}/study-area/reapply |
precisiondocs_export_gis | POST /v1/projects/{id}/gis-exports |
precisiondocs_create_map | POST /v1/projects/{id}/maps |
precisiondocs_get_site_plan | GET /v1/projects/{id}/site-plan |
precisiondocs_get_design_criteria | GET /v1/projects/{id}/design-criteria |
precisiondocs_get_yield_scenario | GET /v1/projects/{id}/yield-scenario |
precisiondocs_export_site_plan | POST /v1/projects/{id}/site-plan-exports |
precisiondocs_list_workflows | GET /v1/projects/{id}/interview/workflows |
precisiondocs_get_interview_step | GET /v1/projects/{id}/interview/step |
precisiondocs_answer_interview | POST /v1/projects/{id}/interview/answers |
precisiondocs_approve_program | POST /v1/projects/{id}/site-plan/program/approve |
precisiondocs_propose_concept | POST /v1/projects/{id}/site-plan/proposals |
precisiondocs_list_proposals | GET /v1/projects/{id}/site-plan/proposals |
precisiondocs_decide_proposal | POST /v1/projects/{id}/site-plan/proposals/{proposal_id}/approve or /reject |
precisiondocs_search_project | POST /v1/projects/{id}/search |
precisiondocs_generate_report | POST /v1/projects/{id}/reports |
precisiondocs_get_report | GET /v1/projects/{id}/reports/{job_id} |
precisiondocs_list_artifacts | GET /v1/projects/{id}/artifacts |
precisiondocs_get_artifact | GET /v1/projects/{id}/artifacts/{artifact_id} |
precisiondocs_get_wallet | GET /v1/wallet |
precisiondocs_route | POST /v1/route: names the workflow a request asks for; the tool returns its tools in order, an argument sketch, the confirmations and the guide |
precisiondocs_get_docs | No REST call for a workflow guide (section set to a guide id such as maps-and-exports); otherwise the public OpenAPI spec (below), served as an index of every operation with the tool that calls it, its scope and its cost, or as one operation with its schemas resolved |
REST-only, each for a stated reason precisiondocs_get_docs repeats: PATCH /v1/projects/{id}/documents/{document_id} (no MCP read lists uploaded-document ids, and a display rename changes nothing an answer reads), GET /v1/projects/{id}/gis-exports/{artifact_id} (precisiondocs_get_artifact re-mints the same file), POST /v1/projects/{id}/summaries (the connected model summarizes itself, so a second model would spend credits for the same text) and GET /v1/projects/{id}/reports (precisiondocs_get_report and precisiondocs_list_artifacts cover it, and re-sending a report request attaches to the running job).
For step-by-step setup with copyable configs, use the in-app Account -> Integrations wizard or the Help Center connector guides.
How do I install the PrecisionDocs skill for Claude?
The PrecisionDocs Agent Skill teaches Claude the workflows behind the MCP tools: which tool answers what and in what order, what needs your confirmation, what spends credits, and every map and export PrecisionDocs makes, with how to put maps into a document. It is generated from the same source as the MCP server's own instructions, so the two never disagree, and it drives the connector above, which must be connected too.
| Client | Install |
|---|---|
| Claude Code (macOS, Linux, Git Bash) | curl -fsSL https://precisiondocs.ai/skills/precisiondocs.zip -o precisiondocs-skill.zip && unzip -o precisiondocs-skill.zip -d ~/.claude/skills && rm precisiondocs-skill.zip |
| Claude Code (Windows PowerShell) | Invoke-WebRequest https://precisiondocs.ai/skills/precisiondocs.zip -OutFile precisiondocs-skill.zip; Expand-Archive -Force precisiondocs-skill.zip "$HOME/.claude/skills"; Remove-Item precisiondocs-skill.zip |
| Claude app (claude.ai, Claude Desktop) | Download precisiondocs.zip, then open Settings, find Skills, choose Upload skill and pick the zip |
Claude loads the skill when a request fits it. Without the skill the same guides are one call away inside MCP: precisiondocs_get_docs with section set to a guide id (for example maps-and-exports), or with no section for the list of guides and the full API index.
Can I address PrecisionDocs by my agent name in Claude?
Yes. Name your agent under Account -> Profile -> Agent personality, the name the in-app chat already answers to. Every MCP session you connect opens its server instructions with that name (for example: The user calls PrecisionDocs "Atlas". A request to Atlas is a request to use these tools.) and names it on precisiondocs_ask_agent, so asking Claude "Atlas, what are the setbacks on my Maple Street project?" reaches PrecisionDocs instead of being answered from Claude's general knowledge.
The name is kept to plain name characters, up to 50. A name that already means Claude or any assistant (Claude, ChatGPT, Gemini, Copilot, Assistant, AI, Agent, Bot and similar) is not used for routing, because it would send your ordinary requests into paid agent turns; pick a distinct name such as Atlas. Claude reads the instructions when it connects, so after a rename start a new Claude session to pick it up.
OpenAPI Spec
The machine-readable schema and importing it into ChatGPT Actions.
Where is the OpenAPI spec?
The public OpenAPI 3.1 schema lives at https://api.precisiondocs.ai/v1/openapi.json. It is pruned to the public /v1 surface and always matches production.
ChatGPT Actions: in your Custom GPT, open Configure -> Actions -> Create new action, click Import from URL, and paste the spec URL. Do not paste the URL into the large Schema editor — use the Import dialog. Then set Authentication -> API Key -> Bearer with your pd_live_ key.
Any OpenAPI-aware client generator (openapi-typescript, openapi-python-client, Postman/Insomnia import) works against the same URL.