Developer guide
Measurement API v3 Documentation
Measurement API: AI Measure v3 (September 2026)
Every contractor and software developer using the Measurement API must sign the Confidentiality, API Access and Roof Measurement Services Agreement before an API key is issued.
AI Measure v3 returns the same summary measurements as the Instant Roofer AI Instant Measure Report: roof area, pitch, perimeter, complexity, facet summary, and the new roof height, story count and seven roof edge totals (eaves, ridges, hips, valleys, rakes, headwalls, sidewalls). Every estimated value carries a low / medium / high confidence band.
v3 is measurement-first. It does not return building-classifier probabilities, footprint geometry, wireframes, point clouds, numeric confidence scores or the AI’s mapWithOutline render. The one image it returns is a plain satellite photo of the measured building (imagery.satellite, on by default; see Satellite Image below). If your integration needs the outline image or buildingPredictions, keep calling v2 for those.
Instant Roofer AI can make mistakes (e.g., sparse LiDAR, heavy tree cover, complex multi-structures). Review the confidence band of each measurement before using it for bids or material orders.
cURL (example)
Send latitude/longitude to the v3 route with your API key. address, googlePlaceId, externalId, boundingBox, zoom and imagery are optional. resultOptions is not accepted on v3.
curl -X POST https://v5.instantroofer.com/v3 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"latitude": 32.865378,
"longitude": -111.677416,
// Optional: your own job / order reference, returned as "externalId" on the response
"externalId": "JOB-48213"
}'
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| latitude | number | yes | Latitude of the building to measure. Must be non-zero and within -90..90. |
| longitude | number | yes | Longitude of the building to measure. Must be non-zero and within -180..180. |
| externalId | string | no | Your own job, lead or order ID, up to 128 characters. Returned unchanged as a top-level externalId on every JSON response, success or error, so you can match the result to your record. See Your own reference below. |
| address | string | no | Street address of the property. Echoed back in the response and printed on the report PDF as the measured address. |
| googlePlaceId | string | no | Google Place ID of the property. Echoed back in the response; not used for geocoding. |
| contractorName | string | no | With report.pdf, labels the “Ordering Company” on the report PDF; otherwise your company name is used. |
| report | object | no | { “pdf”: true } requests the AI Instant Measure Report PDF (accounts with the feature enabled). See Report PDF below. |
| imagery | object | no | { “satellite”: false } opts out of the satellite photo, which is included by default. See Satellite Image below. |
| boundingBox, zoom | object, number | no | Advanced overrides of the scan window. By default v3 scans a 640×640 px viewport centred on the coordinates at zoom 20, the same window as the dashboard’s AI Instant Measure Report. |
Example response
{
"version": "v3",
"externalId": "JOB-48213",
"coordinates": { "latitude": 32.86537804446558, "longitude": -111.67741611599844 },
"confidence": { "band": "high" },
"measurements": {
"sqft": { "aerial": 3215, "measured": 3483, "suggested": 4180 },
"squares": 41.67,
"pitch": "5/12",
"complexity": 3,
"wastePercent": 20,
"perimeter": 262,
"facets": 6,
"confidence": "high",
"roofHeight": {
"averageFt": 13.1,
"lowestFt": 9.3,
"highestFt": 16.9,
"confidence": "high"
},
"stories": { "count": 2, "confidence": "high" },
"edges": {
"eaves": { "lengthFt": 182, "confidence": "high" },
"ridges": { "lengthFt": 64, "confidence": "medium" },
"hips": { "lengthFt": 0, "confidence": null },
"valleys": { "lengthFt": 38, "confidence": "low" },
"rakes": { "lengthFt": 92, "confidence": "high" },
"headwalls": { "lengthFt": 18, "confidence": "medium" },
"sidewalls": { "lengthFt": 26, "confidence": "low" }
},
"dripEdge": { "lengthFt": 274 },
"wasteCalculation": {
"recommendedPercent": 20,
"options": [
{ "wastePercent": 0, "sqft": 3483, "squares": 34.67, "measured": true, "recommended": false },
{ "wastePercent": 15, "sqft": 4006, "squares": 40, "measured": false, "recommended": false },
{ "wastePercent": 16, "sqft": 4041, "squares": 40.33, "measured": false, "recommended": false },
{ "wastePercent": 17, "sqft": 4076, "squares": 40.67, "measured": false, "recommended": false },
{ "wastePercent": 18, "sqft": 4110, "squares": 41, "measured": false, "recommended": false },
{ "wastePercent": 19, "sqft": 4145, "squares": 41.33, "measured": false, "recommended": false },
{ "wastePercent": 20, "sqft": 4180, "squares": 41.67, "measured": false, "recommended": true },
{ "wastePercent": 21, "sqft": 4215, "squares": 42, "measured": false, "recommended": false },
{ "wastePercent": 22, "sqft": 4250, "squares": 42.67, "measured": false, "recommended": false },
{ "wastePercent": 23, "sqft": 4285, "squares": 43, "measured": false, "recommended": false },
{ "wastePercent": 24, "sqft": 4319, "squares": 43.33, "measured": false, "recommended": false },
{ "wastePercent": 25, "sqft": 4354, "squares": 43.67, "measured": false, "recommended": false }
]
}
},
"lidar": {
"wireframeQuality": "high",
"facets": {
"predicted_count": 6,
"area_total_m2": 328.38,
"area_total_sqft": 3534.64,
"areas": [
{ "area_m2": 138.8127900120834, "area_sqft": 1494.1669904110645, "k": 0 },
{ "area_m2": 141.92547368278858, "area_sqft": 1527.671606174168, "k": 1 },
{ "area_m2": 47.64119212395492, "area_sqft": 512.8050279030384, "k": 2 }
],
"pitch_stats": [
{ "facet_k": 0, "pitch_label": "5/12" },
{ "facet_k": 1, "pitch_label": "5/12" },
{ "facet_k": 2, "pitch_label": "6/12" }
],
"pitch_mix": [ { "pct": 100, "pitch_label": "5/12" } ]
}
},
"imagery": {
"satellite": {
"contentType": "image/jpeg",
"base64": "/9j/4AAQSkZJRg...",
"width": 1280,
"height": 1280,
"zoom": 20,
"center": { "latitude": 32.86537804446558, "longitude": -111.67741611599844 }
}
}
imagery.satellite.base64 shortened for clarity; expect roughly 0.3–0.7 MB. externalId is present only when the request included one.
Response field reference
externalId (top level)
- The externalId you sent with the request, returned unchanged. Omitted when the request did not include one.
- Also present on measurement error responses (for example 404 roof_not_found), so a failed request can still be matched to your record.
confidence (top level)
- band - the overall Report Confidence for the whole report: “low”, “medium” or “high”.
- The object is absent on records measured before Report Confidence launched.
measurements
- sqft.aerial - building footprint in square feet before pitch is applied.
- sqft.measured - roof surface area in square feet. Each detected pitch’s share of the roof is scaled by its own pitch factor, so mixed-pitch roofs (steep main roof plus a low-slope porch or garage) are not over-measured. The whole roof at every pitch.
- sqft.shingle - the part of sqft.measured at 2/12 pitch or steeper, where asphalt shingles can be installed. 0 when the whole roof is low slope.
- sqft.flat - the report’s “Total Flat Roof Area”: the part of sqft.measured at 1/12 pitch or flatter, the complement of sqft.shingle. Not added to sqft.suggested or squares, because asphalt shingles should not be installed on it. Absent when the roof carries no pitch information.
- sqft.suggested - recommended square footage of material to replace the roof: sqft.shingle (or sqft.measured when nothing is shingle-able) plus wastePercent, rounded up.
- squares - sqft.suggested in roofing squares, rounded to the nearest third of a square (one bundle).
- pitch - the predominant pitch (rise over 12), i.e. the pitch covering the largest share of the roof.
- complexity - roof complexity class: 0 = Low, 1 = Medium, 2 = High, 3 = Extreme.
- wastePercent - recommended material waste percentage for the roof’s complexity: 8, 11, 13, 18 or 20 (10 when complexity is unknown).
- perimeter - outside linear feet of the building footprint.
- facets - count of roof facets detected.
- confidence - confidence band of the roof-area measurement (see Confidence Bands).
measurements.roofHeight
- averageFt - average roof height above ground, in feet (one decimal).
- lowestFt - average height of the lowest roof tier (about eave level).
- highestFt - average height of the highest roof tier (about ridge level).
- confidence - confidence band of the height estimate.
- The whole object is null when no height could be estimated for the property.
measurements.stories
- count - estimated number of above-ground stories.
- confidence - confidence band of the story classifier, or null when the count came from a height heuristic instead of the classifier.
- The whole object is null when no story count could be estimated.
measurements.edges
Total length of each roof edge type, in whole feet, measured along the slope (the same convention as Human Certified Measure Reports). All seven classes are always present.
| Class | Description |
|---|---|
| eaves | Lower horizontal roof edges (gutter lines). |
| ridges | Horizontal peaks where two roof planes meet at the top. |
| hips | Sloped external corners where two roof planes meet. |
| valleys | Sloped internal corners where two roof planes meet. |
| rakes | Sloped edges at gable ends. |
| headwalls | Horizontal roof-to-wall intersections (flashing). |
| sidewalls | Sloped roof-to-wall intersections (step flashing). |
- lengthFt - total length in feet. 0 means the AI examined the roof and found none of that edge type. null means the edge model did not run for this property (typically no LiDAR coverage).
- confidence - confidence band for that class, or null when lengthFt is 0 or null.
measurements.dripEdge
Drip edge as a material quantity: the eaves plus the rakes, the two edge types that receive drip edge (the same convention as the “Drip Edge” card on the AI Instant Measure Report and on Human Certified Measure Reports).
- lengthFt - edges.eaves.lengthFt + edges.rakes.lengthFt, in whole feet; null when either of those is null.
- It is derived from measurements.edges, so it carries no confidence band of its own (see the eaves and rakes entries) and should not be added to the edge totals again. Townhomes carry one per building under buildings[].measurements.dripEdge.
measurements.wasteCalculation
- recommendedPercent - the recommended waste factor for this roof; always the same value as measurements.wastePercent.
- options[].wastePercent - the column’s waste factor.
- options[].sqft - material area at that factor: sqft.measured plus the waste, rounded up to the square foot.
- options[].squares - that area in roofing squares (100 sqft), rounded to the nearest third of a square (one bundle), like measurements.squares.
- options[].measured - true on the 0% column only: the measured roof with no waste added (labelled “Measured” on the report).
lidar.wireframeQuality
lidar.facets
- Quality band of the LiDAR roof wireframe (the 3D roof graph): “low” / “medium” / “high” from closure, coverage and plane-fit checks. Absent when no wireframe was computed. A “low” value caps the overall Report Confidence at “medium”.
- lidar.facets
- predicted_count - predicted count of detected roof facets.
- areas - size of each detected facet: an array of { area_m2, area_sqft, k } entries, where k is the facet index.
- area_total_m2 / area_total_sqft - combined area across all detected facets.
- pitch_stats - pitch of each detected facet: an array of { facet_k, pitch_label } entries, where facet_k matches the facet index in areas.
coordinates
All pitch labels use the N/12 form.
imagery
- Center of the building that was measured. Compare with your input to confirm the right structure was selected.
Satellite image
- base64 - the image body, base64-encoded with no data: prefix (the same convention as v1/v2’s mapWithOutline). Decode it to bytes, or display it as data:image/jpeg;base64,
. - width / height - delivered pixel size (1280 × 1280).
- zoom - Web Mercator zoom the photo was taken at (the request’s zoom, default 20). At the default zoom the photo covers the same ground area as the measurement’s scan window. Ground resolution is 0.149 m × cos(latitude) per map pixel, and the photo is rendered at scale 2, so each delivered pixel covers about 0.06 m (0.2 ft) at mid-latitudes.
- center - the photo’s centre coordinates, for georeferencing.
- error - present instead of the photo when it could not be fetched ({ “code”: “fetch_failed” | “not_configured”, “message”: ”…” }). satellite is then null; the measurement itself is still returned with status 200 and billed as usual.
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"latitude": 32.865378,
"longitude": -111.677416,
"imagery": { "satellite": false }
}'
Confidence bands
Every estimated measurement carries a band of “low”, “medium” or “high”. Numeric confidence scores are not returned in v3.
v3 bands are empirically calibrated: each band is assigned from the measured share of measurements with that rating that landed within 10% of a professional reference report, on properties the models never trained on. A “high” band means that share met our High bar with a low blow-out rate; classes that cannot yet meet the bar (hips, headwalls, sidewalls) never display “high”, no matter how confident the underlying model is.
- low: lower confidence in the measurement result. Review the report carefully before using it for bids or material orders.
- medium: moderate confidence in the measurement result. Review is still recommended, especially for complex or partially obstructed roofs.
- high: stronger confidence in the measurement result. Review is still recommended for critical project decisions.
Underneath the calibration: edge-length signals come from cross-model agreement on the LiDAR data; roof height and story signals from those models’ calibrated confidence; the roof-area signal from the area model’s expected-error estimate. lidar.wireframeQuality reports the LiDAR roof-graph quality band (closure, coverage and plane-fit checks); a “low” value caps the overall Report Confidence at “medium”.
Townhomes and commercial buildings: the LiDAR cloud often spans attached neighbor units, so the AI can measure more roof than the target unit. All edge-length bands are capped to low for these buildings; the lengths themselves are still returned.
Report PDF
Accounts with the AI Instant Measure Report PDF enabled can ask for a PDF alongside the JSON: add “report”: { “pdf”: true } to any v3 request. The PDF is the same multi-page AI Instant Measure Report shown to Instant Roofer contractors (property overview with aerial imagery, measurements and waste calculation, attached units for townhomes, segment lengths with the 2D roof-line diagram, roof facets with the facet identification diagram, and the disclaimer), rendered from this measurement. It is included in the AI measurement price.
cURL (measurement with report PDF)
curl -X POST https://v5.instantroofer.com/v3 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"latitude": 32.865378,
"longitude": -111.677416,
"address": "123 Main St, Casa Grande, AZ 85122",
"report": { "pdf": true },
// Optional: printed as the "Ordering Company" on the report
"contractorName": "ABC Roofing",
// Optional: your own reference, returned as "externalId" (not printed on the report)
"externalId": "JOB-48213"
}'
report (response)
"report": {
"pdfUrl": "https://....amazonaws.com/imr/2026/09/<requestId>.pdf?X-Amz-...",
"expiresAt": "2026-09-23T20:31:07.000Z",
"filename": "IMR-<requestId>.pdf",
"pageCount": 5,
"whiteLabel": false
}
- pdfUrl - signed download link. It expires at expiresAt (24 hours by default), so download the file and store it on your side; the link is not a permanent document URL.
- filename / pageCount - suggested file name (the report id is your request’s X-Request-Id) and number of pages.
- whiteLabel - true when the PDF carries your own branding (see below).
- error - present instead of a link when the PDF could not be produced ({ “code”: “render_failed” | “storage_not_configured” | “storage_failed”, “message”: ”…” }). The measurement itself is still returned with status 200 and pdfUrl is null; retry the request to get a PDF.
address in the request is printed on the report as the measured address; without it the coordinates are shown. contractorName (the same optional field Human Certified orders accept) labels the report’s “Ordering Company”; otherwise your company name is used.
Requesting report.pdf on a key without the PDF enabled, or on v1/v2, returns 403 AI_REPORT_NOT_ENABLED and no measurement is run or billed. Rendering adds a few seconds to the response; allow for it in your HTTP timeout.
White label. Accounts approved for white-labelled AI reports can replace Instant Roofer’s logo and report name with their own from the AI Instant Measure Report White Label page of your customer dashboard (the same logo used on Human Certified Reports, with a separate AI Instant Measure Report name). The disclaimer then states that the report is licensed to your company by Up on the Roof Technologies Inc. DBA Instant Roofer. Use Support if you would like the PDF or white-labelling enabled on your account.
Changes from v2
- New: optional report: { pdf: true } request field and report response object (AI Instant Measure Report PDF, accounts with the feature enabled).
- New: optional externalId request field (your own job / order reference), echoed on the response and available to webhooks as order.external_id. Also accepted on v1 and v2.
- New: top-level confidence (overall Report Confidence) and lidar.wireframeQuality.
- New: measurements.roofHeight, measurements.stories (now an object), measurements.edges, measurements.wastePercent.
- New: measurements.wasteCalculation, the report’s Waste Calculation table (material area and squares at each waste factor, with the recommended column flagged), also on every townhome building. Its twelve columns follow the recommended factor (0% plus eleven consecutive percents centred on it) rather than a fixed list, so read them from options[].
- New: measurements.dripEdge, the eaves plus rakes length (the report’s “Drip Edge” card), also on every townhome building. A sibling of edges, which still carries exactly the seven classes.
- measurements.confidence is a band string (“low” | “medium” | “high”) instead of { score, display }.
- Changed: imagery now carries a plain satellite photo of the measured building (on by default, opt out with “imagery”: { “satellite”: false }) instead of the AI’s mapWithOutline render.
- Removed: imagery.mapWithOutline and buildingPredictions (townhome / commercial / complexity probabilities). measurements.complexity and wastePercent remain.
- resultOptions is no longer accepted; the image-model and raw-data options are fixed off.
- sqft.measured uses a pitch-mix weighted surface factor and squares is rounded to the nearest third; v2 values are unchanged.
- New: sqft.shingle (roof area at 2/12 pitch or steeper, the base sqft.suggested, squares and wasteCalculation are built on) and sqft.flat (roof area at 1/12 pitch or flatter, the report’s “Total Flat Roof Area”, which those numbers leave out). Also on every townhome building.
- v1 and v2 remain available on their existing routes for keys that have them enabled.
Possible error responses
Measurement errors use the envelope { “version”: “v3”, “error”: { “code”: ”…”, “message”: ”…” } }.
- 400 - Invalid or missing coordinates (invalid_coordinates), or an externalId longer than 128 characters (INVALID_EXTERNAL_ID)
- 401 - Missing/expired/invalid API key
- 402 - Billing setup or report credits required, or Human Certified Report usage limit reached
- 403 - v3 is not enabled for this API key (MEASURE_API_VERSION_NOT_ENABLED), the report PDF is not enabled for this API key or was requested on v1/v2 (AI_REPORT_NOT_ENABLED), or Human Certified Reports are not enabled for this API key
- 404 - Roof not found (roof_not_found). Please make sure your latitude/longitude is in the center of a building.
- 408 - Measurement timed out (time_out)
- 409 - Duplicate Human Certified Report request for the same coordinates within the duplicate window
- 422 - Roof too large or irregular (roof_too_large)
- 429 - Rate limit exceeded
- 500 - Internal measurement error (cannot_measure)
- 502 - Upstream forwarding error
- 504 - Upstream timeout
Human Certified Reports
Human Certified Reports can be requested from the v3 endpoint with reportType: “human”. They use the Human Certified Report workflow instead of the AI Measure v3 response payload.
cURL (Human Certified Report with optional job context)
Required fields are latitude, longitude, and reportType: “human”. contractorName, customerName, originalAddress, and externalId are optional.
curl -X POST https://v5.instantroofer.com/v3 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"latitude": 32.865378,
"longitude": -111.677416,
"reportType": "human",
// Optional job context:
"contractorName": "ABC Roofing",
"customerName": "Jane Smith",
"originalAddress": "123 Main St, Casa Grande, AZ 85122",
// Optional: your own job / order reference, echoed back and sent to your webhooks
"externalId": "JOB-48213"
}'
Human Certified Report optional fields
- contractorName - optional contractor or company name for the job, up to 255 characters
- customerName - optional homeowner/customer name, up to 255 characters
- originalAddress - optional source property address entered by the customer, up to 1000 characters
- externalId - optional reference from your own system (job, lead or order ID), up to 128 characters. Returned as externalId on the acceptance response and available to your webhooks as order.external_id; not shown on the report and not checked for uniqueness
These fields are only used for Human Certified Report workflow context. Latitude and longitude remain required and are still the source of truth for where the report is drawn.
Human Certified Report accepted response
{
"ok": true,
"code": "HUMAN_QUEUED",
"message": "Human report request accepted.",
"requestId": "generated-request-id",
"humanReportId": "generated-human-report-id",
"companyId": "your-api-key-id",
"companyName": "Your Company",
"coords": {
"latitude": 32.865378,
"longitude": -111.677416
},
"contractorName": "ABC Roofing",
"customerName": "Jane Smith",
"originalAddress": "123 Main St, Casa Grande, AZ 85122",
"externalId": "JOB-48213"
}
Webhooks for Human Certified Measure Report Completions & Rejections (June 2026)
Human Certified Report webhooks notify your system when report files are ready, or when a Human Certified Report fails. Webhooks are configured per API key and per report format: CSV, PDF, HTML, and XML.
MINIMUM REQUIRED PAYLOAD FIELDS SHOULD BE:
requestID - dynamic - order.request_id
url - dynamic - report.url
status - dynamic - order.status_code

Only enabled webhook types with a URL are sent. A completed report can trigger more than one webhook if multiple report formats are enabled.
You can setup your webhooks in your customer dashboard. You can also email support for us to add them.
When Webhooks Are Sent
When a Human Certified Report completes, Instant Roofer sends one webhook for each enabled report format that has a generated file URL.
When a Human Certified Report fails, Instant Roofer sends enabled report-format webhooks with the failure status and no report URL. Supported report formats are csv, pdf, html, and xml.
Delivery Request
Instant Roofer sends a JSON POST request to the configured webhook URL. The request uses Content-Type: application/json.
Authentication Headers
You can configure a bearer token for each report type.
The default auth header name is Authorization.
If the token does not start with Bearer , Instant Roofer automatically prefixes it.
You can also configure extra static headers as JSON.
Timeout
The default webhook request timeout is 8 seconds. Admins can configure a timeout from 1 to 60 seconds per report type.
Payload Fields
Payloads are configurable. Admins choose the outgoing JSON keys and map each key to either a static value or one of the dynamic values below.
Dynamic Values
- order.id - Human Certified Report order ID.
- order.request_id - API request ID returned when the report was queued.
- order.api_key_id - API key ID associated with the request.
- order.latitude - requested latitude.
- order.longitude - requested longitude.
- order.contractor_name - optional contractor/company name submitted with the request.
- order.customer_name - optional homeowner/customer name submitted with the request.
- order.original_address - optional source property address submitted with the request.
- order.status - current Human Certified Report status, such as completed or failed.
- order.status_code - final status code for the report workflow.
- order.failure_reason - failure reason when the report fails.
- report.type - report format for this webhook: csv, pdf, html, or xml.
- report.url - generated report file URL when available.
Example Payload
The example below assumes you configure the event as a static value and map the remaining fields from dynamic values.
{
"event": "human_report.completed",
"reportType": "pdf",
"reportUrl": "https://example.com/report.pdf",
"humanReportId": "generated-human-report-id",
"requestId": "generated-request-id",
"status": "completed",
"latitude": 32.865378,
"longitude": -111.677416,
"contractorName": "ABC Roofing",
"customerName": "Jane Smith",
"originalAddress": "123 Main St, Casa Grande, AZ 85122"
}
Example Failure Payload
Failed report webhooks can use the same configured payload shape, with failure-specific fields mapped from the order.
{
"event": "human_report.failed",
"reportType": "pdf",
"humanReportId": "generated-human-report-id",
"requestId": "generated-request-id",
"status": "failed",
"statusCode": 422,
"failureReason": "Human report failed"
}
Recommended Response
Return any 2xx status code after your system safely receives the webhook. Instant Roofer marks the delivery as sent when your endpoint returns 200 through 299.
{
"ok": true
}
Retries and Delivery Status
- sent - your endpoint returned a 2xx response.
- pending - delivery is waiting to be sent.
- sending - delivery is currently in progress.
- failed - delivery failed but is scheduled for retry.
- dead - delivery will not retry automatically.
Retryable Responses
Instant Roofer retries timeouts, network errors, and these status codes: 408, 429, 500, 502, 503, and 504.
Retry Schedule
Retries use this backoff schedule: 15 seconds, 60 seconds, 5 minutes, 30 minutes, and 2 hours. After the final retry is exhausted, the delivery is marked dead.
Non-Retryable Responses
400, 401, 403, and 404 are marked dead because they usually indicate a bad URL, rejected authentication, or an invalid request.
Best Practices
Store webhook payloads idempotently. A retry can deliver the same report event more than once.
- Respond quickly with a 2xx status, then process heavy work asynchronously.
- Use a bearer token or custom auth header to verify requests are from Instant Roofer.
- Log humanReportId, requestId, and reportType for support and reconciliation.
- Do not rely on the webhook alone for billing; Human Certified Reports are billed from Instant Roofer’s report workflow.