Estimates

[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
post/webhooks/estimate

Request

  • Base URL: https://api.handoff.ai
  • URL: https://api.handoff.ai/webhooks/estimate
  • Auth: none declared

Request body

event'estimate.updated' required

Webhook event type

estimateJobIdstring uuid required

Unique identifier for the estimate generation job

externalIdstring nullable required

Partner's internal estimate ID provided during job creation

type'MATERIAL_LIST' | 'BLUEPRINT' required

Type of estimate request: 'MATERIAL_LIST' for material lists or 'BLUEPRINT' for blueprints

fileIdsstring[] required

List of file IDs used to generate this estimate (deprecated: use 'files' for full metadata)

addressstring nullable required

Address or ZIP code for regional availability and pricing

promptstring nullable required

Special instructions and scope of work for the estimate (e.g., 'Create an estimate for framing and painting.')

customerReferencestring nullable required

Partner's internal reference/order number for tracking specific customers

callbackUrlstring nullable required

Webhook URL for job completion notification

status'PENDING' | 'EXTRACTING' | 'LABELING' | 'AGENTIC_TAKEOFF' | 'INGESTING' | 'PROCESSING' | 'HUMAN_REVIEW' | 'COMPLETED' | 'FAILED' | 'CANCELED' required

Job status. Possible values: PENDING, EXTRACTING, LABELING, AGENTIC_TAKEOFF, INGESTING, PROCESSING, HUMAN_REVIEW, COMPLETED, FAILED, CANCELED

resultUrlstring uri required

URL to retrieve full job results via GET request

versioninteger required

Version number of the estimate (1, 2, 3, ...)

metadataobject

Custom metadata provided during job creation

timestampstring date-time required

ISO 8601 timestamp of the status change

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

successboolean required

Example response

{
  "success": true
}

Changes

No recorded changes to this operation across all 1 revision of this API.