Documentation

Estimates API

Render, fetch, approve, and void saved estimates for a project.

GET /api/v1/estimates/{estimateId}/render

Render a saved estimate as HTML. Returns Content-Type: text/html — open directly in a browser tab or embed in a webview.

Response

Raw HTML document (not JSON). On failure, returns 404 Not Found if the estimate doesn't exist for your tenant, or 500 if rendering fails.


GET /api/v1/estimates/{estimateId}

Get a saved estimate's metadata as JSON. Returns the totals breakdown plus the project's currency code, localized tax label, and the estimate's snapshotted tax rate.

Response (200 OK)

{
    "id": 45,
    "projectId": 123,
    "estimateNumber": "EST-2025-007",
    "estimateDate": "2025-04-12T00:00:00Z",
    "validDays": 30,
    "validUntil": "2025-05-12T00:00:00Z",
    "totalAmount": 57295.90,
    "totalEquipment": 41200.00,
    "totalServices": 8500.00,
    "totalShipping": 1250.00,
    "totalTax": 6345.90,
    "taxRate": 8.250,
    "snapshotedAt": "2025-04-12T15:22:00Z",
    "createdAt": "2025-04-12T15:22:00Z",
    "hasSnapshot": true,
    "currency": "USD",
    "taxLabel": "Tax"
}
Currency-aware tax label

taxLabel tracks the project's currency: USD returns "Tax", GBP/EUR return "VAT", CAD/AUD/INR return "GST", JPY returns "Consumption Tax". Display this verbatim — no client-side mapping needed.

Errors

  • 404 Not Found — estimate does not exist for your tenant.

DELETE /api/v1/estimates/{estimateId}

Delete a saved estimate. Refuses to delete an approved estimate — void the approval first.

Response

  • 204 No Content — deleted successfully.
  • 404 Not Found — estimate does not exist for your tenant.
  • 409 Conflict — estimate is approved (has a linked Contract). Void the approval first.

409 Response Body

{
    "error": "estimate_approved",
    "message": "Cannot delete an approved estimate. Void the approval first."
}

POST /api/v1/estimates/{estimateId}/approve

Mark a saved estimate as Approved. Creates a Contract row and flips the underlying Version's status to Contracted.

Response (200 OK)

{
    "id": 88,
    "versionId": 17,
    "approvedAt": "2025-04-18T14:05:00Z",
    "approvedByUserId": "auth0|abc123"
}

Errors

  • 400 Bad Request — estimate cannot be approved (already approved, not the active version, etc.). Body: { "error": "<reason>" }.
  • 401 Unauthorized — missing user context.

POST /api/v1/projects/{projectId}/approve-latest-estimate

Project-keyed approval — looks up the latest saved estimate on the project (or the explicit estimateId in the body, if supplied), then approves it the same way as /api/v1/estimates/{id}/approve. Idempotent: re-approving an already-Contracted version returns the existing Contract with alreadyApproved: true instead of erroring.

Request body (all fields optional)

{
    "estimateId": 1234
}

Omit the body (or pass {}) to approve the latest saved estimate on the project's active version.

Response (200 OK)

{
    "success": true,
    "projectId": 42,
    "estimateId": 1234,
    "estimateNumber": "EST-2026-042",
    "contractId": 567,
    "versionId": 789,
    "versionLabel": "V2",
    "totalAmount": 57295.90,
    "currency": "USD",
    "approvedAt": "2026-05-28T18:42:00Z",
    "alreadyApproved": false
}

Errors

  • 404 Not Found — no saved estimate on the project. Body: { "error": "no_draft_estimate", "projectId": 42, "estimateId": null, "message": "..." }.
  • 404 Not Found — explicit estimateId doesn't exist. Body: { "error": "estimate_not_found", ... }.
  • 400 Bad Request — explicit estimateId belongs to a different project. Body: { "error": "estimate_project_mismatch", ... }.
  • 400 Bad Request — target version is Archived. Body: { "error": "version_archived", "versionId": 789, ... }.
  • 401 Unauthorized — missing user context.

POST /api/v1/projects/{projectId}/active-version-estimate-html

Returns the rendered HTML of the latest saved estimate ON THE PROJECT'S ACTIVE VERSION (or an explicit estimateId if supplied). The HTML comes from the SavedEstimate's stored snapshot — not a fresh re-render — so the output matches what was approved at save time. If the project has no ActiveVersionId (pre-version-system project), falls back to the latest saved estimate across the whole project.

Request body (all fields optional)

{
    "estimateId": 1234
}

Response (200 OK)

{
    "success": true,
    "projectId": 42,
    "estimateId": 1234,
    "estimateNumber": "EST-2026-042",
    "html": "<!DOCTYPE html><html>...</html>"
}

Errors

  • 404 Not Found — no saved estimate. Body: { "error": "no_draft_estimate", ... }.
  • 404 Not Found — explicit estimateId doesn't exist. Body: { "error": "estimate_not_found", ... }.
  • 400 Bad Request — explicit estimateId belongs to a different project. Body: { "error": "estimate_project_mismatch", ... }.
  • 500 Internal Server Error — render failed. Body: { "error": "render_failed", "estimateId": 1234, "message": "..." }.

POST /api/v1/estimates/{estimateId}/void

Void an approved estimate. Deletes the linked Contract and flips the Version back to Draft. Allowed only when the Contract has zero activity — no approved Change Orders and no saved invoices.

Response

  • 200 OK — void succeeded.
  • 422 Unprocessable Entity — Contract has live activity. Body includes counts:
{
    "error": "Contract has approved change orders or saved invoices...",
    "approvedChangeOrders": 1,
    "invoices": 2
}
  • 400 Bad Request — estimate is not in an approved state. Body: { "error": "<reason>" }.
  • 401 Unauthorized — missing user context.

POST /api/v1/projects/{projectId}/void-latest-estimate

Project-keyed void — resolves the project's latest saved estimate (or the explicit estimateId in the body, if supplied) and voids the approval. Same guards as /api/v1/estimates/{id}/void. Idempotent: voiding an already-Draft estimate is a no-op returning alreadyVoided: true.

Request body (all fields optional)

{
    "estimateId": 1234
}

Response (200 OK)

{
    "success": true,
    "projectId": 42,
    "estimateId": 1234,
    "estimateNumber": "EST-2026-042",
    "alreadyVoided": false
}

Errors

  • 404 Not Found — no saved estimate. Body: { "error": "no_draft_estimate", ... }.
  • 400 Bad Request — explicit estimateId belongs to a different project. Body: { "error": "estimate_project_mismatch", ... }.
  • 422 Unprocessable Entity — Contract has approved Change Orders or live invoices. Body: { "error": "void_blocked", "approvedChangeOrders": N, "invoices": M, "message": "..." } — caller must void the downstream COs/invoices first.
  • 401 Unauthorized — missing user context.