GET /api/v1/sketches API key
Your account's sketches, newest first: sketchId, externalInspectionId, subject, status, revision, totalGlaSqFt, areaCount, created, updated and exported times.
Put the Planimble sketcher inside your inspection app. Your inspector draws the property there, and your system gets back the areas, the totals and the geometry, recalculated by Planimble's server.
Everything here is available on the B2B API plan. Requests go to https://app.planimble.com; on your own test server, use its address instead.
Four parties take part. Your API key never leaves your server; the browser only ever holds a short-lived link for one inspection.
API access requires an active B2B API plan. There is no separate test-key service: a live key operates on real account data. Open the playground’s key-creation steps: sign in as the owner in another tab, prepare the key request, then send it from the API console. The key is shown once; store it in your server’s secret store. You can also test the public health endpoint without a key.
POST /api/v1/org/api-keys (signed in as the owner)
→ { "apiKey": "pk_live_…", "note": "Store this now; it is not shown again." }Only registered websites can show the sketcher. Use the address of the page that will hold the iframe: https, no path. http://localhost is accepted for testing. Up to 10.
PUT /api/v1/org/embed-origins (signed in as the owner)
{ "origins": ["https://inspections.example.com"] }One link per inspection visit. It is valid for two hours, works only on the website you name, and reaches only this one sketch.
curl -X POST "https://app.planimble.com/api/v1/embed/sessions" \
-H "X-Api-Key: $PLANIMBLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "externalInspectionId": "INS-1042",
"origin": "https://inspections.example.com",
"userName": "Pat Field",
"subject": "12 Elm St, policy 77-1042",
"mode": "edit" }'// ASP.NET Core: an endpoint your own page calls. The key stays here.
app.MapPost("/inspections/{id}/sketch-link", async (string id, IHttpClientFactory f, IConfiguration cfg) =>
{
var http = f.CreateClient();
http.BaseAddress = new Uri("https://app.planimble.com");
http.DefaultRequestHeaders.Add("X-Api-Key", cfg["Planimble:ApiKey"]);
var res = await http.PostAsJsonAsync("/api/v1/embed/sessions", new
{
externalInspectionId = id,
origin = "https://inspections.example.com",
userName = "Pat Field",
subject = "12 Elm St, policy 77-1042",
mode = "edit"
});
res.EnsureSuccessStatusCode();
var link = await res.Content.ReadFromJsonAsync<JsonElement>();
return Results.Ok(new { embedUrl = link.GetProperty("embedUrl").GetString() });
});// Express: an endpoint your own page calls. The key stays here.
app.post('/inspections/:id/sketch-link', async (req, res) => {
const r = await fetch('https://app.planimble.com/api/v1/embed/sessions', {
method: 'POST',
headers: { 'X-Api-Key': process.env.PLANIMBLE_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
externalInspectionId: req.params.id,
origin: 'https://inspections.example.com',
userName: 'Pat Field',
subject: '12 Elm St, policy 77-1042',
mode: 'edit'
})
});
const link = await r.json();
if (!r.ok) return res.status(r.status).json(link);
res.json({ embedUrl: link.embedUrl });
});Load the SDK from Planimble and give it the link. It saves on its own while the inspector works; onSaved fires after every save on the server.
<div id="sketch" style="height: 720px"></div>
<script src="https://app.planimble.com/sdk/planimble-embed-sdk.js"></script>
<script>
const { embedUrl } = await (await fetch('/inspections/INS-1042/sketch-link', { method: 'POST' })).json();
const sketch = PlanimbleEmbed.mount('#sketch', {
embedUrl,
height: '100%',
onReady: m => console.log('ready', m.mode),
onSaved: m => showTotals(m.calculatedSummary), // { totalGlaSqFt, totalNonGlaSqFt, … }
onError: m => m.code === 'embed_expired' && reopen()
});
</script>Read by your own inspection ID. Treat this server copy as the record: it is recalculated by Planimble, not taken from the browser.
curl "https://app.planimble.com/api/v1/sketches/INS-1042" \ -H "X-Api-Key: $PLANIMBLE_API_KEY"
var sketch = await http.GetFromJsonAsync<JsonElement>("/api/v1/sketches/INS-1042");
var totals = sketch.GetProperty("calculatedSummary");
decimal gla = totals.GetProperty("totalGlaSqFt").GetDecimal();
decimal rc = totals.GetProperty("estimatedReplacementCostUsd").GetDecimal();const r = await fetch('https://app.planimble.com/api/v1/sketches/INS-1042', {
headers: { 'X-Api-Key': process.env.PLANIMBLE_API_KEY }
});
const { calculatedSummary, revision, status } = await r.json();| Credential | Who holds it | Sent as | Reaches |
|---|---|---|---|
API key pk_live_… | Your server only | X-Api-Key header | Every sketch, image and review note of your account |
| Embed token | The iframe, inside embedUrl | Added by the sketcher itself | One sketch and its images, for two hours, on one website. view tokens cannot save. |
| Owner sign-in | The account owner's browser | Cookie | Account settings: API keys and registered websites |
Call the API from your server, not a browser. If a browser that is signed in to Planimble sends a request, its sign-in takes priority over X-Api-Key. Server-to-server calls carry no cookie, so the key is always used.
An API key works only while the account is on an active B2B API plan (or a payment is past due). Removing a website from the registered list stops its links at once, including ones already handed out.
POST /api/v1/embed/sessions API key
| Field | Required | Meaning |
|---|---|---|
externalInspectionId | Yes | Your ID for the inspection, up to 100 characters. The sketch is stored and read under it. |
origin | Yes | The registered website that will show the iframe, for example https://inspections.example.com. |
mode | No | edit (default) or view. In view mode every floor is locked and nothing is saved. |
userName | No | Shown in the sketcher as the inspector, up to 80 characters. |
subject | No | The address or policy line shown on the sketch, up to 200 characters. |
{
"token": "CfDJ8…",
"embedUrl": "https://app.planimble.com/embed?token=CfDJ8…&subject=12%20Elm%20St",
"expiresAtUtc": "2026-10-24T13:40:00+00:00",
"origin": "https://inspections.example.com",
"mode": "edit"
}Inside your page the sketcher shows only the sketch: no Planimble sign-in, account, billing or list of other sketches. It saves to the server automatically while the inspector works, and works on touch screens with the number pad, voice and photos. Grant microphone and camera to the iframe (the SDK does) if you want voice input and photos.
After two hours, or if the website is removed, the sketcher shows "This sketch session has ended" and sends PLANIMBLE_ERROR with code: "embed_expired". Anything saved so far is kept. Ask your server for a new link and mount it again.
Load /sdk/planimble-embed-sdk.js from Planimble. It adds window.PlanimbleEmbed (version 2.0) and talks only to the Planimble origin in embedUrl, and only with the iframe it created.
PlanimbleEmbed.mount(container, options)| Option | Type | Meaning |
|---|---|---|
embedUrl | string | Required. The link from your server. |
height | string | CSS height of the iframe. Default 720px. |
title | string | Accessible name of the iframe. Default "Property sketch". |
onReady(m) | function | The sketch is open. m.externalInspectionId, m.mode. |
onSaved(m) | function | After every save on the server. m.revision, m.calculatedSummary. |
onError(m) | function | m.code: embed_expired, not_ready or not_saved (it retries on its own). |
onData(payload) | function | Every answer to getData(). |
| Method | Does |
|---|---|
save() | Saves now. Normally not needed; useful before your own "Submit" button. |
getData(timeoutMs = 8000) | Resolves with the current sketch as JSON (see Sketch data), including unsaved edits. Rejects if the sketcher does not answer. |
load(payload) | Replaces the drawing with your JSON, for example a sketch from a previous inspection. Keep the same externalInspectionId as the link, or the server refuses to save it. |
destroy() | Removes the iframe and stops listening. |
The SDK is a thin wrapper over postMessage. If you build your own, send to and accept from the exact Planimble origin, and check event.source is your iframe.
| Direction | Type | Fields |
|---|---|---|
| To the sketcher | PLANIMBLE_SAVE | none |
| To the sketcher | PLANIMBLE_GET_DATA | none |
| To the sketcher | PLANIMBLE_LOAD | payload: sketch JSON |
| From the sketcher | PLANIMBLE_READY | externalInspectionId, mode |
| From the sketcher | PLANIMBLE_SAVED | externalInspectionId, revision, calculatedSummary |
| From the sketcher | PLANIMBLE_DATA_RESPONSE | payload: sketch JSON |
| From the sketcher | PLANIMBLE_ERROR | code, sometimes message |
| From the sketcher | PLANIMBLE_DIRTY_CHANGED | isDirty, changeCount: there are unsaved edits |
| From the sketcher | PLANIMBLE_VALIDATION_WARNING | A drawing problem the inspector should look at, in args |
The last two are not passed to SDK callbacks yet; listen for them on window if you need them. The playground's message log shows every message.
All responses are JSON. Errors return { "code": "…", "message": "…" }; save validation errors return { "code": "…", "error": "…" }. Every /api response is sent with Cache-Control: no-store.
{id} is your externalInspectionId or Planimble's sketchId.
/api/v1/sketches API keyYour account's sketches, newest first: sketchId, externalInspectionId, subject, status, revision, totalGlaSqFt, areaCount, created, updated and exported times.
/api/v1/sketches/{id} API keyOne sketch: revision, status, rates, homeStyle, rcFormula, calculatedSummary and the full drawing in vectorCanvas (see Sketch data). 404 not_found if there is none yet.
/api/v1/sketches/{id}/versions API keySaved versions, newest first: versionId, revision, createdAtUtc, totalGlaSqFt, areaCount. Read one with GET /api/v1/sketches/{id}/versions/{versionId}; it has the same shape as a sketch.
/api/v1/sketches/sync API keyCreates or updates a sketch from your server, for example to start an inspection from last year's sketch. Send baseRevision (the revision you read, or 0 for a new sketch). Planimble recalculates every area and returns the result, with validation.issues and the month's usage. With an API key you may also set status to Draft, QA_Review or Approved, and ask for a webhook.
/api/v1/sketches/{id}/exported API keyRecords that a report was produced. From then on the sketch keeps its place in the monthly count even if it is deleted.
/api/v1/sketches/{id} API keyDeletes the sketch, and after a day any images no other sketch uses. If it was never exported, this month's count is given back: { "deleted": true, "refunded": true, "usage": {…} }.
/api/v1/sketches/{id}/comments API keyQA review notes pinned to walls, areas or points, with replies and open / resolved status. Writing notes needs a signed-in reviewer.
/api/v1/files API keyUploads a PNG or JPEG (body is the image; Content-Type: image/png or image/jpeg; up to 8 MB and 8192 pixels per side). Returns file.id to use as a photo's fileId or a background image's imageFileId. Read it back with GET /api/v1/files/{fileId}; check storage with GET /api/v1/files/usage.
/api/v1/embed/sessions API keyCreates an embed link. See Embedding the sketcher.
/api/v1/org/embed-origins Owner sign-inLists or replaces the registered websites: { "origins": [ … ] }. PUT replaces the whole list.
/api/v1/org/api-keys Owner sign-inCreates a new API key and returns it once. Existing keys keep working.
/api/v1/health, /api/v1/ready PublicHealth answers while the server runs. Ready also checks the database and returns 503 when it is unavailable.
The same shape is used to save (POST /sketches/sync), to load into the sketcher (PLANIMBLE_LOAD), and in vectorCanvas when you read a sketch (schema 1.7). Lengths are feet. x grows east and y grows south, so north is up on screen.
{
"externalInspectionId": "INS-1042",
"subject": "12 Elm St, policy 77-1042",
"baseRevision": 0,
"glaRate": 195, "auxRate": 88, "materialMultiplier": 1.0,
"floors": [ { "id": "main", "name": "Main floor", "visible": true, "locked": false } ],
"activeFloorId": "main",
"areas": [
{ "id": "a1", "code": "GLA1", "label": "Main house", "isLivingAreaGla": true, "factor": 1,
"floorId": "main",
"verticesFt": [ {"x":0,"y":0}, {"x":48,"y":0}, {"x":48,"y":30}, {"x":0,"y":30} ] },
{ "id": "a2", "code": "GAR", "label": "Garage", "isLivingAreaGla": false, "factor": 1,
"floorId": "main",
"verticesFt": [ {"x":48,"y":6}, {"x":72,"y":6}, {"x":72,"y":30}, {"x":48,"y":30} ] }
],
"stamps": []
}| Field | Meaning |
|---|---|
id | Your stable ID for the area. Keep it between saves. |
code, label | Area type code and name. GLA, GLA1, GLA2 and GLA3 always count as living area; others count as living area when isLivingAreaGla is true. |
factor | Multiplies the measured area, for example 0.5 for a half storey. Above 1.0 is allowed but reported as a warning. |
verticesFt | Corners in order, at least 3, up to 2,000. Don't repeat the first corner at the end. |
bulges | Optional, one per wall: 0 is straight, ±1 is a half circle (DXF bulge). |
holes | Optional openings cut out of the area (courtyards, stairwells), up to 50. |
floorId | Which floor the area is on. Required once floors is sent. |
| Field | Meaning |
|---|---|
floors, activeFloorId | 1 to 50 floors with unique IDs and names (up to 60 characters). |
stamps | Labels and symbols (id, type, x, y, floorId). A door, window, garage door or opening also has wallRef {polyId, edge, t 0–1} and opening {kind, width, swing, hinge}. |
annotations | Text notes on the sketch. |
photos | Up to 300: id, fileId, optional polyId, edge, caption (200 characters), takenAt, floorId. Photos never change square footage. |
underlay | Background image with its scale and position, from imageFileId or an embedded PNG/JPEG up to 2 MB. Up to 3 more in layers. Send null to remove it. |
homeStyle | { code, name, group }, for example a colonial or a ranch. |
rcFormula | Custom replacement cost: { "mode": "custom", glaRate, auxRate, multiplier, adjustment, note }, or { "mode": "default" }. |
Leaving out underlay, annotations, photos, bulges or holes keeps what is saved, so an older integration never wipes them by accident. Send an empty list or null to clear them.
Planimble ignores any areas or totals you send and measures every area itself (curved walls and openings included), rounded to a tenth of a square foot. Replacement cost is (living × glaRate + other × auxRate) × materialMultiplier, rounded to whole dollars, unless a custom rcFormula is set. Drawing problems are reported in validation.issues, never silently fixed:
SELF_INTERSECTION TOO_FEW_POINTS ZERO_AREA DUPLICATE_POINT SHORT_WALL FACTOR_RANGE BULGE_RANGE HOLE_OUTSIDE HOLE_OVERLAP
Every accepted save adds one to revision. Send the revision you last read as baseRevision. If someone saved in between (the inspector in the field, or QA in the office), the save is refused with 409, and the response carries the current server copy so you can merge or ask:
{ "error": "revision_conflict",
"message": "This sketch was changed on another device or by the office since your last sync.",
"yourBaseRevision": 3,
"server": { "revision": 4, … } }A new externalInspectionId counts toward the month's new sketches (500 on the B2B API plan). Edits to an existing sketch never count. Over the limit, a new sketch is refused with 402 quota_exceeded and resetsAtUtc.
When your server saves with an API key, add "webhookCallbackUrl": "https://…" to the body. Planimble then posts the save result to that address, signed with the webhook signing secret Planimble gives you: X-Planimble-Signature is the HMAC-SHA256 of the raw body, in upper-case hex.
Webhooks are sent once, with no retry, and only for saves made with your API key. Saves made by the inspector inside the sketcher do not send one: use onSaved on your page, then read the sketch from your server.
app.MapPost("/planimble/webhook", async (HttpRequest req, IConfiguration cfg) =>
{
using var reader = new StreamReader(req.Body);
var body = await reader.ReadToEndAsync();
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(cfg["Planimble:WebhookSecret"]!));
var expected = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(body)));
var given = req.Headers["X-Planimble-Signature"].ToString();
if (!CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(expected), Encoding.ASCII.GetBytes(given)))
return Results.Unauthorized();
var saved = JsonDocument.Parse(body).RootElement; // eventType: PLANIMBLE_SKETCH_SAVED
return Results.Ok();
});import crypto from 'node:crypto';
app.post('/planimble/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const expected = crypto.createHmac('sha256', process.env.PLANIMBLE_WEBHOOK_SECRET)
.update(req.body).digest('hex').toUpperCase();
const given = req.get('X-Planimble-Signature') || '';
if (given.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given))) return res.sendStatus(401);
const saved = JSON.parse(req.body); // eventType: PLANIMBLE_SKETCH_SAVED
res.sendStatus(200);
});| Status | Code | What to do |
|---|---|---|
| 400 | invalid_inspection_id | Send an ID of 1–100 characters. |
| 400 | invalid_origin, too_many_origins, invalid_mode | Fix the field named in the message. |
| 400 | invalid_floors, invalid_shape, invalid_opening, invalid_underlay, invalid_annotations, invalid_photos, invalid_rc_formula, invalid_home_style, sketch_too_large, missing_file | The sketch body breaks a rule; error says which. |
| 401 | auth_required, api_key_required | Send a valid X-Api-Key from your server. Check the plan is active. |
| 401 | embed_expired | The link ran out or its website was removed. Create a new link. |
| 402 | plan_required, quota_exceeded | Needs the B2B API plan, or the month's new sketches are used up. |
| 403 | origin_not_allowed | Register the website first (owner). |
| 403 | embed_scope | The link is view-only, or the call is outside its one sketch. |
| 403 | owner_only, not_allowed | The signed-in person lacks the role. |
| 403 | csrf_header_missing, bad_origin | A browser call without the app's headers. Call from your server with the key. |
| 404 | not_found, version_not_found | No sketch, version or file with that ID in your account. |
| 409 | revision_conflict | Someone saved first. See Saving and conflicts. |
| 409 | account_changed | Only in a signed-in browser after switching accounts. Reload. |
| 413, 415 | too_large, unsupported_type, invalid_image | Images must be PNG or JPEG, up to 8 MB. |
| 429 | rate_limited | Too many uploads from one address; wait a minute. |
| 507 | storage_full | Image storage is full. Delete sketches or images you no longer need. |
| Limit | B2B API plan |
|---|---|
| New sketches a month | 500 (edits are free) |
| Image storage | 20 GB per account |
| Image uploads | 60 a minute from one IP address |
| Embed link lifetime | 2 hours |
| Registered websites | 10 |
| Areas per sketch | 500, each up to 2,000 corners |
| Floors, photos, background images | 50 floors, 300 photos, 4 images |
| Image size | 8 MB and 8192 px per side (uploads); 2 MB and 4096 px (embedded background) |
embedUrl from your own server, after your own sign-in and permission checks.embedUrl in your database or logs.view mode for people who should only look.GET /api/v1/sketches/{id} on your server, not from browser messages.