[Webhook] Estimate updated
This is not an API endpoint you call. This documents the webhook payload sent to your callbackUrl whenever an estimate's status changes.
Webhook Details:
- Trigger: Any estimate status change (COMPLETED, FAILED, HUMAN_REVIEW, PROCESSING)
- Destination: Your callbackUrl provided in the estimate request
- Method: POST
- Headers:
- Content-Type: application/json
- User-Agent: Handoff-Enterprise-Webhook/1.0
- X-Webhook-Attempt: <attempt_number>
- Authorization: Bearer <access_token> (if outbound authentication is configured)
- Expected Response: HTTP 2xx status code within 10 seconds
- Retry Policy: Exponential backoff (1m, 5m, 15m, 1h, 6h)
Payload Structure:
- event: Always estimate.updated
- status: The current status (COMPLETED, FAILED, HUMAN_REVIEW, PROCESSING)
- result: Present when status is COMPLETED or HUMAN_REVIEW, null otherwise
- error: Present when status is FAILED, null otherwise
- files: The input files you uploaded for this estimate
- takeoffFiles: Generated takeoff deliverables ready to download. Non-empty only on COMPLETED/HUMAN_REVIEW for blueprint estimates
- timestamp: ISO 8601 timestamp of the status change
Status Meanings:
- COMPLETED: Estimate finished successfully, result contains the estimate data
- FAILED: Estimate processing failed, error contains error details
- HUMAN_REVIEW: Estimate requires human review
- PROCESSING: Estimate files were processed and the estimate is being generated
Takeoff Deliverables: For blueprint estimates on the takeoff delivery contract, a COMPLETED or HUMAN_REVIEW webhook always carries exactly one TAKEOFF_MARKUP entry in takeoffFiles, resolved before the success status is recorded. Entries are discriminated on status:
- READY — full metadata with a stable id (unchanged across webhook retries) and a checksum for integrity verification; fetch the bytes via GET /v1/estimate/{estimateJobId}/files/{fileId}/download
- FAILED — the file could not be produced and will not arrive; the entry carries only fileType and error: { code, message }. The job itself still succeeds — act on the estimate without the attachment.
A presigned URL is deliberately not embedded here because this payload is signed when queued and retried for up to six hours, so an embedded URL could arrive already expired. The same takeoffFiles array is returned by GET /v1/estimate/{estimateJobId}, so a missed webhook never strands the deliverable.
Error Codes (when status is FAILED):
- FILE_PROCESSING_FAILED: One or more files failed to process (details contains failed file IDs)
- FILE_EXTRACTION_NO_ITEMS: File extraction succeeded but no items were found in the provided files (details contains processed file IDs)
- FILE_NO_CONTENT: The uploaded file(s) contain no readable content (e.g. a blank page) and the request carried no prompt
- CONFIGURATION_ERROR: Client configuration is missing or invalid
- AI_ESTIMATE_GENERATION_ERROR: AI estimate generation failed
- MARKUP_GENERATION_FAILED: Never a job-level status code — it appears only inside a takeoffFiles entry's error (see Takeoff Deliverables above)
- INTERNAL_ERROR: Internal system error
Versioning: Each time an estimate is updated/regenerated, a new version is created and a webhook is sent with the new version number. Use ?version=N on the resultUrl to fetch specific historical versions.
Security:
- Validate resultUrl domain is api.handoff.ai
- Fetch full results from resultUrl using your access token
- Implement idempotency using estimateJobId + version
Request
- Base URL: https://api.handoff.ai
- URL: https://api.handoff.ai/webhooks/estimate
- Auth: none declared
Request body
Example request
{
"event": "estimate.updated",
"estimateJobId": "e1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c",
"externalId": "EST-12345",
"type": "MATERIAL_LIST",
"fileIds": [
"f1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c",
"f2b3c4d5-e6f7-4a8b-9c0d-1e2f3a4b5c6d"
],
"files": [
{
"id": "f1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c",
"filename": "floor_plan.pdf",
"contentType": "application/pdf",
"sizeBytes": 1048576,
"confidence": "HIGH"
},
{
"id": "f2b3c4d5-e6f7-4a8b-9c0d-1e2f3a4b5c6d",
"filename": "materials.xlsx",
"contentType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"sizeBytes": 524288,
"confidence": null
}
],
"takeoffFiles": [
{
"id": "a7c31c2b-9f4e-5d3a-8b1c-2e6f4a5b6c7d",
"filename": "takeoff-markup.pdf",
"fileType": "TAKEOFF_MARKUP",
"contentType": "application/pdf",
"sizeBytes": 2456789,
"status": "READY",
"checksum": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
],
"address": "Austin, TX 78701",
"prompt": "Create an estimate for framing and painting.",
"customerReference": "CUST-ORD-789",
"callbackUrl": "https://partners.example.com/webhooks/estimate",
"status": "COMPLETED",
"resultUrl": "https://api.handoff.ai/v1/estimate/e1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c",
"version": 1,
"metadata": {
"includeMEP": true,
"source": "takeoff-api",
"projectId": "proj-123"
},
"timestamp": "2024-01-15T10:30:00.000Z",
"result": {
"items": [
{
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"group": "Framing",
"productName": "2x6 PT Bottom Plates at First Floor",
"quantity": 3100,
"rateAmount": 50,
"totalAmount": 155000,
"description": "Pressure treated bottom plates for first floor framing",
"category": "Material",
"unit": "LINEAR_FOOT",
"unitAbbreviation": "LF",
"currency": "USD",
"attributes": {
"wasteFactor": 0.1,
"location": "First Floor",
"building": "Main House",
"categoryName": "Lumber & Composites",
"categoryCode": "L",
"extractedLabel": "200 - 2x6 PT Bottom Plates (1234567)",
"contextualSection": "Framing",
"partnerItemId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
},
"productMatch": {
"supplier": "Lowe's",
"productName": "Severe Weather 2-in x 6-in x 8-ft #2 Prime Southern yellow pine Above ground Pressure Treated Lumber",
"productUrl": "https://www.lowes.com/pd/Severe-Weather-Common-2-in-x-6-in-x-8-ft-Actual-1-5-in-x-5-5-in-x-8-ft-2-Prime-Treated-Lumber/4564606",
"productBrand": "Severe Weather",
"attributes": {
"productId": "4564606",
"productUnitRatio": {
"EA": 1,
"LF": 8
},
"conversionRatio": 8,
"quantitySellableRound": 427,
"unitSellable": "EA",
"rateAmountSellable": 3200,
"totalAmountSellable": 136640,
"itemNumber": "196037",
"modelNumber": "OG2P20608-AG",
"productImageUrl": "https://handoff.ai/exampleimage/4564606/4564606.jpg",
"partialMatch": true
}
},
"districtAvailability": "in-store"
}
],
"summary": {
"description": "This estimate covers first floor framing including walls, headers, and floor joists for a 2,500 sq ft residential addition.",
"totalItems": 25,
"matchedItems": 23,
"unmatchedItems": 2,
"matchingPercentage": 92,
"totalAmount": 1550000,
"currency": "USD"
},
"message": "I increased the drywall quantity to 1,200 SF and added two interior doors as requested."
},
"error": {
"code": "FILE_PROCESSING_FAILED",
"message": "One or more files failed to process",
"details": "Failed file IDs: file-123, file-456"
}
}Response
Webhook received successfully
Example response
{
"success": true
}Changes
No recorded changes to this operation across all 1 revision of this API.