Developers
The vPlan AR API
Read your floor plans from your own software: list your projects, get each room’s measured area, wall area, openings, finishes and recorded damage, check stairs against the model codes, and download the same DXF, IFC, glTF, OBJ and XML files the editor exports. The API is part of the Business plan. It reads projects; changes are made in the app.
Authentication
Create a key in the web app under Settings → API & Developer. It is shown once; copy it then. Send it with every request as a bearer token. A key reads the projects of the account that made it, and stops working when it is revoked or when the account leaves the Business plan. You can have ten active keys.
curl https://vplan.ai/api/v1 \
-H "Authorization: Bearer vp_live_..."Endpoints
| Request | Returns |
|---|---|
| GET /api/v1 | Checks your key; lists the endpoints. |
| GET /api/v1/projects | Your projects, most recently changed first. |
| GET /api/v1/projects/{id} | A project’s details and measured summary. |
| GET /api/v1/projects/{id}/plan | Each level’s floor plan as JSON. |
| GET /api/v1/projects/{id}/export/{format} | The plan as a DXF, IFC, GLB, OBJ, MTL or XML file. |
Listing projects
GET /api/v1/projects returns up to limit projects (1 to 100, default 50), most recently changed first. Pass updated_since (an ISO 8601 time) to fetch only what changed since your last sync. When there are more, next_before is set; pass it back as before for the next page. An example:
curl "https://vplan.ai/api/v1/projects?limit=2&updated_since=2026-10-01T00:00:00Z" \
-H "Authorization: Bearer $VPLAN_KEY"
{
"data": [
{ "id": "9c1e...", "name": "12 Elm St", "created_at": "2026-10-02T14:03:11Z",
"updated_at": "2026-10-07T09:41:52Z", "total_area_m2": 122.6, "wall_count": 14 }
],
"next_before": null
}Project summary
GET /api/v1/projects/{id} returns the project’s details (client, property address, date, inspector and reference number as entered), its levels, and every room measured the way the editor’s takeoffs and reports measure it: area by square-footage category (Gross Living Area apart from garage, below-grade, unfinished and covered space), floor area less openings, ceiling height, length and width, wall area before and after doors and windows, baseboard length, door and window counts, finishes and recorded damage. Stairs come with their IRC or IBC checks. Lengths are metres and areas square metres, with square feet alongside the areas. An example, shortened:
{
"id": "9c1e...", "name": "12 Elm St",
"levels": [{ "id": "L1", "name": "Main Level", "elevation_m": 0, "ceiling_height_m": 2.74 }],
"totals": { "rooms": 7, "gross_living_area_m2": 122.6, "gross_living_area_sqft": 1319.7, ... },
"rooms": [
{ "name": "Kitchen", "level": "Main Level", "category": "living", "area_m2": 31.2, "area_sqft": 335.8,
"ceiling_height_m": 2.74, "wall_area_net_m2": 54.7, "baseboard_m": 19.8, "doors": 2, "windows": 1,
"floor_finish": "White oak, straight", "damage": "condition Damaged; severity Moderate; types Water", ... }
],
"stairs": [{ "type": "straight", "code": "irc", "risers": 14, "checks": [{ "kind": "riser", "status": "ok", ... }] }]
}Plan JSON
GET /api/v1/projects/{id}/plan returns each level’s plan in vPlan’s own format: walls, rooms, doors, windows, openings, furniture, stairs and annotations in metres, already brought up to the current format. Each level carries its elevation, ceiling height and its placement in the project’s shared frame.
Exports
GET /api/v1/projects/{id}/export/{format} returns a file with a download name, made by the same exporters as the editor. Every level is included unless you pass level=active. The 3D formats take floors, ceilings, furniture and finishes as true or false.
dxf: AutoCAD DXF. Several levels: stacked in the aligned project frame (default) or side by side with layout=sideBySide. units=imperial (default) or metric.ifc: IFC4 for Revit, ArchiCAD and other BIM tools; element ids stay the same from one export to the next, so a re-import updates.glb: Binary glTF 3D model: walls cut at doors and windows, floors, furniture, finishes as materials.obj: Wavefront OBJ of the same model; mtl is its material file.xml: Rooms, walls, doors and windows with dimensions in feet (units=metric for metres), the property address and every level.
curl -OJ "https://vplan.ai/api/v1/projects/9c1e.../export/dxf?units=metric" \
-H "Authorization: Bearer $VPLAN_KEY"PDF plan sets and reports are made in the app for now.
Webhooks
Add an endpoint under Settings → API & Developer → Webhooks and choose its events. vPlan sends it a POST with a JSON body when one happens, from the web editor or the iOS app:
project.created: a project was saved for the first time.project.updated: its plan, levels, name or details changed; at most one every 10 minutes per project, since the editor saves as you work.export.completed: a PDF, DXF, 3D model or other export was made, with its format.webhook.test: sent by the “Send test event” button.
Fetch the details with the API: events carry the project’s id, name and API URL, not the whole plan. An example:
POST https://example.com/vplan-webhook
vPlan-Event: project.updated
vPlan-Event-Id: 5b0e... (the same on every retry: use it to drop duplicates)
vPlan-Delivery: 9a41...
vPlan-Signature: t=1791490000,v1=4f6c...
{ "id": "5b0e...", "type": "project.updated", "created_at": "2026-10-08T17:01:02Z",
"data": { "project": { "id": "9c1e...", "name": "12 Elm St", "created_at": "...", "updated_at": "...",
"url": "https://vplan.ai/api/v1/projects/9c1e..." } } }Check the signature before trusting a request. It is an HMAC-SHA256, keyed with the endpoint’s signing secret (Reveal secret in Settings), of the timestamp, a dot and the raw body. Refuse it when it doesn’t match or the timestamp is more than five minutes old:
import { createHmac, timingSafeEqual } from 'crypto';
function verify(secret, header, rawBody) {
const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(v1, 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
}Answer with any 2xx status within 10 seconds; do slow work afterwards. Anything else, a redirect or no answer is retried after 1 minute, 10 minutes, 1 hour and 6 hours, then given up. An endpoint that fails 15 sends in a row is turned off until you turn it back on. Endpoints must be public HTTPS URLs. Settings shows each endpoint’s recent deliveries, and Roll secret replaces the signing secret at once. You can have five endpoints.
Errors and limits
Errors are JSON with a code and a message: {"error":{"code":"not_found","message":"No project with that id."}}. 401 means the key is missing, wrong or revoked; 403 that the account is not on Business; 404 that the project doesn’t exist or isn’t yours; 400 a bad parameter; 429 too many requests, with a Retry-After header. Each key can make 60 requests a minute.
Questions or something you need that isn’t here? Contact us.