Your balance, recent activity, and a place to order a report without writing any code.
| Reports Measured property geometry |
Live | ||
| Instant Squares Instant roof size estimate |
Coming soon | — |
One balance and one set of API keys cover every product.
Same pipeline the API uses. Sandbox orders draw no balance.
Reports draw against a prepaid balance. Your card is only charged when you top up — never per order.
Ready to spend right now.
Charges your default card and adds the amount to your balance immediately.
Keeps your balance topped up so orders never fail for want of credit. If an order takes you below the threshold, your card is charged and the credit lands straight away.
If the balance has gone negative, the charge covers the shortfall as well as the amount above — one top-up always clears the threshold. A declined card switches auto-recharge off rather than retrying on every order.
These are your defaults — used whenever a report is downloaded without
branding of its own. You can also pass branding on the download itself
(companyName, brandColor, brandTextColor,
logoUrl as query params), which overrides everything here for that one
file. See API docs → White labeling.
Setting a company name is what makes a report white-labeled: the header, footer, attribution and legal contact block all become yours. A small RoofQuery watermark stays in the footer. Leave the name blank and reports carry full RoofQuery branding. Either way it's applied at download time, so a change here re-skins reports you already ordered.
One key works across every RoofQuery product — they're account-wide, not per
product. rq_live_sk_* keys draw real balance; rq_test_sk_*
keys run sandbox timelines and never spend anything.
Account-wide, not per product: one endpoint receives events from everything you
use. Today that's the report stream, which carries every status change —
switch on the payload's status field in your handler.
Every order placed through the API or this dashboard.
Sandbox orders are free — no credit is drawn and no card is charged.
Order with a rq_test_sk_* key at any address below to trigger a specific timeline.
Matching is case-insensitive, and coordinates within ~110m match too. Anything unlisted runs
the unrecognized timeline. Override with the
X-RoofQuery-Sandbox-Scenario header.
- Sandbox and live are strictly separate. A
rq_test_sk_*key can never place a real order, and arq_live_sk_*key can never run a sandbox timeline. The environment comes from the key prefix, not from anything in your request, and an order is refused outright if the two ever disagree. - An unlisted address stays in sandbox. Order any address you like —
out of coverage, made up, malformed. It runs the
unrecognizedtimeline. There is no address that turns a sandbox order into a real one. - Sandbox never draws credit. Your balance is untouched and no card is charged, however many orders you place. The response still shows the real price so you can see what a live order would cost.
- Completed sandbox orders carry genuine XML — same parser and same PDF path as live.
- Any sandbox order that completes can be revised. Not just the
scenario named
revision— that one is named for the roof it carries, not for a behaviour. Request a revision against any completed sandbox order and you'll getrevisions_requested, thenrevision_completedabout 15 seconds later. - Webhooks fire for real against your configured endpoints.
payment_failedandorder_failedreturn a 402/502 from the order call itself, because that's what happens in production. They never arrive as a webhook.
Not available yet.
An instant roof-size estimate returned in the API response, with no measuring turnaround — for triage and quoting where a full measured report is more than you need.
Your existing API keys and webhook endpoints will cover it — those are account-wide, not per product, so there'll be nothing new to set up. It'll draw against the same balance too.
Base URL: https://api.roofquery.com. Authenticate with Authorization: Bearer <your key>.
Copies this whole page as Markdown, so you can paste it into an assistant and have it write the integration. Nothing secret goes on the clipboard — your keys aren't included.
One API key and one set of webhook endpoints cover every product, and every product draws on the same balance. These don't change as products are added.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/identity | Who the key belongs to, and whether it can order |
| GET | /api/v1/balance | Available credit and per-product prices |
| GET | /api/v1/webhooks | List your endpoints |
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/reports/pricing | Report products and prices |
| POST | /api/v1/reports/orders | Place an order |
| GET | /api/v1/reports/orders/:id | Order status and measurements |
| GET | /api/v1/reports/orders/:id/status | The same order, plus a resolved statusDetail block |
| GET | /api/v1/reports/orders/:id/pdf | Branded PDF (rendered per request) |
| GET | /api/v1/reports/orders/:id/xml | Delivered XML, verbatim |
| POST | /api/v1/reports/orders/:id/revisions | Request a revision on a completed report |
| GET | /api/v1/reports/orders/:id/revisions | Revision history for one order |
| GET | /api/v1/reports/revision-options | Valid revision topics and rules |
| GET | /api/v1/reports/sandbox/scenarios | Sandbox addresses and timelines |
A completed order exposes two files. Each is fetched with the same key you ordered
with, and each appears in the order payload's files[] array so you can hand
the URLs straight to your own code.
| File | Path | What it is |
|---|---|---|
report.pdf |
/orders/:id/pdf |
The white-labeled report. Served inline so you can iframe it; add ?download=1 to force a save dialog. |
report.xml |
/orders/:id/xml |
The measurement document exactly as delivered, byte for byte. |
curl -H "Authorization: Bearer rq_live_sk_..." \
"https://api.roofquery.com/api/v1/reports/orders/rq_ord_abc123/pdf?download=1" \
-o report.pdf
All three return 409 not_ready until the order reaches completed.
Wait for the webhook rather than polling.
Reports are white-labeled — they carry your brand. Set a company name and the header, footer, cover-page attribution and legal contact block all become yours; a small RoofQuery watermark remains in the footer. Leave it blank and the report falls back to full RoofQuery branding.
Two ways to supply it. Account-wide — set it once under Report branding and every report uses it. Per request — pass query params on the PDF URL to override the saved values for a single download, which is how you brand a report for one of your customers without touching your account settings.
| Parameter | Example | Effect |
|---|---|---|
companyName | Acme%20Roofing | Header, footer, attribution and legal block. Setting this is what turns white labeling on. A small RoofQuery watermark stays in the footer. |
brandColor | %231668B8 | Header band, table headers, accents. Hex — remember to URL-encode the #. |
brandTextColor | %23ffffff | Text on the header band. Picked automatically for contrast if omitted. |
logoUrl | https://.../logo.png | Logo on the header band. Must be publicly fetchable. |
curl -H "Authorization: Bearer rq_live_sk_..." \
"https://api.roofquery.com/api/v1/reports/orders/rq_ord_abc123/pdf\
?companyName=Acme%20Roofing\
&brandColor=%231668B8\
&logoUrl=https://acme.example.com/logo.png" \
-o acme-report.pdf
Nothing is cached — the PDF is rendered at the moment you request it. Change your branding and every report you've already ordered comes back re-skinned on its next download. The XML file carries no branding at all.
| Status | Meaning |
|---|---|
processing | Accepted. First status you'll see. |
in-progress | Measurement work has started. |
on-hold | Paused for clarification. Resolves on its own. |
completed | XML available, measurements populated, files listed. |
updated | A completed report received new geometry. |
cancelled | Ended without delivery. The cost was refunded as account credit. |
revisions_requested | A revision is open against a delivered report. |
revision_completed | The corrected report has been re-delivered. |
Insufficient balance and upstream rejections are not statuses — they come back as a 402/502 from the order call before an order exists, so there's nothing to webhook about.
GET /api/v1/reports/orders/:id/status returns the entire order — property,
report settings, billing, revisions, measurements and file links — plus a
statusDetail block that answers the two questions you'd otherwise write a
switch statement for.
{
"orderId": "rq_ord_abc123",
"status": "in-progress",
"statusDetail": {
"code": "in-progress",
"label": "In progress",
"description": "A technician is measuring the property.",
"terminal": false, // is anything further expected?
"filesReady": false, // can I download yet?
"known": true, // false if we add a status your code hasn't seen
"changedAt": "2026-07-31T14:02:11.000Z",
"estimatedDeliveryHours": 2,
"estimatedReadyAt": "2026-07-31T15:47:00.000Z",
"openRevisionId": null
},
"totals": null, // populated once measured
"files": [] // populated once measured
}
status stays the same plain string it is in the webhook payload, so a handler
written against webhooks works here unchanged. Turnaround is an estimate from the
product's typical time — around 2 hours residential and 8 commercial — not a
commitment, and it's only meaningful while the order is still moving.
Measurements arrive on the order itself, so the webhook body,
GET /orders/:id and GET /orders/:id/status all carry the same
numbers — you never need a second call to get them. totals covers the whole
property; each entry in structures[] is one building (house, detached
garage, shed) with the identical shape.
"totals": {
"structureCount": 1,
"totalAreaSqFt": 5597.29,
"totalSquares": 55.97,
"facetCount": 33,
"predominantPitch": "12/12",
"pitchAreas": { "12/12": 5204.08, "5/12": 257.97, "11/12": 66.42 },
"edgeTotals": {
"EAVE": { "lengthFt": 276.31, "count": 27 },
"RAKE": { "lengthFt": 169.20, "count": 13 },
"RIDGE": { "lengthFt": 114.58, "count": 12 },
"HIP": { "lengthFt": 304.25, "count": 20 },
"VALLEY":{ "lengthFt": 237.30, "count": 17 }
},
"totalEdgeLengthFt": 1234.58,
"hipsAndRidgesLengthFt": 418.83,
"complexity": "complex",
"estimatedAtticSqFt": 4023.89
},
"structures": [ { "index": 1, "...": "every field above, for this building",
"faces": [ { "id": "F1", "areaSqFt": 338.51, "pitch": "12/12",
"edges": [ { "type": "EAVE", "lengthFt": 24.5 } ] } ] } ]
| Field | What it is |
|---|---|
structureCount | Separate buildings measured. totals is the sum across all of them. |
predominantPitch | The pitch covering the most area. Flat or N/12. |
pitchAreas | Roof area at each pitch — what you need to price steep-slope labour. |
edgeTotals | Length and count per category: EAVE, RAKE, RIDGE, HIP, VALLEY, STEPFLASH, FLASHING, OTHER. Always all eight, zero-filled. |
hipsAndRidgesLengthFt | Hips and ridges together — ridge cap is ordered against the pair. |
complexity | simple / normal / complex, from facet count and total hip+valley run. |
estimatedAtticSqFt | Sloped area projected back to horizontal. For ventilation and insulation sizing, not a measured floor area. |
faces[] | Every roof plane, with its own area, pitch and edge list. |
Edges shared between adjacent faces are counted once per structure, so adding up
faces[].edges[] yourself will overcount — use edgeTotals.
Pitches are snapped to a reporting ladder (0, ¼, ½, then whole numbers): measured
slopes arrive as values like 10.31, and the PDF and these fields round
through the same function so they can't disagree.
Revisions are only available on a report that has been delivered.
An order has to be completed (or updated) before there is
anything to correct — asking earlier comes back 409 NOT_REVISABLE.
Revisions are free: a correction to work you already paid for draws no further balance.
One open revision per report. While a revision is open the order sits
in revisions_requested and a second request is refused. The previously
delivered files stay downloadable the whole time — the correction replaces them when it
lands, and the order returns to completed with a
revision_completed webhook.
curl -X POST "https://api.roofquery.com/api/v1/reports/orders/rq_ord_abc123/revisions" \
-H "Authorization: Bearer rq_live_sk_..." \
-H "Content-Type: application/json" \
-d '{"topic":"Roof Has Wrong Pitch","notes":"Front elevation reads 12/12, closer to 6/12."}'
topic must be one of the values from
GET /api/v1/reports/revision-options. It is a fixed list, and the same list
in sandbox and live:
| Topic | Use it when |
|---|---|
Roof Has Wrong Pitch | One or more pitches are wrong. |
Missing Structures | A building on the property isn't in the report. |
Wrong Structure | The report includes something it shouldn't. |
Measurements Are Off | Areas or lengths don't match the property. |
Other | Anything else — explain it in notes. |
notes is required and capped at 2000 characters. Be specific about which
structure and which facet — it's read by the person making the correction.
Sandbox runs the whole cycle, on any completed order. Every sandbox
test address that reaches completed can be revised — there is no special
address for it. Request a revision and you'll get revisions_requested
immediately and revision_completed about 15 seconds later: the same two
webhooks, in the same order, that a live revision sends. Once it closes the order is
revisable again.
| Code | HTTP | Meaning |
|---|---|---|
unauthorized | 401 | Missing, invalid, or revoked key |
validation | 400 | A field is missing or malformed |
INSUFFICIENT_CREDITS | 402 | Available balance won't cover the report |
BILLING_SETUP_REQUIRED | 402 | No card or balance on the account yet |
ORDER_FAILED | 502 | Order could not be accepted; cost refunded as account credit |
not_found | 404 | No such order on this account |
not_ready | 409 | Report hasn't completed, so the file doesn't exist yet |
REVISION_ALREADY_OPEN | 409 | One open revision per report |
NOT_REVISABLE | 409 | Revisions need a delivered report — this order isn't completed |
INVALID_TOPIC | 400 | Topic isn't one of the accepted values |