Skip to main content

OSQR Takeoff API

v1
Get API Key
Coming Soon

Autonomous Blueprint Takeoff — as an API

Upload a PDF. Get back structured quantities, cost estimates, and anomaly detection. No manual measurement. The AI reads plans like a senior estimator.

$1/page

Simple usage-based pricing

30-page residential = $30

200-page commercial = $200

Request Early Access

Quantities

Items, dimensions, CY

Cost Estimates

Per-item pricing

Multi-Building

Per-building breakdown

Webhooks

Async results delivery

How It Works

Upload a PDF blueprint and get back structured quantity takeoffs — items, dimensions, cubic yards, rebar tonnage, and cost estimates, organized by building. Provide a location (state + county) and each item gets matched against cost databases for per-unit pricing.

$1

per page processed

60

requests/minute

2-5 min

per blueprint set

Getting Started

1

Create an OSQR account and subscribe to a plan with the Estimating add-on.

2

Go to Settings → Integrations → Developer API and create an API key. Copy it immediately — it's only shown once.

3

Make your first request:

curl https://osqr.ai/api/v1/usage \
  -H "Authorization: Bearer otak_your_key_here"

Authentication

All requests require a Bearer token in the Authorization header. All responses are application/json.

Base URLhttps://osqr.ai
Auth headerAuthorization: Bearer otak_...
Content typeapplication/json (responses), multipart/form-data (file uploads)

Keys start with otak_. Store securely — they grant full API access to your account.

Endpoints

POST
/api/v1/takeoff

Submit a blueprint PDF for autonomous takeoff

GET
/api/v1/takeoff

List your recent takeoff jobs

GET
/api/v1/takeoff/:jobId

Get job status, progress, and results

GET
/api/v1/usage

Check page budget and rate limits

Submit a Takeoff

Send a PDF as multipart form data. The response is immediate (202) — the pipeline runs asynchronously.

Request

curl -X POST https://osqr.ai/api/v1/takeoff \
  -H "Authorization: Bearer otak_your_key_here" \
  -F "file=@foundation-plans.pdf" \
  -F "projectName=Desert Ridge Phase 2" \
  -F "state=AZ" \
  -F "county=Maricopa" \
  -F "webhookUrl=https://your-app.com/webhook/osqr"

Parameters

filerequiredPDF file (max 100MB)
projectNameoptionalHuman-readable project name (defaults to filename)
staterecommendedUS state abbreviation (e.g. "AZ"). Enables cost matching.
countyrecommendedCounty name (e.g. "Maricopa"). Required with state for cost matching.
zipcodeoptionalZIP code for more precise regional pricing
webhookUrloptionalURL to POST results when complete (HTTPS recommended)

Without state and county, you still get quantities — but no per-item cost estimates.

Response (202 Accepted)

{
  "jobId": "clx7abc123def",
  "status": "queued",
  "pageCount": 234,
  "pollUrl": "/api/v1/takeoff/clx7abc123def",
  "message": "Job queued. Poll /api/v1/takeoff/clx7abc123def for status."
}

List Jobs

Fetch your 50 most recent takeoff jobs.

curl https://osqr.ai/api/v1/takeoff \
  -H "Authorization: Bearer otak_your_key_here"

Response

{
  "jobs": [
    {
      "id": "clx7abc123def",
      "status": "completed",
      "progress": 100,
      "projectName": "Desert Ridge Phase 2",
      "fileName": "foundation-plans.pdf",
      "pageCount": 234,
      "costUSD": 2.40,
      "createdAt": "2026-04-15T20:30:00.000Z",
      "completedAt": "2026-04-15T20:32:25.000Z"
    },
    {
      "id": "clx8xyz789ghi",
      "status": "processing",
      "progress": 50,
      "projectName": "Maple Street Condos",
      "fileName": "structural.pdf",
      "pageCount": 48,
      "costUSD": null,
      "createdAt": "2026-04-15T21:00:00.000Z",
      "completedAt": null
    }
  ]
}

Results are omitted from the list view for completed jobs. Fetch individual jobs for full results.

Poll Job Status

Poll until status is "completed" or "failed". Recommended polling interval: 5-10 seconds.

curl https://osqr.ai/api/v1/takeoff/clx7abc123def \
  -H "Authorization: Bearer otak_your_key_here"

Response (processing)

{
  "id": "clx7abc123def",
  "status": "processing",
  "progress": 50,
  "statusMessage": "Reading Building 2 foundation plan..."
}

Response (completed)

{
  "id": "clx7abc123def",
  "status": "completed",
  "progress": 100,
  "result": {
    "buildings": {
      "Building 1": {
        "items": [
          {
            "name": "Continuous Footing F1",
            "category": "Foundations",
            "quantity": 480,
            "unit": "LF",
            "dimensions": "24\" x 12\"",
            "volumeCY": 35.6,
            "rebarTons": 1.2,
            "confidence": "high",
            "source": "vision",
            "unitCostCents": 4250,
            "extendedCostCents": 2040000,
            "costNeedsReview": false
          },
          {
            "name": "Slab on Grade",
            "category": "Slabs",
            "quantity": 12500,
            "unit": "SF",
            "dimensions": "5\" thick, 4000 PSI",
            "volumeCY": 193.0,
            "confidence": "high",
            "source": "vision",
            "unitCostCents": 850,
            "extendedCostCents": 10625000,
            "costNeedsReview": false
          }
        ],
        "totalCY": 228.6,
        "totalRebarTons": 3.4
      }
    },
    "costs": {
      "totalCostCents": 12665000,
      "totalCostUSD": 126650.00,
      "matchedItems": 20,
      "unmatchedItems": 4,
      "tradeBreakdown": {
        "Foundations": 45200.00,
        "Slabs": 52800.00,
        "Walls": 18650.00,
        "Site Concrete": 10000.00
      },
      "location": { "state": "AZ", "county": "Maricopa" }
    },
    "reconciliation": null,
    "summary": {
      "totalItems": 24,
      "totalCY": 228.6,
      "totalRebarTons": 3.4,
      "buildingCount": 1,
      "tradeBreakdown": {
        "Foundations": 8,
        "Slabs": 4,
        "Walls": 6,
        "Site Concrete": 6
      }
    },
    "pipeline": {
      "version": "v5",
      "totalTimeMs": 145000,
      "costUSD": 2.40,
      "pageCount": 234,
      "steps": [
        { "name": "text-extraction", "timeMs": 3200 },
        { "name": "vision-reading", "timeMs": 85000 },
        { "name": "assembly", "timeMs": 52000 },
        { "name": "cost-matching", "timeMs": 4800 }
      ]
    }
  },
  "costUSD": 2.40,
  "createdAt": "2026-04-15T20:30:00.000Z",
  "completedAt": "2026-04-15T20:32:25.000Z"
}

Failed Jobs

If the pipeline can't extract from the PDF (corrupted file, scanned images without text, etc.), the job fails with an error message.

{
  "id": "clx9fail456jkl",
  "status": "failed",
  "progress": 20,
  "statusMessage": "Text extraction failed",
  "error": "PDF contains only raster images with no extractable text or vectors",
  "result": null,
  "createdAt": "2026-04-15T22:00:00.000Z",
  "completedAt": "2026-04-15T22:00:12.000Z"
}

Failed jobs still consume pages from your monthly budget (the pipeline attempted to process them). Common failure causes: password-protected PDFs, image-only scans, corrupted files.

Webhooks

If you provide a webhookUrl, we'll POST results when the job completes (or fails). No polling needed.

Webhook Payload

{
  "event": "takeoff.completed",
  "jobId": "clx7abc123def",
  "status": "completed",
  "result": { ... },
  "completedAt": "2026-04-15T20:32:25.000Z"
}

Verifying Signatures

Every webhook includes X-OSQR-Signature and X-OSQR-Timestamp headers. Verify them to confirm the request came from OSQR. Contact kable@osqr.app to receive your webhook signing secret.

const crypto = require('crypto');

function verifyOSQRWebhook(req, webhookSecret) {
  const signature = req.headers['x-osqr-signature'];
  const timestamp = req.headers['x-osqr-timestamp'];
  const body = JSON.stringify(req.body);

  const expected = crypto
    .createHmac('sha256', webhookSecret)
    .update(timestamp + '.' + body)
    .digest('hex');

  // Timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

Reject webhooks where the timestamp is more than 5 minutes old to prevent replay attacks.

Errors

StatusMeaning
400Bad request — missing file, wrong format, invalid webhook URL
401Invalid, expired, or missing API key
403Key doesn't have the required permission
404Job not found (or belongs to different account)
429Rate limit exceeded or monthly page budget exhausted
500Server error — retry with exponential backoff

All error responses include an error field with a human-readable message. Rate limit responses include Retry-After header.

Check Usage

curl https://osqr.ai/api/v1/usage \
  -H "Authorization: Bearer otak_your_key_here"

Response

{
  "key": { "name": "Production", "prefix": "otak_abc1..." },
  "usage": {
    "pagesUsed": 342,
    "pageLimit": 1000,
    "billingMonth": "2026-04",
    "pagesRemaining": 658
  },
  "rateLimit": { "requestsPerMinute": 60 }
}

Rate Limits

Rate limit headers are included on every response:

X-RateLimit-LimitMax requests per minute
X-RateLimit-RemainingRequests remaining in current window
Retry-AfterSeconds to wait (only on 429 responses)

Code Examples

Python

import requests
import time

API_KEY = "otak_your_key_here"
BASE = "https://osqr.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

# Submit a blueprint
with open("plans.pdf", "rb") as f:
    resp = requests.post(
        f"{BASE}/takeoff",
        headers=HEADERS,
        files={"file": ("plans.pdf", f, "application/pdf")},
        data={"projectName": "My Project", "state": "AZ", "county": "Maricopa"},
    )
job = resp.json()
print(f"Job submitted: {job['jobId']}")

# Poll until complete
while True:
    status = requests.get(
        f"{BASE}/takeoff/{job['jobId']}",
        headers=HEADERS,
    ).json()
    print(f"  {status['status']} — {status.get('statusMessage', '')}")
    if status["status"] in ("completed", "failed"):
        break
    time.sleep(10)

# Print results
if status["status"] == "completed":
    result = status["result"]
    print(f"Buildings: {result['summary']['buildingCount']}")
    print(f"Total CY: {result['summary']['totalCY']}")
    print(f"Total items: {result['summary']['totalItems']}")
    if result.get("costs"):
        print(f"Estimated cost: ${result['costs']['totalCostUSD']:,.2f}")

Node.js

const fs = require('fs');

const API_KEY = 'otak_your_key_here';
const BASE = 'https://osqr.ai/api/v1';

async function runTakeoff(pdfPath) {
  // Submit
  const form = new FormData();
  form.append('file', new Blob([fs.readFileSync(pdfPath)]), 'plans.pdf');
  form.append('projectName', 'My Project');
  form.append('state', 'AZ');
  form.append('county', 'Maricopa');

  const submitRes = await fetch(BASE + '/takeoff', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + API_KEY },
    body: form,
  });
  const { jobId } = await submitRes.json();
  console.log('Job submitted:', jobId);

  // Poll
  while (true) {
    const statusRes = await fetch(BASE + '/takeoff/' + jobId, {
      headers: { Authorization: 'Bearer ' + API_KEY },
    });
    const status = await statusRes.json();
    console.log(status.status, status.statusMessage || '');
    if (status.status === 'completed' || status.status === 'failed') {
      return status;
    }
    await new Promise(r => setTimeout(r, 10000));
  }
}

runTakeoff('./plans.pdf').then(r => console.log(JSON.stringify(r.result?.summary, null, 2)));

Questions? Email kable@osqr.app