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.

MethodPathWhat it does
GET/meYour account and credit balance
POST/quotesPrice a PDF without starting anything
POST/takeoffsUpload a PDF and start a takeoff
GET/takeoffsList your takeoffs, newest first
GET/takeoffs/{id}Status, progress and totals
GET/takeoffs/{id}/itemsThe line items
GET/takeoffs/{id}/excelThe Excel workbook

POST /quotes

Multipart form. Reads the set, classifies each page, and prices it. Nothing is stored or charged.

FieldDescription
pdfRequired. The drawing set (PDF, up to 1 GB).
pagesOptional. 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.

FieldDescription
pdfRequired. The drawing set.
pagesOptional. Pages to take off, as for quotes.
nameOptional. Project name; defaults to the file name.
specOptional. The project specification (PDF), used for gauge, lining and pressure class.
clientRequestIdRecommended. 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.
acceptNoPlanSheetsOptional. true to run a selection with no plan sheet anyway (otherwise 422 no_plan_sheets).
acceptPrintedScaleOptional. 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.

StatusCommon codes
400invalid_id, invalid_cursor, too_many_pages, a reduced-print refusal (code: "reduced_print"), an unreadable PDF
401invalid_api_key
402insufficient_credits (with balance and quote)
403email_verification_required
404not_found: no such takeoff on your account
409not_ready, location_required (finish your account profile in the app once)
413file_too_large
422no_plan_sheets, quote_unavailable
429rate_limited: see the RateLimit and Retry-After headers
503ai_unavailable: nothing was started or charged; retry later

Limits

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.