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.
- Base URL:
https://app.instanttakeoff.ai/api/v1 - OpenAPI: [
https://app.instanttakeoff.ai/api/v1/openapi.json](/api/v1/openapi.json) - MCP connector for Claude, ChatGPT and other assistants: see the connector guide.
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:
| Stage | Meaning |
|---|---|
reading | The PDF is being read and indexed |
finding | The pages are checked for lighting plans |
counting | The recommended sheets are being counted |
ready | Done; results and the estimate are available |
failed | The 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
| Method | Path | Scope | What |
|---|---|---|---|
| GET | /me | read | The company, user and key behind the request |
| GET | /projects | read | Projects, newest first |
| POST | /projects | write | Create a project {name} |
| GET | /projects/{project_id} | read | A project with its drawing sets, stages and totals |
| GET | /projects/{project_id}/review | read | Review status: reviewed of total sheets, each sheet's state (counting, pending, queued, checking, reviewed) and its review lines |
| POST | /projects/{project_id}/uploads | write | Start a chunked upload {filename, bytes} |
| POST | /projects/{project_id}/uploads/from-url | write | Upload from an https link {url, filename?} |
| GET | /uploads/{upload_id} | read | Upload progress |
| PUT | /uploads/{upload_id}/parts/{n} | write | Part n as the raw body |
| POST | /uploads/{upload_id}/complete | write | Finish the upload; returns the set_id |
| GET | /sets/{set_id} | read | Stage, sheets and totals |
| GET | /sets/{set_id}/pages | read | Every page: number, sheet number, title, recommended |
| POST | /sets/{set_id}/takeoff | write | Count chosen pages, or check the whole set |
| GET | /sets/{set_id}/results | read | Counts per type per sheet (?sheet_id= for one sheet) |
| GET | /sheets/{sheet_id} | read | One sheet's counts |
| GET | /jobs/{job_id} | read | A job's status |
| GET | /sets/{set_id}/estimate | read | The 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}/exports | read | A signed Excel or CSV link {format} |
| GET | /sets/{set_id}/model3d | read | The 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/build | write | Build 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}/boxes | read | Every box on a sheet: id, box in PDF points, kind, type, score, source, counted |
| POST | /sheets/{sheet_id}/similar | read (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-type | write | Rename a type on the sheet {from, to, controls?} |
| POST | /sheets/{sheet_id}/wiring | write | Trace the wiring (poll the job; runs are stored on the sheet) |
| POST | /sheets/{sheet_id}/retake | write | Take the sheet off again {fresh?} (edits kept unless fresh) |
| POST | /sets/{set_id}/retake | write | Take every sheet off again {fresh?}; its sheets are read only until it finishes |
| GET | /sets/{set_id}/retake | read | Progress of that run (also in GET /jobs/{job_id} as run) |
Pricing, bids and proposals
| Method | Path | Scope | What |
|---|---|---|---|
| GET | /price-list | read | Search the company's price list (?q=&vendor=&limit=&offset=) |
| POST | /price-list | write, owner or admin | Add 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 admin | One line; change or delete it |
| POST | /price-list/import | write, owner or admin | Import 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 admin | Upload 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/settings | read / write, owner or admin | Labor units (hours each or per foot), labor rates, markup defaults, proposal defaults, currency |
| GET | /sets/{set_id}/pricing | read | Each estimate line's price list match and suggestions (catalog # first, then description) |
| POST | /sets/{set_id}/pricing/match | write | Confirm or clear a line's match {type, item_id, labor_unit?, remember?}; remembered for later projects |
| POST | /sets/{set_id}/pricing/accept | write | Confirm every unpriced line whose catalog # matches |
| GET, PUT | /projects/{project_id}/pricing | read / write | The project's markups {overhead_pct, profit_pct, tax_pct, contingency_pct} and other_costs |
| GET, POST | /projects/{project_id}/quotes | read / write | Vendor 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}/bid | read | Bid 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}/proposals | read / write | Proposals; a new one starts from the company's proposal settings {set_id?, doc?} |
| GET, PUT | /projects/{project_id}/proposals/{proposal_id} | read / write | A proposal's text (doc) and price (lump sum or itemized); change it {doc, status, revision} |
| POST | /projects/{project_id}/proposals/{proposal_id}/exports | read | A 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.
| Method | Path | Scope | What |
|---|---|---|---|
| GET | /assemblies | read | The library (?q=): each assembly with its parts, material and labor hours per fixture, used_for; the starter assemblies and whether each was added |
| POST | /assemblies | write, owner or admin | Create one {name, description, labor_unit, used_for, parts}, or from a starter {template: troffer, downlight, highbay, exit, sensor or switch} |
| POST | /assemblies/starters | write, owner or admin | Add every starter assembly the company doesn't have yet |
| GET, PUT, DELETE | /assemblies/{assembly_id} | read / write, owner or admin | One assembly; change it (any of the fields, revision for a 409 on a concurrent change) or delete it |
| GET | /sets/{set_id}/assemblies | read | Each 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/assembly | write | Pick 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": "..."}}.
| Status | When |
|---|---|
| 401 | Missing, unknown, revoked or expired key |
| 402 | Billing does not allow this write (payment_required, trial_limit, read_only) |
| 403 | insufficient_scope (a read key tried to write), or the member left the company |
| 404 | Not found, including any id that belongs to another company |
| 409 | Not ready yet, or a conflict (for example an upload that is already finished) |
| 410 | An export link that expired or was revoked |
| 422 | Invalid input |
| 429 | Rate limited; wait for Retry-After seconds |
Rate limits apply per key and per company, per minute and per day.