InstantTakeoffAPIMCP connector

InstantTakeoff API (v1)

Upload a lighting drawing set, let the takeoff run, then read counts per fixture type per sheet, the priced estimate and Excel or CSV exports.

v1 is stable: fields may be added, never renamed or removed. Page numbers are 1-based, like a PDF viewer.

Authentication

Create an API key in the app under Team and billing, API keys. The key is shown once; store it like a password. Send it on every request:

Authorization: Bearer itk_...

A key belongs to one member and one company and acts as that member. It stops working when it is revoked, when it expires, or when the member leaves the company. Owners and admins can see and revoke every member's keys.

Scopes: a key is either read (look at projects, results and estimates, make export links) or read and write (also create projects, upload drawing sets and start takeoffs).

Billing rules are the app's: a company on its free trial can change only its free trial project, and a view-only company can still read and export. A write that is not allowed answers 402 with payment_required, trial_limit or read_only.

Quick start

KEY=itk_your_key
API=https://app.instanttakeoff.ai/api/v1

# 1. a project
curl -s -X POST $API/projects -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" -d '{"name": "Riverside Clinic"}'

# 2a. upload from a link (the server downloads it; poll the job)
curl -s -X POST $API/projects/$PROJECT/uploads/from-url -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" -d '{"url": "https://files.example.com/plans.pdf"}'

# 2b. or upload the file in parts
curl -s -X POST $API/projects/$PROJECT/uploads -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" -d '{"filename": "plans.pdf", "bytes": 48213004}'
#   -> {"id": UPLOAD, "part_size": 33554432, "parts": 2, ...}
curl -s -X PUT $API/uploads/$UPLOAD/parts/1 -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/octet-stream" --data-binary @part1
curl -s -X PUT $API/uploads/$UPLOAD/parts/2 -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/octet-stream" --data-binary @part2
curl -s -X POST $API/uploads/$UPLOAD/complete -H "Authorization: Bearer $KEY"
#   -> {"set_id": SET, "job_id": ..., "duplicate": false}

# 3. wait until the stage is "ready"
curl -s $API/sets/$SET -H "Authorization: Bearer $KEY"

# 4. counts per type per sheet, the estimate, an Excel link
curl -s $API/sets/$SET/results -H "Authorization: Bearer $KEY"
curl -s $API/sets/$SET/estimate -H "Authorization: Bearer $KEY"
curl -s -X POST $API/sets/$SET/exports -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" -d '{"format": "xlsx"}'

Split the file into parts of exactly part_size bytes (the last part is shorter), for example with split -b 33554432 plans.pdf part. The same file uploaded twice to one company returns the existing drawing set ("duplicate": true).

The takeoff

Completing an upload starts everything on its own. GET /sets/{set_id} reports the stage:

StageMeaning
readingThe PDF is being read and indexed
findingThe pages are checked for lighting plans
countingThe recommended sheets are being counted
readyDone; results and the estimate are available
failedThe file couldn't be read (the failure field says why)

To count more sheets, look them up with GET /sets/{set_id}/pages and send POST /sets/{set_id}/takeoff with {"page_numbers": [12, 13]}. {"all_pages": true} checks every page, not only the electrical ones.

The model counts every fixture, exit sign and lighting control on the chosen sheets and types each one from its tag against the fixture schedule. The model keeps improving and is checked against a held-out test set. Every count is a box the estimator can open and check in the app; untagged in the results is the number of counted symbols with no type tag beside them, worth a look before bidding.

After the count, a review layer double-checks every sheet. GET /projects/{project_id}/review reports it: summary (e.g. "Review: 3 of 5 sheets reviewed"), and per sheet the lines the app shows (the quick pass, the checks, what is being re-examined now, the fixtures it found and added, the corrections applied), corrections counts once reviewed, and undo: the groups of added boxes that can still be taken back in the app.

Endpoints

MethodPathScopeWhat
GET/mereadThe company, user and key behind the request
GET/projectsreadProjects, newest first
POST/projectswriteCreate a project {name}
GET/projects/{project_id}readA project with its drawing sets, stages and totals
GET/projects/{project_id}/reviewreadReview status: reviewed of total sheets, each sheet's state (counting, pending, queued, checking, reviewed) and its review lines
POST/projects/{project_id}/uploadswriteStart a chunked upload {filename, bytes}
POST/projects/{project_id}/uploads/from-urlwriteUpload from an https link {url, filename?}
GET/uploads/{upload_id}readUpload progress
PUT/uploads/{upload_id}/parts/{n}writePart n as the raw body
POST/uploads/{upload_id}/completewriteFinish the upload; returns the set_id
GET/sets/{set_id}readStage, sheets and totals
GET/sets/{set_id}/pagesreadEvery page: number, sheet number, title, recommended
POST/sets/{set_id}/takeoffwriteCount chosen pages, or check the whole set
GET/sets/{set_id}/resultsreadCounts per type per sheet (?sheet_id= for one sheet)
GET/sheets/{sheet_id}readOne sheet's counts
GET/jobs/{job_id}readA job's status
GET/sets/{set_id}/estimatereadThe priced estimate (with wiring lines once a sheet's wiring is traced; the home-runs line has by_panel, e.g. {"LP1": 12, "LP2": 9})
POST/sets/{set_id}/exportsreadA signed Excel or CSV link {format}
GET/sets/{set_id}/model3dreadThe 3D model: state (ready, stale, building, none, failed, empty), per floor counts, wall and wiring feet, heights, panels, drops and risers (vertical), open_in_app, and failure (why the last build didn't finish)
POST/sets/{set_id}/model3d/buildwriteBuild the 3D model when it is missing or out of date (no takeoff data changes); poll job_id. One build per set at a time; 429 when the company's 3D build limits are reached, 409 when the set is too big for the 3D view
GET/sheets/{sheet_id}/boxesreadEvery box on a sheet: id, box in PDF points, kind, type, score, source, counted
POST/sheets/{sheet_id}/similarread (write to accept)Find all like this: box, box_id or type picks the example; kind?, accept?, set_type?, use_tags?
POST/sheets/{sheet_id}/rename-typewriteRename a type on the sheet {from, to, controls?}
POST/sheets/{sheet_id}/wiringwriteTrace the wiring (poll the job; runs are stored on the sheet)
POST/sheets/{sheet_id}/retakewriteTake the sheet off again {fresh?} (edits kept unless fresh)
POST/sets/{set_id}/retakewriteTake every sheet off again {fresh?}; its sheets are read only until it finishes
GET/sets/{set_id}/retakereadProgress of that run (also in GET /jobs/{job_id} as run)

Pricing, bids and proposals

MethodPathScopeWhat
GET/price-listreadSearch the company's price list (?q=&vendor=&limit=&offset=)
POST/price-listwrite, owner or adminAdd a line {catalog_no, manufacturer, description, unit, material_price, currency, vendor, effective_date, notes}
GET, PATCH, DELETE/price-list/{item_id}read / write, owner or adminOne line; change or delete it
POST/price-list/importwrite, owner or adminImport a vendor price list given as CSV text {csv_text, mapping?, defaults?, commit?, update_existing?}: a preview (new, updated, unchanged, skipped and why), or applied with commit: true
PUT/price-list/imports?filename=write, owner or adminUpload a CSV or .xlsx file (raw body, up to 10 MB): its columns and a mapping guess; then POST /price-list/imports/{import_id}/preview and /commit with {mapping}
GET, PUT/pricing/settingsread / write, owner or adminLabor units (hours each or per foot), labor rates, markup defaults, proposal defaults, currency
GET/sets/{set_id}/pricingreadEach estimate line's price list match and suggestions (catalog # first, then description)
POST/sets/{set_id}/pricing/matchwriteConfirm or clear a line's match {type, item_id, labor_unit?, remember?}; remembered for later projects
POST/sets/{set_id}/pricing/acceptwriteConfirm every unpriced line whose catalog # matches
GET, PUT/projects/{project_id}/pricingread / writeThe project's markups {overhead_pct, profit_pct, tax_pct, contingency_pct} and other_costs
GET, POST/projects/{project_id}/quotesread / writeVendor quotes per fixture type {type, vendor, price, basis: unit or lot, quote_no, valid_until}; a quote replaces the price list price in that project
GET/projects/{project_id}/bidreadBid summary: quantities by type, material, tax, labor, other costs, overhead, contingency, profit, total, and what is not priced (?set_id=)
GET, POST/projects/{project_id}/proposalsread / writeProposals; a new one starts from the company's proposal settings {set_id?, doc?}
GET, PUT/projects/{project_id}/proposals/{proposal_id}read / writeA proposal's text (doc) and price (lump sum or itemized); change it {doc, status, revision}
POST/projects/{project_id}/proposals/{proposal_id}/exportsreadA signed PDF or Word link {format: pdf or docx}

GET /sets/{set_id}/estimate prices every line the way the app does: a price typed in the app, else the project's selected vendor quote, else the matched price list line; labor hours from the line's labor unit unless typed. Each item says where its price came from (material_source, price_item, labor_unit), and bid holds the totals with markups and the warnings.

Assemblies

An assembly is a fixture plus the parts that go with it (a whip, a box, connectors, supports...), priced as one unit. Each part is a price list line (item_id) or a typed item (description, unit, price), with a quantity per fixture (qty) and labor hours per part (hours). A fixture type of an estimate uses the assembly picked for it in the project, else one remembered for its catalog #, else the first rule that fits: a type tag, a word or phrase of the schedule description, or the fixture kind (used_for: {tags, words, kinds}). The parts are added to every counted fixture of the type.

MethodPathScopeWhat
GET/assembliesreadThe library (?q=): each assembly with its parts, material and labor hours per fixture, used_for; the starter assemblies and whether each was added
POST/assemblieswrite, owner or adminCreate one {name, description, labor_unit, used_for, parts}, or from a starter {template: troffer, downlight, highbay, exit, sensor or switch}
POST/assemblies/starterswrite, owner or adminAdd every starter assembly the company doesn't have yet
GET, PUT, DELETE/assemblies/{assembly_id}read / write, owner or adminOne assembly; change it (any of the fields, revision for a 409 on a concurrent change) or delete it
GET/sets/{set_id}/assembliesreadEach type of the estimate with its assembly, why it was picked (chosen_by: project, remembered, tag, words, kind), its parts for the counted fixtures and the line totals
POST/sets/{set_id}/pricing/assemblywritePick a type's assembly in this project {type, assembly_id, remember?}: an id, null for none, or "auto" for the rules; remembered for the same catalog # in later projects

In GET /sets/{set_id}/estimate, an item that uses an assembly has assembly (its name, chosen_by, per_fixture, the fixture's own totals and every part's quantity, material and labor). material_each and labor_hours_each stay the fixture's own; material_total, labor_hours, labor_total and line_total include the parts, so the bid totals, markups and proposals include them too. The Excel export adds an Assemblies sheet (the fixture row and the part rows of every type that uses an assembly), and the CSV the same table at the end. PUT /projects/{project_id}/pricing takes use_assemblies: false to price a bid without them.

The starter assemblies are starting points: round quantities and labor hours to replace with your own, and no prices until their parts are linked to your price list or given a price. A company holds at most 200 assemblies of at most 30 parts each.

Export links work for a few minutes, need no Authorization header, and stop working when the key that made them is revoked. Share them only with people who may see the estimate.

Errors and limits

Errors are JSON: {"detail": "..."} or {"detail": {"error": "...", "message": "..."}}.

StatusWhen
401Missing, unknown, revoked or expired key
402Billing does not allow this write (payment_required, trial_limit, read_only)
403insufficient_scope (a read key tried to write), or the member left the company
404Not found, including any id that belongs to another company
409Not ready yet, or a conflict (for example an upload that is already finished)
410An export link that expired or was revoked
422Invalid input
429Rate limited; wait for Retry-After seconds

Rate limits apply per key and per company, per minute and per day.