Skip to content
TruTecDevelopers
Get an API key

Estimates & proposals

Read estimates as structured data, and build them from your own systems — create, add and price lines, issue versions, mark them won or lost, and start change orders.

View as Markdown

An estimate is the priced bid for a job: areas of the site, line items in each area (built from your catalog), and the totals TruTec's pricing engine works out. Through the API you can read every estimate and build or change one the same way an estimator does on the estimating sheet.

The customer-facing document an estimate becomes is a proposal. Creating, editing, sending and tracking proposals is on Proposals.

How it works#

Every number comes from TruTec's pricing engine. You send quantities, catalog services and rates; TruTec prices. Each write re-runs the engine and returns the estimate's fresh totals, so what you read always matches the estimating sheet. Reads never re-price: they return the numbers as last calculated. POST /v1/estimates/{id}/recalculate re-prices on demand.

Writes behave exactly like the app. They act as the user who created the API key, with that user's estimating role (see Authentication):

Role Can
Owner or estimating admin Everything, including overriding the profit-floor approval when issuing.
Estimator Build, price, issue, and mark estimates won or lost.
Sales Create estimates and add or change unit-priced lines.
Viewer Read only. Writes return 403 with the reason.

The app's rules apply too: a sent estimate keeps its frozen numbers, a won estimate needs a change order for new scope, a quantity you type replaces the takeoff's number and is flagged as manual, and every change lands in the estimate's revision log under that user.

Nothing reaches the customer from these endpoints. Issuing a version freezes numbers but sends nothing; only POST /v1/proposals/{id}/send emails a customer.

Typical flow:

  1. POST /v1/estimates with a job_id (optionally a template_id).
  2. POST /v1/estimates/{id}/line-items for each catalog service and quantity.
  3. PATCH /v1/estimates/{id} to set the markup, tax or price list. Adjust lines with PATCH .../line-items/{line_item_id}.
  4. POST /v1/estimates/{id}/issue to freeze version 1.
  5. POST /v1/estimates/{id}/proposal, then POST /v1/proposals/{id}/send (Proposals).
  6. POST /v1/estimates/{id}/mark-won when the customer accepts.

Estimates and versions#

When an estimator issues an estimate, TruTec freezes a numbered version of it: version 1 for the first one sent, version 2 for the next revision, and so on. The live estimate can keep changing afterwards; issued versions never change.

  • GET /v1/estimates/{id}?version=N returns version N exactly as issued. GET /v1/estimates/{id}/versions lists every version with its totals and issue date.
  • Each line item's allocated_price is the price the proposal shows for that line, after job-level money (mobilization, overhead, contingency, bond, rounding) is spread across the lines.
  • The takeoff map is at GET /v1/jobs/{id}/exports/proposal_map (Jobs).
  • Webhooks fire when an estimate is issued, revised, won or lost, and when a proposal is sent, viewed, accepted or declined (Webhooks & events).

Response format#

GET /v1/estimates and /revisions return {"data": [...], "pagination": {"total", "limit", "offset"}}. GET /v1/estimates/{id} returns the estimate object directly, not wrapped in data; every write returns it wrapped in data. Errors return JSON with an error string, and a code where one helps (issue_blocked, approval_required).

Estimate timestamps are UTC in ISO 8601 with a Z suffix, to the second, for example 2026-10-08T21:14:09Z. Money is in dollars, rounded to cents. Rates are fractions (0.25 = 25%).

Note

Partner keys that act on behalf of several workspaces must also pass firm_id (query string, or JSON body on writes).

The estimate object#

Field Type Description
id integer TruTec id.
doc_number integer or null Customer-facing document number. The estimate, its proposal and its invoices share it.
name string Estimate name.
status string draft (being built), estimate (issued to the customer), won, lost or change_order.
job_id integer or null The job this estimate belongs to.
parent_estimate_id integer or null For a change-order estimate, the estimate it changes.
is_change_order boolean True for change-order estimates.
current_version integer or null Latest issued version number. Null if never issued.
version integer or null Null for the live estimate. The version number when you asked for ?version=N.
issued_at, issued_by_user_id string, integer When and by whom this version was issued. Only present with ?version=N.
priced boolean False if the estimate has never been priced. totals is then null.
awarded_at, archived_at, created_at, updated_at string or null UTC timestamps.
totals object or null See below.
areas array Areas and their line items. Only on single-estimate responses.
pricing object The estimate's own pricing inputs. Only on single-estimate responses; see below.

totals:

Field Description
price Customer total, including tax.
subtotal price minus tax.
tax Sales tax.
cost Total cost, including job-level costs.
margin Profit in dollars.
margin_pct Margin as a fraction of price (0.2814 means 28.14%).
optional_total Price of optional add-ons. Not included in price.

Use totals for the bid. Line costs do not include job-level costs such as mobilization, so line items will not sum to totals.cost.

pricing (what PATCH /v1/estimates/{id} sets; null means the workspace default applies):

Field Description
markup_method markup (on cost) or margin (of price).
markup_rate The estimate-wide rate, a fraction (0.25 = 25%).
contingency_rate Contingency added before markup, or null when off.
tax_rate Sales tax rate, or null for the workspace's.
minimum_charge Smallest total this estimate sells for, in dollars.
price_list_id The price column it prices from (Catalog).

The line item object#

Each area has id, name, quantity_source (manual, takeoff or assessment), takeoff_id (the takeoff its quantities come from, if any) and services (its line items). Inactive areas and lines are left out, and so are lines the estimator excluded from the bid.

Field Type Description
id integer Line id.
name string Line name as written on the estimate.
service_type string or null The catalog item this line was built from.
category string or null That catalog item's group.
assembly_id integer or null Catalog item id.
estimating_mode string detailed or unit_costing.
quantity, unit number, string Quantity and unit, for example 48250 and SF.
unit_cost, unit_price number or null Cost and price per unit.
price, cost, margin, margin_pct number or null Line totals. margin_pct is a fraction of price.
allocated_price number or null The price the proposal shows for this line, after job-level money is spread.
is_optional boolean An optional add-on. Not in totals.price.
hide_on_proposal boolean Priced into the bid but not listed on the proposal.
parent_service_id integer or null Parent line, for sub-lines.
cost_code string or null Your cost code for the line.
is_bundle_master boolean This line's price and cost are the roll-up of its sub-lines.
bundle_child_service_ids array The sub-lines rolled into this line.
bundle_parent_service_id integer or null The bundle line this line rolls into.
area_id integer The area the line is in.
quantity_source string inherited (from the takeoff, assessment or catalog), manual (typed) or derived (follows another line).
pricing_method, pricing_rate string, number or null The line's own markup or margin, when it has one.
price_override number or null A customer price pinned on this line; the difference is spread across the other lines.
proposal_group string or null Lines sharing a group print as one proposal line.
proposal_text string or null This line's proposal wording. null uses the catalog service's.
what_to_expect string or null HTML printed under the line on the proposal ("What to expect"): the line's own, else the catalog wording frozen when it was added.
internal_scope string or null HTML scope for this line that goes with quote requests to subcontractors. null uses the catalog service's internal description.
notes string or null The estimator's note on the line.

Warning

A bundle line already includes its sub-lines. When you add up lines, sum either the bundle lines or their sub-lines, not both.

The cost line object#

A line built in detail is costed from cost lines: the crews, equipment, materials, trucking, subcontractors, misc items and overhead its recipe brought in. GET /v1/estimates/{id}/line-items/{line_item_id} returns them under cost_lines.

Field Type Description
id integer Cost line id.
line_item_id integer The line it belongs to.
type string crew, equipment, material, trucking, subcontractor, misc, overhead or adjustment.
catalog_item_id integer or null The catalog cost item it was copied from.
name string
quantity, unit number, string What the engine worked out, for example 50 GAL.
unit_cost number or null This estimate's unit cost (editing it never changes the catalog).
cost, price number or null In dollars.
quantity_formula string How quantity follows the line's quantity, for example qty * 0.05.
manual_quantity number or null A typed quantity that replaces the formula.
waste_factor number or null This line's waste override, a fraction.
pricing_method, pricing_rate string, number or null A markup pinned on this cost line (null follows the line).
billing string billed, or internal_only (costed but not billed).
is_alternate boolean A comparison option, costed but left out of every total.
suppressed boolean Costed but left out of every total.
hauls array Trucking only: what the truck hauls — asphalt, millings, spoil.
automatic_driving_time boolean Trucking only: the line updates its own driving time from the plant, job and dump site.
route object or null Trucking only: the trip its dump site chose — kind plant_round_trip (the dump site is the plant), three_leg (plant → job → dump site → plant, with legs_hr) or dump_round_trip (job ↔ dump site), plus the plant, job and dump-site addresses. null = a plain plant round trip.
dump_material string or null Misc only: millings or spoil when the line is a dump fee.
dump_site object or null Misc dump fees only: vendor_id and name of the dump site picked on the line.

Read estimates#

List estimates#

GET /v1/estimates

Permission: Read jobs

Returns summary rows (no line items), most recently updated first. With updated_after, results are sorted by updated_at, oldest first, so you can page forward. Archived estimates are left out unless you ask for them.

Name Type Required Description
job_id integer No Only this job's estimates.
status string No draft, estimate, won, lost or change_order.
updated_after string No ISO 8601 timestamp. Only estimates changed after it.
include_archived boolean No true to include archived estimates.
limit integer No Page size. Default 50, maximum 100.
offset integer No Rows to skip. Default 0.
curl "https://trutec.ai/api/v1/estimates?job_id=70412" \
  -H "X-API-Key: sk_your_api_key_here"
{
  "data": [
    {
      "id": 9840,
      "doc_number": 1412,
      "name": "Northgate Plaza Sealcoat",
      "status": "won",
      "job_id": 70412,
      "parent_estimate_id": null,
      "is_change_order": false,
      "current_version": 2,
      "awarded_at": "2026-10-09T13:12:47Z",
      "archived_at": null,
      "created_at": "2026-10-01T16:02:55Z",
      "updated_at": "2026-10-09T13:12:47Z",
      "totals": {
        "price": 48500.0,
        "subtotal": 48500.0,
        "tax": 0.0,
        "cost": 34852.1,
        "margin": 13647.9,
        "margin_pct": 0.281400,
        "optional_total": 1850.0
      },
      "version": null,
      "priced": true
    }
  ],
  "pagination": { "total": 1, "limit": 50, "offset": 0 }
}

Errors: 400 status must be one of draft, estimate, won, lost, change_order. 400 updated_after must be an ISO 8601 timestamp. 403 API key lacks job read permission.

Get an estimate#

GET /v1/estimates/{id}

Permission: Read jobs

Returns one estimate with totals, areas and line items. Add version=N to get issued version N, frozen as the customer received it.

Name Type Required Description
id integer (path) Yes Estimate id.
version integer No Issued version number. Omit for the live estimate.
curl "https://trutec.ai/api/v1/estimates/9840?version=2" \
  -H "X-API-Key: sk_your_api_key_here"
{
  "id": 9840,
  "doc_number": 1412,
  "name": "Northgate Plaza Sealcoat",
  "status": "estimate",
  "job_id": 70412,
  "parent_estimate_id": null,
  "is_change_order": false,
  "current_version": 2,
  "awarded_at": "2026-10-09T13:12:47Z",
  "archived_at": null,
  "created_at": "2026-10-01T16:02:55Z",
  "updated_at": "2026-10-09T13:12:47Z",
  "version": 2,
  "issued_at": "2026-10-06T19:30:02Z",
  "issued_by_user_id": 311,
  "priced": true,
  "totals": {
    "price": 48500.0, "subtotal": 48500.0, "tax": 0.0,
    "cost": 34852.1, "margin": 13647.9, "margin_pct": 0.2814,
    "optional_total": 1850.0
  },
  "areas": [
    {
      "id": 22018,
      "name": "Front lot",
      "services": [
        {
          "id": 140233,
          "name": "Sealcoat, 2 coats",
          "service_type": "Sealcoat",
          "category": "Sealcoating",
          "assembly_id": 5120,
          "estimating_mode": "detailed",
          "quantity": 48250.0,
          "unit": "SF",
          "unit_cost": 0.4512,
          "unit_price": 0.6275,
          "price": 30276.88,
          "cost": 21770.4,
          "margin": 8506.48,
          "margin_pct": 0.280956,
          "allocated_price": 31940.12,
          "is_optional": false,
          "hide_on_proposal": false,
          "parent_service_id": null,
          "cost_code": "01-SEAL",
          "is_bundle_master": false,
          "bundle_child_service_ids": [],
          "bundle_parent_service_id": null
        }
      ]
    }
  ]
}

(Response trimmed to one line item.)

Errors: 400 version must be an integer. 404 Estimate not found. 404 Version 3 not found for this estimate.

List issued versions#

GET /v1/estimates/{id}/versions

Permission: Read jobs

Every issued version, oldest first, with its totals. Not paginated.

curl "https://trutec.ai/api/v1/estimates/9840/versions" \
  -H "X-API-Key: sk_your_api_key_here"
{
  "data": [
    {
      "number": 1,
      "issued_at": "2026-10-02T15:10:44Z",
      "issued_by_user_id": 311,
      "totals": { "price": 51200.0, "subtotal": 51200.0, "tax": 0.0, "cost": 36010.55, "margin": 15189.45, "margin_pct": 0.296669, "optional_total": 1850.0 }
    },
    {
      "number": 2,
      "issued_at": "2026-10-06T19:30:02Z",
      "issued_by_user_id": 311,
      "totals": { "price": 48500.0, "subtotal": 48500.0, "tax": 0.0, "cost": 34852.1, "margin": 13647.9, "margin_pct": 0.2814, "optional_total": 1850.0 }
    }
  ],
  "estimate_id": 9840
}

Errors: 404 Estimate not found.

List revisions#

GET /v1/estimates/{id}/revisions

Permission: Read jobs

The estimate's edit log, newest first: who changed what, and when. summary is a short plain-English description.

Name Type Required Description
id integer (path) Yes Estimate id.
limit integer No Page size. Default 50, maximum 100.
offset integer No Rows to skip. Default 0.
curl "https://trutec.ai/api/v1/estimates/9840/revisions?limit=2" \
  -H "X-API-Key: sk_your_api_key_here"
{
  "data": [
    {
      "id": 771204,
      "action": "lifecycle.issue",
      "actor_user_id": 311,
      "occurred_at": "2026-10-06T19:30:02Z",
      "summary": "Issued version 2 ($48,500.00)",
      "undone": false
    },
    {
      "id": 771188,
      "action": "service.update",
      "actor_user_id": 311,
      "occurred_at": "2026-10-06T19:22:40Z",
      "summary": "service update: qty_value",
      "undone": false
    }
  ],
  "pagination": { "total": 64, "limit": 2, "offset": 0 }
}

undone is true when the estimator undid that edit.

Errors: 404 Estimate not found.

Create and change estimates#

Create an estimate#

POST /v1/estimates

Permission: Write jobs (role: owner, admin, estimator or sales)

Starts a draft estimate on a job. Without a template it is the workspace's standard sheet: one area, plus any catalog rows marked "on every new estimate" and, when the job has a site assessment, its services. With template_id it copies that template's areas, lines and pricing.

Name Type Required Description
job_id integer Yes The job.
template_id integer No From GET /v1/estimate-templates.
name string No Defaults to " — Estimate".
takeoff_id integer No One of the job's takeoffs (measurement ids from GET /v1/measurements). Adds an area whose quantities come from it.
curl -X POST "https://trutec.ai/api/v1/estimates" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"job_id": 70412, "name": "Front lot reseal"}'
{
  "data": {
    "id": 9841,
    "doc_number": 1413,
    "name": "Front lot reseal",
    "status": "draft",
    "job_id": 70412,
    "parent_estimate_id": null,
    "is_change_order": false,
    "current_version": null,
    "awarded_at": null,
    "archived_at": null,
    "created_at": "2026-10-12T14:56:38Z",
    "updated_at": "2026-10-12T14:56:39Z",
    "totals": { "price": 0.0, "subtotal": 0.0, "tax": 0.0, "cost": 0.0, "margin": 0.0, "margin_pct": 0.0, "optional_total": 0.0 },
    "version": null,
    "priced": true,
    "pricing": { "markup_method": "margin", "markup_rate": 0.0, "contingency_rate": null, "tax_rate": null, "minimum_charge": null, "price_list_id": null },
    "areas": [
      { "id": 22101, "name": "Area 1", "quantity_source": "manual", "takeoff_id": null, "services": [] }
    ]
  }
}

Errors: 400 job_id is required (an integer job id). 400 Unknown fields: …. 403 API key lacks job write permission, or the key's user can't create estimates. 404 Job not found, Estimate template not found.

Update an estimate#

PATCH /v1/estimates/{id}

Permission: Write jobs (role: owner, admin or estimator for pricing; sales may rename)

Rename the estimate or change its pricing inputs. Only the fields you send change; the estimate re-prices.

Name Type Description
name string Up to 200 characters. Also renames the proposal.
markup_method string markup or margin.
markup_rate number Fraction. A margin must be below 1.
contingency_rate number or null Fraction; null or 0 turns contingency off.
tax_rate number or null Fraction; null uses the workspace's rate.
minimum_charge number or null Dollars; null uses the workspace's.
price_list_id integer or null A price list from GET /v1/catalog/price-lists; null for the default. Re-reads every catalog-priced line from that column.
curl -X PATCH "https://trutec.ai/api/v1/estimates/9841" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"markup_method": "margin", "markup_rate": 0.28, "tax_rate": 0.0825}'

Returns 200 with the estimate.

Errors: 400 markup_method must be markup or margin, A margin rate must be below 1 (100%). 404 Estimate not found, Price list not found. 409 when the estimate is locked (for example sent, and the change would move its price).

Archive an estimate#

DELETE /v1/estimates/{id}

Permission: Write jobs and Delete records (role: owner, admin or estimator)

Archives, never deletes: the estimate leaves active lists (and GET /v1/estimates unless include_archived=true) and keeps every number. Returns 204.

Restore an estimate#

POST /v1/estimates/{id}/restore

Permission: Write jobs (role: owner, admin or estimator)

Brings an archived estimate back. Returns 200 with the estimate.

Duplicate an estimate#

POST /v1/estimates/{id}/duplicate

Permission: Write jobs (role: owner, admin, estimator or sales)

Copies an estimate as a new draft with its own document number: areas, lines, cost lines and pricing, exactly as the app's "Save as template" captures them. Takeoff links are not copied (they belong to the original job).

Name Type Description
job_id integer The job for the copy. Defaults to the original's job.
name string Defaults to " (copy)".

Returns 201 with the new estimate.

Re-price an estimate#

POST /v1/estimates/{id}/recalculate

Permission: Write jobs

Runs the pricing engine now, as opening the estimating sheet does (catalog changes to frozen lines still need the estimator's "update prices"). Returns 200 with the estimate and its fresh totals.

Areas#

Add an area#

POST /v1/estimates/{id}/areas

Permission: Write jobs (role: owner, admin, estimator or sales)

Name Type Required Description
name string Yes For example Back lot.
takeoff_id integer No A takeoff on the estimate's job. Quantities for lines in this area come from it.
curl -X POST "https://trutec.ai/api/v1/estimates/9841/areas" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name": "Back lot"}'
{
  "data": {
    "id": 22102,
    "estimate_id": 9841,
    "name": "Back lot",
    "quantity_source": "manual",
    "takeoff_id": null,
    "services": [],
    "estimate_totals": { "price": 3100.0, "subtotal": 3100.0, "tax": 0.0, "cost": 2480.0, "margin": 620.0, "margin_pct": 0.2, "optional_total": 0.0 }
  }
}

Update an area#

PATCH /v1/estimates/{id}/areas/{area_id}

Permission: Write jobs

name, and takeoff_id (an id to bind a takeoff, null to switch the area to typed quantities). Returns the area.

Remove an area#

DELETE /v1/estimates/{id}/areas/{area_id}

Permission: Write jobs (role: owner, admin or estimator)

Removes the area and its lines from the estimate. The revision log keeps them, and Undo in TruTec brings them back. Returns 204.

Line items#

Get a line item#

GET /v1/estimates/{id}/line-items/{line_item_id}

Permission: Read jobs

The line item object plus its cost_lines.

curl "https://trutec.ai/api/v1/estimates/9841/line-items/140260" \
  -H "X-API-Key: sk_your_api_key_here"
{
  "data": {
    "id": 140260,
    "name": "Mill and overlay",
    "service_type": "Mill and overlay",
    "category": "Paving",
    "assembly_id": 5121,
    "estimating_mode": "detailed",
    "area_id": 22101,
    "quantity": 1000.0,
    "unit": "SY",
    "quantity_source": "manual",
    "unit_cost": 21.4,
    "unit_price": 27.82,
    "cost": 21400.0,
    "price": 27820.0,
    "margin": 6420.0,
    "margin_pct": 0.230769,
    "allocated_price": 28150.4,
    "is_optional": false,
    "hide_on_proposal": false,
    "cost_lines": [
      {
        "id": 448812,
        "line_item_id": 140260,
        "type": "material",
        "catalog_item_id": 742,
        "name": "HMA surface mix",
        "quantity": 110.0,
        "unit": "TON",
        "unit_cost": 86.0,
        "cost": 9460.0,
        "price": 12298.0,
        "quantity_formula": "qty * 0.11",
        "manual_quantity": null,
        "waste_factor": null,
        "pricing_method": "markup",
        "pricing_rate": null,
        "billing": "billed",
        "is_alternate": false,
        "suppressed": false
      }
    ]
  }
}

(Response trimmed.)

Errors: 404 Line item not found.

Add a line item#

POST /v1/estimates/{id}/line-items

Permission: Write jobs (role: owner, admin, estimator or sales)

Adds a catalog service, the way the sheet's Add service does: its recipe of crews, equipment and materials is copied onto the line, its subservices come along (unless include_subservices is false), and the estimate re-prices. When the area is bound to a takeoff and you send no quantity, the line starts at the takeoff's measured quantity. Or add a custom line with name.

Name Type Required Description
catalog_service_id integer One of these From GET /v1/catalog/services.
name string One of these A custom line not in the catalog.
quantity number No In the service's unit.
unit string No Defaults to the catalog service's unit.
area_id integer No Defaults to the estimate's first area.
include_subservices boolean No Default true.
unit_cost number No Custom lines only: price per unit.
pricing_mode string No Custom lines only: unit_costing (default) or detailed.
curl -X POST "https://trutec.ai/api/v1/estimates/9841/line-items" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"catalog_service_id": 5120, "quantity": 48250}'
{
  "data": {
    "id": 140261,
    "name": "Sealcoat, 2 coats",
    "service_type": "Sealcoat, 2 coats",
    "category": "Sealcoating",
    "assembly_id": 5120,
    "estimating_mode": "unit_costing",
    "area_id": 22101,
    "quantity": 48250.0,
    "unit": "SF",
    "quantity_source": "inherited",
    "unit_cost": 0.25,
    "unit_price": 0.25,
    "price": 12062.5,
    "cost": 12062.5,
    "allocated_price": 12062.5,
    "is_optional": false,
    "hide_on_proposal": false
  },
  "created_line_items": [ { "id": 140261, "name": "Sealcoat, 2 coats" } ],
  "estimate_totals": { "price": 39882.5, "subtotal": 39882.5, "tax": 0.0, "cost": 33462.5, "margin": 6420.0, "margin_pct": 0.160972, "optional_total": 0.0 }
}

(Response trimmed.) data is the first new line; created_line_items lists every line the add created (subservices included).

Errors: 400 Send catalog_service_id, or name for a custom line, quantity must be 0 or more. 404 Catalog service not found, Area not found. 409 when the app refuses the add (for example the service is already on that parent).

Update a line item#

PATCH /v1/estimates/{id}/line-items/{line_item_id}

Permission: Write jobs (role: owner, admin, estimator or sales)

Only the fields you send change, then the estimate re-prices. A quantity you send is kept as typed: it replaces the takeoff's number and the line's quantity_source becomes manual.

Name Type Description
name string
quantity number
unit string SF ↔ SY converts the quantity and unit price.
unit_cost number Price per unit (unit-priced lines).
pricing_mode string detailed or unit_costing.
pricing_method, pricing_rate string, number or null This line's own markup or margin (fraction); null rate follows the estimate.
price_override number or null Pin this line's customer price; null clears.
minimum_price number or null
is_optional boolean An add-on the customer can choose; left out of totals.price.
hide_on_proposal boolean Priced but not listed on the proposal.
proposal_group string Lines sharing a group print as one proposal line.
proposal_text string The line's proposal wording.
what_to_expect string HTML printed under the line on the proposal. An empty string shows nothing.
internal_scope string HTML scope sent with this line's quote requests to subcontractors (Quote requests). An empty string sends none.
curl -X PATCH "https://trutec.ai/api/v1/estimates/9841/line-items/140261" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"quantity": 51000, "is_optional": false}'

Returns 200 with data (the line) and estimate_totals.

Errors: 400 Send at least one field to change, Unknown fields: …. 404 Line item not found. 409 when the estimate is locked for that change.

Remove a line item#

DELETE /v1/estimates/{id}/line-items/{line_item_id}

Permission: Write jobs (role: owner, admin or estimator)

A service group takes its subservices with it. Returns 204.

Cost lines#

Add a cost line#

POST /v1/estimates/{id}/cost-lines

Permission: Write jobs (role: owner, admin or estimator)

Adds a catalog cost item to one line's recipe, on this estimate only.

Name Type Required Description
line_item_id integer Yes The line.
type string Yes crew, equipment, material, trucking, subcontractor, misc or overhead.
catalog_item_id integer Yes From GET /v1/catalog/{item_type}.
quantity_formula string No Quantity per the line's quantity, for example qty * 0.05. Defaults to the catalog's.
curl -X POST "https://trutec.ai/api/v1/estimates/9841/cost-lines" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"line_item_id": 140260, "type": "material", "catalog_item_id": 742, "quantity_formula": "qty * 0.05"}'

Returns 201 with data (the cost line) and estimate_totals.

Errors: 400 type must be one of …. 404 Line item not found, Catalog item not found.

Update a cost line#

PATCH /v1/estimates/{id}/cost-lines/{cost_line_id}

Permission: Write jobs (role: owner, admin or estimator)

Name Type Description
quantity number or null A typed quantity; null goes back to the formula.
quantity_formula string
unit_cost number This estimate's unit cost. The catalog is never changed.
waste_factor number or null Fraction.
pricing_method, pricing_rate string, number or null Pin a markup on this cost line; a null rate follows the line again.
billing string billed or internal_only.
is_alternate boolean Cost it as a comparison option, outside every total.
hauls array or null Trucking only: what the truck hauls (asphalt, millings, spoil). With automatic driving time on, millings or spoil send it to the service's dump site.
dump_material string or null Misc only: mark the line as the dump fee for millings or spoil; null = an ordinary misc line.
dump_site_vendor_id integer or null Misc dump fees only: the dump site — a vendor with a price on this dump fee. The fee prices at that site, and the service's millings/spoil trucks re-route: a round trip when the dump site is the asphalt plant, otherwise plant → job → dump site → plant. null clears it.

Returns 200 with data and estimate_totals.

Remove a cost line#

DELETE /v1/estimates/{id}/cost-lines/{cost_line_id}

Permission: Write jobs (role: owner, admin or estimator)

Returns 204.

Issue, win, lose, change orders#

Issue a version#

POST /v1/estimates/{id}/issue

Permission: Write jobs (role: owner, admin or estimator)

Re-prices, then freezes the next numbered version (draft → estimate). Nothing is sent to the customer. TruTec's issue checks apply: unfinished recipe inputs, the workspace's profit floor and blocking review checks return 409 with code: issue_blocked and the reason. A key whose user is an owner or estimating admin can send override_approval: true to release the profit floor and review checks, as the app's admin approval does.

Name Type Description
override_approval boolean Admins only.
curl -X POST "https://trutec.ai/api/v1/estimates/9841/issue" \
  -H "X-API-Key: sk_your_api_key_here"

Returns 200 with data (the estimate, now current_version 1 and status estimate) and issued_version.

{
  "error": "This bid's profit is below your company's approval floor — an admin has to approve it.",
  "code": "issue_blocked"
}

Mark won#

POST /v1/estimates/{id}/mark-won

Permission: Write jobs (role: owner, admin or estimator)

The estimate must be issued first (409 otherwise). Freezes the award for job costing, moves the job to the workspace's won stage (when the user may move it; otherwise job_status_notice says what stayed put) and queues the crew work on the schedule. For a change-order estimate it approves the change order. Optional reason. Returns 200 with the estimate, plus job_status_notice, crew_work_orders_created and change_order_id when they apply.

Mark lost#

POST /v1/estimates/{id}/mark-lost

Permission: Write jobs (role: owner, admin or estimator)

Name Type Description
winning_price number What the job went for, if known.
winning_contractor string Who won it.
reason string Why.

Returns 200 with the estimate.

Start a change order#

POST /v1/estimates/{id}/change-orders

Permission: Write jobs (role: owner, admin or estimator)

Creates a change-order estimate linked to this one (parent_estimate_id) and its job change order. Edit it with the same line-item endpoints, prepare and send its proposal, and mark it won to approve it.

Name Type Description
title string Names the change order.
pricing_mode string revision (default): a copy of the whole estimate to re-price. additive: price only the new work, on a blank area.
request_key string (UUID) additive only: repeat the call with the same key and you get the same change order back.

Returns 201 with the change-order estimate and change_order_id.

Errors: 400 pricing_mode must be revision or additive, and the app's reason when a change order can't start yet.

Estimate templates#

GET /v1/estimate-templates lists them (see Catalog); template_id on POST /v1/estimates starts an estimate from one. These calls save and manage them, exactly as Save as template and the Templates page do.

Permission: Write jobs (role: owner, admin or estimator); archiving also needs Delete records.

Call What it does
POST /v1/estimate-templates Save an estimate as a template: estimate_id, name, optional description. Its areas, lines, cost lines and pricing are copied, never the job or customer. Returns 201.
PATCH /v1/estimate-templates/{id} name, description, and/or refresh_from_estimate_id to re-save the template from an estimate. Estimates already made from it don't change.
POST /v1/estimate-templates/{id}/duplicate Copy it (named "… (copy)" unless you send name). Returns 201.
DELETE /v1/estimate-templates/{id} Archive it. Returns 204.
curl -X POST "https://trutec.ai/api/v1/estimate-templates" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"estimate_id": 9841, "name": "Parking lot sealcoat + restripe"}'
{
  "data": {
    "id": 214,
    "name": "Parking lot sealcoat + restripe",
    "description": null,
    "area_count": 1,
    "service_count": 3,
    "created_at": "2026-10-13T18:20:04Z",
    "updated_at": "2026-10-13T18:20:04Z"
  }
}

Errors: 400 name is required. 404 Estimate not found, Estimate template not found. 403 when the key's user's estimating role can't manage templates.