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
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
Create an OSQR account and subscribe to a plan with the Estimating add-on.
Go to Settings → Integrations → Developer API and create an API key. Copy it immediately — it's only shown once.
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 URL | https://osqr.ai |
| Auth header | Authorization: Bearer otak_... |
| Content type | application/json (responses), multipart/form-data (file uploads) |
Keys start with otak_. Store securely — they grant full API access to your account.
Endpoints
/api/v1/takeoffSubmit a blueprint PDF for autonomous takeoff
/api/v1/takeoffList your recent takeoff jobs
/api/v1/takeoff/:jobIdGet job status, progress, and results
/api/v1/usageCheck 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
| file | required | PDF file (max 100MB) |
| projectName | optional | Human-readable project name (defaults to filename) |
| state | recommended | US state abbreviation (e.g. "AZ"). Enables cost matching. |
| county | recommended | County name (e.g. "Maricopa"). Required with state for cost matching. |
| zipcode | optional | ZIP code for more precise regional pricing |
| webhookUrl | optional | URL 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
| Status | Meaning |
|---|---|
| 400 | Bad request — missing file, wrong format, invalid webhook URL |
| 401 | Invalid, expired, or missing API key |
| 403 | Key doesn't have the required permission |
| 404 | Job not found (or belongs to different account) |
| 429 | Rate limit exceeded or monthly page budget exhausted |
| 500 | Server 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-Limit | Max requests per minute |
| X-RateLimit-Remaining | Requests remaining in current window |
| Retry-After | Seconds 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