DuctIQ API
Version 1 · Last updated: October 13, 2026
Send a mechanical drawing set, get the takeoff back. The DuctIQ API runs the same takeoff as the web app: duct linear feet by size and system, fittings, diffusers and equipment, with the sheet each line came from. Results come back as JSON or as the same Excel workbook you download in the app.
Authentication
Create a key in Settings → API Keys. It is shown once; store it like a password. Send it on every request:
Authorization: Bearer dq_live_…
X-API-Key: dq_live_… works too. A key acts as your account: it sees your takeoffs and spends your credits. Revoke a key in Settings and it stops working immediately. Keys cannot create or revoke other keys.
Billing
API takeoffs draw on the same credit balance as uploads in the app, at the same per-sheet rate shown on the pricing page. You are charged for mechanical plan sheets; legends, schedules, details and specifications are read for context at no charge. Ask for a quote first if you want the exact price. A takeoff is refused with 402 insufficient_credits before anything starts if your balance does not cover it. The free one-sheet trial is available only in the web app.
Endpoints
Base URL: https://ductiq.ca/api/v1. Requests and responses are JSON unless noted.
| Method | Path | What it does |
|---|---|---|
| GET | /me | Your account and credit balance |
| POST | /quotes | Price a PDF without starting anything |
| POST | /takeoffs | Upload a PDF and start a takeoff |
| GET | /takeoffs | List your takeoffs, newest first |
| GET | /takeoffs/{id} | Status, progress and totals |
| GET | /takeoffs/{id}/items | The line items |
| GET | /takeoffs/{id}/excel | The Excel workbook |
POST /quotes
Multipart form. Reads the set, classifies each page, and prices it. Nothing is stored or charged.
| Field | Description |
|---|---|
pdf | Required. The drawing set (PDF, up to 1 GB). |
pages | Optional. Comma-separated 1-based pages to take off, e.g. 3,4,7. Default: every plan sheet. |
curl https://ductiq.ca/api/v1/quotes \
-H "Authorization: Bearer $DUCTIQ_API_KEY" \
-F pdf=@mechanical-set.pdf
{
"object": "quote",
"pageCount": 6,
"pages": [
{ "page": 1, "sheetId": "M-001", "kind": "legend" },
{ "page": 2, "sheetId": "M-101", "kind": "plan" }
],
"selectedPages": [],
"billableSheets": 2,
"creditsPerSheet": 10,
"totalCredits": 20,
"density": "medium",
"creditBalance": 140,
"sufficientCredits": true
}
Page kind is one of plan, schedule, legend, details, specification, index, riser, other.
POST /takeoffs
Multipart form. Starts a takeoff and answers 202 as soon as it is queued; poll GET /takeoffs/{id} until status is ready or error. A takeoff usually takes a few minutes per plan sheet.
| Field | Description |
|---|---|
pdf | Required. The drawing set. |
pages | Optional. Pages to take off, as for quotes. |
name | Optional. Project name; defaults to the file name. |
spec | Optional. The project specification (PDF), used for gauge, lining and pressure class. |
clientRequestId | Recommended. Your id for this request (up to 80 characters). Retrying with the same id never starts or charges a second takeoff; it answers 200 with the original and "duplicate": true. |
acceptNoPlanSheets | Optional. true to run a selection with no plan sheet anyway (otherwise 422 no_plan_sheets). |
acceptPrintedScale | Optional. true to accept a sheet that looks reduced in print as being at its printed scale (otherwise 400 with code: "reduced_print"). Lengths are not verified when you do. |
curl https://ductiq.ca/api/v1/takeoffs \
-H "Authorization: Bearer $DUCTIQ_API_KEY" \
-F pdf=@mechanical-set.pdf \
-F pages=2,3 \
-F name="Riverside Medical — L2" \
-F clientRequestId=7f3c9e1a-job-1182
The takeoff object
{
"id": 1182,
"object": "takeoff",
"name": "Riverside Medical — L2",
"fileName": "mechanical-set.pdf",
"status": "processing", // processing | ready | error
"createdAt": "2026-10-13T14:02:11.000Z",
"completedAt": null,
"totalLinearFt": null,
"creditsUsed": null,
"locked": false,
"degraded": false,
"error": null, // the reason, when status is "error"
"progress": { // GET /takeoffs/{id} only, while processing
"stageLabel": "Measuring duct runs",
"progressPercent": 45,
"etaSeconds": 210,
"queueAhead": 0
},
"links": {
"self": "/api/v1/takeoffs/1182",
"items": "/api/v1/takeoffs/1182/items",
"excel": "/api/v1/takeoffs/1182/excel",
"app": "/projects/1182"
}
}
degraded: true means the run fell back to a weaker reader and the numbers are approximate; re-run it in the app. locked is only ever true for a free preview made in the web app, whose quantities are withheld until it is unlocked.
GET /takeoffs
Query: limit (1–200, default 50), before (a takeoff id; pass the previous page's nextCursor), status.
{ "object": "list", "data": [ /* takeoffs */ ], "hasMore": true, "nextCursor": 1140 }
GET /takeoffs/{id}/items
Available once status is ready (409 not_ready before then).
{
"object": "list",
"takeoffId": 1182,
"locked": false,
"data": [
{
"id": 90311,
"system": "Supply Air",
"category": "ductwork",
"description": "Rectangular duct",
"size": "600x400",
"shape": "rect",
"quantity": 84.5,
"unit": "LF",
"status": "clean", // clean | review | out_of_scope
"confidence": 0.86,
"sheet": "M-101",
"viewport": "M-101/1",
"gauge": "24", "material": "galvanized steel", "lining": null, "pressureClass": null,
"flags": [],
"edited": false
/* equipment rows also carry manufacturer, model, tonnage, mbhInput, cfm, voltage, scheduleRef */
}
]
}
Rows with status review need a person to check them against the drawing before bidding; flags says why. out_of_scope rows (gas piping, demolition) are listed separately so they are never billed as HVAC. edited is true when someone corrected the row in the app.
GET /takeoffs/{id}/excel
Returns the workbook (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) with your cover-sheet defaults from Settings.
curl -L -o takeoff.xlsx https://ductiq.ca/api/v1/takeoffs/1182/excel \
-H "Authorization: Bearer $DUCTIQ_API_KEY"
Errors
Errors are JSON: { "error": "code", "message": "A sentence you can show a person." }, sometimes with extra fields.
| Status | Common codes |
|---|---|
| 400 | invalid_id, invalid_cursor, too_many_pages, a reduced-print refusal (code: "reduced_print"), an unreadable PDF |
| 401 | invalid_api_key |
| 402 | insufficient_credits (with balance and quote) |
| 403 | email_verification_required |
| 404 | not_found: no such takeoff on your account |
| 409 | not_ready, location_required (finish your account profile in the app once) |
| 413 | file_too_large |
| 422 | no_plan_sheets, quote_unavailable |
| 429 | rate_limited: see the RateLimit and Retry-After headers |
| 503 | ai_unavailable: nothing was started or charged; retry later |
Limits
- 120 requests per minute per key, and 40 uploads or quotes per 15 minutes per account.
- One PDF per request, up to 1 GB. Very large sets are refused with
too_many_pages; send the M-series sheets. - Poll status no more than every 10 seconds.
Accuracy
Takeoffs are AI-assisted estimates. Review them against the drawings before you bid, as the Terms of Use require; API use is governed by section 9 of the Terms.
Questions or a use case we don't cover? Email info@ductiq.ca.