Developer API · v1
Roof measurements, by API.
Send an address, get back a measured roof: slope areas, pitches, edge lengths, a material takeoff, 3D geometry, the LiDAR point cloud and four solar layouts. The same data behind the RoofLine360 workspace and PDF report, for your CRM, quoting tool or AI assistant.
- REST + JSONOne key, plain HTTPS
- MCP serverFor Claude and other AI assistants
- Across the USAny address with LiDAR coverage
curl https://roofline360.com/api/v1/roofs \
-H "Authorization: Bearer $RL360_KEY" \
-H "Content-Type: application/json" \
-d '{"address": "10360 Wylde Point Ln, Houston, TX"}'
{
"id": "h271360-3290360",
"status": "ready",
"summary": {
"slopes": 14,
"roof_area_ft2": 5605.7,
"squares": 56.06,
"ridge_height_m": 10.3
},
"links": {
"measurements": ".../roofs/h271360-3290360/measurements",
"solar": ".../roofs/h271360-3290360/solar",
"report": ".../roofs/h271360-3290360/report.pdf"
}
}
What one address gives you
Every product below comes from the same roof model. Build it once; every later call reads the saved model and answers in milliseconds.
- GET /measurementsMeasurements
Surface, plan and footprint area, roofing squares, predominant pitch, ridge height and edge lengths by type: eave, rake, ridge, hip, valley, step, slope break.
- GET /measurementsMaterial takeoff
Shingle bundles at your waste allowance and bundles per square, underlayment, starter and drip edge, hip and ridge caps, valley lining. US or metric.
- GET /geometry3D geometry
Every slope, step wall, edge, footprint and roof obstacle as GeoJSON with elevations, in WGS 84 or the local UTM zone. Drop it into a map or CAD.
- GET /scanLiDAR point cloud
About 30,000 classified points around the house: ground, vegetation, neighbouring roofs and the selected building.
- GET /solarSolar layouts
Four panel layouts with obstacle-aware placement, kW DC, annual and monthly kWh from a typical weather year, shade loss and the best slopes.
- GET /report.pdfPDF report
The customer-ready multi-page report: aerial view, dimensioned plan, pitch and area diagrams, schedules, takeoff and solar.
Quickstart
Three calls from an address to a material list.
-
1
Authenticate
Every request carries your key in the
Authorizationheader. Keys start withrl360_. Keep them on your server, never in a public web page.export RL360_KEY="rl360_..." export RL360="https://roofline360.com/api/v1" curl $RL360/usage -H "Authorization: Bearer $RL360_KEY"import os, time, requests API = "https://roofline360.com/api/v1" session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['RL360_KEY']}"const API = "https://roofline360.com/api/v1"; const headers = { Authorization: `Bearer ${process.env.RL360_KEY}` }; const get = async path => (await fetch(API + path, { headers })).json(); -
2
Measure an address
A house measured before answers
200with statusready. A new one answers202; poll it until it is ready, usually 10 to 60 seconds, up to a few minutes for the first roof in an area.curl $RL360/roofs -H "Authorization: Bearer $RL360_KEY" \ -H "Content-Type: application/json" \ -d '{"address": "10360 Wylde Point Ln, Houston, TX"}' # then, until "status" is "ready": curl $RL360/roofs/h271360-3290360 -H "Authorization: Bearer $RL360_KEY"roof = session.post(f"{API}/roofs", json={"address": "10360 Wylde Point Ln, Houston, TX"}).json() while roof["status"] in ("queued", "building"): time.sleep(3) roof = session.get(roof["links"]["self"]).json() if roof["status"] == "failed": raise RuntimeError(roof["message"])let roof = await (await fetch(API + "/roofs", { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ address: "10360 Wylde Point Ln, Houston, TX" }), })).json(); while (roof.status === "queued" || roof.status === "building") { await new Promise(r => setTimeout(r, 3000)); roof = await get(`/roofs/${roof.id}`); } -
3
Read the measurements
Areas, edges and the material takeoff in one call. Change
unit,wasteandbundles_per_squareto match your quote.curl "$RL360/roofs/h271360-3290360/measurements?unit=us&waste=12" \ -H "Authorization: Bearer $RL360_KEY"m = session.get(roof["links"]["measurements"], params={"unit": "us", "waste": 12}).json() print(m["roof"]["squares"], m["materials"]["shingle_bundles"])const m = await get(`/roofs/${roof.id}/measurements?unit=us&waste=12`); console.log(m.roof.squares, m.materials.shingle_bundles);
Endpoints
Base URL https://roofline360.com. All responses are JSON unless noted. Open an endpoint for its parameters and an example answer.
Roofs
POST/api/v1/roofsMeasure the roof at an address
Looks the address up and starts the roof model, or returns the saved roof when this house was measured before. Send address, or lat and lon. A new roof counts against your daily limit only when it is built successfully; a failed build and a saved roof do not.
| Body field | Type | Description |
|---|---|---|
address | string | Street address, up to 200 characters. |
lat, lon | number | The rooftop point, instead of an address. |
200 ready · 202 queued or building · 400 address not found · 429 daily limit reached
GET/api/v1/roofs/{id}Status and summary
status moves queued → building → ready or failed. While building, stage says where: downloading (survey data for a new area) or processing (the roof model). links lists every product once the roof is ready.
{
"object": "roof",
"id": "h271360-3290360",
"status": "ready",
"address": "10360 Wylde Point Ln, Houston, Texas, 77002",
"location": { "lat": 29.722357, "lon": -95.363715, "utm_epsg": 6344 },
"created_at": "2026-11-12 10:06",
"finished_at": "2026-11-12 10:06",
"summary": {
"slopes": 14, "roof_area_m2": 520.79, "roof_area_ft2": 5605.7,
"squares": 56.06, "footprint_m2": 414.09, "ridge_height_m": 10.3
},
"links": {
"self": "https://roofline360.com/api/v1/roofs/h271360-3290360",
"workspace": "https://roofline360.com/workspace/#h271360-3290360",
"measurements": "…/measurements", "geometry": "…/geometry", "scan": "…/scan",
"solar": "…/solar", "report": "…/report.pdf"
}
}
GET/api/v1/roofsRoofs your key created
Newest first, as {"object": "list", "data": [roof, …]}. Query limit, 1–100, default 50.
Measurements
GET/api/v1/roofs/{id}/measurementsAreas, pitches, edges, material takeoff
| Query | Default | Description |
|---|---|---|
unit | us | us (ft, ft²) or metric (m, m²). |
waste | 10 | Waste allowance, percent, 0–100. |
bundles_per_square | 3 | Shingle bundles per 100 ft², 1–20. |
{
"object": "roof_measurements",
"unit": "us",
"units": { "length": "ft", "area": "ft²" },
"roof": {
"surface_area": 5605.7, "plan_area": 4457.1, "footprint_area": 4457.2,
"squares": 56.06, "ridge_height": 33.8, "predominant_pitch": "11.9/12",
"slope_count": 14, "obstacle_count": 7
},
"edges": {
"eave": 219.7, "rake": 106.8, "ridge": 61.0, "hip": 244.8,
"valley": 146.6, "step": 68.5, "slope_break": 0.0, "total": 847.3
},
"materials": {
"waste_percent": 10, "bundles_per_square": 3,
"coverage_area": 6166.3, "squares": 61.66, "shingle_bundles": 185,
"underlayment_area": 5605.7, "starter_drip_edge": 326.5,
"hip_ridge_cap": 305.7, "valley_lining": 146.6
},
"slopes": [
{ "id": 0, "pitch": "11.9/12", "slope_deg": 44.8, "azimuth_deg": 102.8,
"facing": "E", "plan_area": 850.6, "surface_area": 1198.1 },
{ "id": 2, "pitch": "7.9/12", "slope_deg": 33.5, "azimuth_deg": 192.8,
"facing": "S", "plan_area": 758.4, "surface_area": 909.6 }
…
],
"obstacles": [ { "id": "O3", "kind": "vent or pipe", "slope_id": 2, "plan_area": 2.9, "height": 0.2 } … ]
}
GET/api/v1/roofs/{id}/geometry3D roof as GeoJSON
One FeatureCollection. properties.layer is facet (a slope), step_side (a wall between roof levels), edge (with kind), footprint or obstacle. Each position carries an elevation in metres. Properties are metric.
| Query | Default | Description |
|---|---|---|
crs | wgs84 | wgs84: [lon, lat, z]. utm: metres of the house's UTM zone (location.utm_epsg), for CAD. |
{
"type": "FeatureCollection",
"features": [
{ "type": "Feature",
"properties": { "layer": "facet", "id": 4, "pitch_in_12": 11.9, "slope_deg": 44.8,
"area_true_m2": 28.58, "z_min": 20.4, "z_max": 27.82 },
"geometry": { "type": "Polygon", "coordinates": [[[-95.36379, 29.72241, 20.399], …]] } },
{ "type": "Feature",
"properties": { "layer": "edge", "kind": "eave", "length_3d_m": 3.51 },
"geometry": { "type": "LineString", "coordinates": [[-95.36380, 29.72244, 20.399], …] } }
]
}
GET/api/v1/roofs/{id}/scanClassified LiDAR points
Column arrays x, y, z in metres relative to origin (UTM zone of the house) and cat, an index into categories: ground, noise, low returns, elevated surroundings, other roof candidates, selected building. About 30,000 points, 1–2 MB.
GET/api/v1/roofs/{id}/report.pdfCustomer-ready PDF
The full report as application/pdf. Takes unit, waste, bundles_per_square and the solar options with a solar_ prefix: solar_layout (sun, max, portrait, landscape), solar_module, solar_setback, solar_loss, solar_excluded. A few seconds for the first copy; repeats come from storage.
Solar
GET/api/v1/roofs/{id}/solarCompare four panel layouts
Flush-mounted panels on the actual slope polygons, clear of edges and obstacles. sun uses the best slopes only, max fills every slope, portrait and landscape fix the orientation. Production comes from a PVGIS typical weather year at the property.
| Query | Default | Description |
|---|---|---|
module | res430 | A key from /solar/modules, or custom with length, width (m) and watts. |
setback | 0.6 | Clearance from every slope edge, metres, 0–3. |
loss | 14 | System losses, percent, 0–50. |
exclude | Slope ids to leave empty, e.g. 3,7. |
{
"object": "solar_summary",
"module": { "key": "res430", "name": "Residential", "watts": 430, "length_m": 1.722, "width_m": 1.134 },
"setback_m": 0.6, "loss_percent": 14,
"weather_source": "PVGIS-ERA5 · Houston TMY (2005–2023)",
"most_production": "max",
"layouts": {
"sun": { "name": "Best sun", "panels": 46, "installed_kw_dc": 19.78, "annual_kwh": 27905,
"specific_yield_kwh_per_kwp": 1411, "shade_loss_percent": 0.5, "monthly_kwh": [1736, 2070, …] },
"max": { "name": "Max fill", "panels": 76, "installed_kw_dc": 32.68, "annual_kwh": 42334,
"specific_yield_kwh_per_kwp": 1295, "shade_loss_percent": 3.1, "monthly_kwh": [2454, 3012, …] },
"portrait": { "panels": 65, "installed_kw_dc": 27.95, "annual_kwh": 36527, … },
"landscape": { "panels": 75, "installed_kw_dc": 32.25, "annual_kwh": 41864, … }
},
"slopes": [ { "id": 3, "azimuth_deg": 193.1, "tilt_deg": 26.5, "area_m2": 91.4, "usable_area_m2": 66.4,
"open_sky_yield_kwh_per_kwp": 1458,
"panels": { "sun": 23, "max": 23, "portrait": 22, "landscape": 23 },
"annual_kwh": { "sun": 14409, "max": 14409, "portrait": 13783, "landscape": 14409 } } … ]
}
GET/api/v1/solar/modulesPanel formats
Eight example formats from 120 W to 720 W, by key: name, watts, length and width in metres.
Account
GET/api/v1/usageYour key and today's builds
builds_today (successful builds), builds_in_progress, failed_today (not counted), daily_build_limit, remaining_today and the number of roofs your key created.
POST/mcpModel Context Protocol server
JSON-RPC 2.0 over Streamable HTTP for AI assistants. Same key. See MCP.
MCP for AI assistants
Connect Claude, Cursor or any MCP client and ask in plain words: “How many bundles for 10360 Wylde Point Ln at 12% waste?”
measure_roofAddress or lat/lon → roof id and measurements. Waits up to 55 s for a new model.get_roofStatus and summary by id.get_roof_measurementsAreas, edges, takeoff and slopes, US or metric, any waste allowance.plan_solarFour layouts with kW DC and kWh per year for a panel format.list_my_roofsRoofs measured with this key.list_solar_modulesPanel formats plan_solar accepts.
Endpoint https://roofline360.com/mcp, Streamable HTTP, stateless. Authenticate with your API key as a Bearer header.
claude mcp add --transport http roofline360 \
https://roofline360.com/mcp \
--header "Authorization: Bearer rl360_..."
// claude_desktop_config.json
{
"mcpServers": {
"roofline360": {
"command": "npx",
"args": ["mcp-remote", "https://roofline360.com/mcp",
"--header", "Authorization: Bearer rl360_..."]
}
}
}
// .cursor/mcp.json
{
"mcpServers": {
"roofline360": {
"url": "https://roofline360.com/mcp",
"headers": { "Authorization": "Bearer rl360_..." }
}
}
}
curl https://roofline360.com/mcp \
-H "Authorization: Bearer $RL360_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": "measure_roof",
"arguments": {"address": "10360 Wylde Point Ln, Houston, TX"}}}'
How many shingle bundles do I need for 10360 Wylde Point Ln, Houston, at 12% waste?
measure_roof · get_roof_measurements(waste_percent=12)
The roof has 14 slopes and 5,606 ft² of surface (56.1 squares, mostly 11.9/12). At 12% waste that is 62.8 squares: 189 bundles at 3 per square, plus 327 ft of starter and drip edge, 306 ft of hip and ridge cap and 147 ft of valley lining.
Try it
Live requests to this server with your key. The key stays in this browser tab only.
Errors, limits and units
Errors
Every error is {"error": {"code", "message"}} with an HTTP status.
401 | unauthorized | Missing or unknown key. |
400 | invalid_parameter | Bad option, or the address was not found. |
404 | not_found | No roof with this id. |
409 | not_ready | Still building; poll the roof. |
409 | build_failed | No LiDAR or no building at this point. |
429 | rate_limited | Daily limit of successful new roofs reached. |
503 | server_busy | The roof server is starting; retry in a moment. |
Limits
A key starts with 25 new roofs per day. Only roofs that build successfully count: a failed build (no LiDAR, no building at the point) is free, and a build still running holds its place until it ends. Roofs measured before, by anyone, do not count, and reading a ready roof is not limited. Ask for more when you request your key.
Units and coordinates
Measurements follow unit. Geometry, scan and solar are metric. Azimuths are degrees clockwise from true north. Elevations are metres in the survey's vertical datum (NAVD88 in the continental US).
Accuracy
Measurements are estimates from aerial LiDAR, typically within a few percent of a tape measure on clean roofs. Verify on site before ordering. Solar figures are a screening estimate, not an engineering design.
On the roadmap
Planned additions to the API. Tell us which you need first when you request a key.
- Planned
webhooksBuild notificationsA
callback_urlonPOST /roofs; we call it withroof.readyorroof.failedinstead of you polling. - Planned
POST /batchesBatch measurementUp to 1,000 addresses per CSV or JSON upload, with one status and a combined export.
- Planned
GET /roofs/{id}/imageryAerial imageryThe licensed aerial photo of the property, cropped and north-up, with source and date.
- Planned
/geometry.glb · .dxf3D and CAD exportThe roof as glTF for web viewers and DXF for CAD and estimating software.
- Planned
GET /roofs/{id}/solar/hourlyHourly production24-hour profiles per month and P05–P95 bands, as in the workspace charts.
- Planned
OAuth 2.1One-click AI connectorsSign in from Claude and other assistants without pasting a key.
- Planned
SDKsPython and TypeScriptTyped clients generated from the OpenAPI spec, with polling built in.
- Planned
/dashboardKeys and usageCreate and revoke keys, see usage by day and set limits per key.
Get an API key
Sign in with your work email and tell us what you are building. Once we approve access, you create your keys right here.
Signed in as ·
Your request is with us. We email you when access is approved, then you create your key on this page.
| Key | Name | Created | New roofs today | Last used |
|---|
Your new key. This is the only time it is shown: store it now.