Skip to content
TruTecDevelopers
Get an API key

Catalog

Read and set up the estimating catalog — services with their recipes and qualifiers, the crews, equipment, materials, trucking, subcontractors and overhead they cost out with, vendors and their prices, groups, and bulk import.

View as Markdown

The catalog is what TruTec estimates are built from. It has two layers:

  • Catalog services (/v1/catalog/services): the services you sell, such as "Sealcoat, 2 coats" or "2\" mill and overlay". Each has a unit (SF, SY, LF, LM for linear miles, EA, TON, DAY…), a way of pricing, proposal wording, optional qualifiers (for example Depth: 2" / 3") and a recipe: the crews, equipment and materials one unit of the service takes. Adding a catalog service to an estimate copies its recipe onto the estimate line (Estimates & proposals).
  • Cost items (/v1/catalog/{item_type}): the priced building blocks recipes use, one list per type.
item_type What it holds unit is unit_cost is
materials Sealer, asphalt, paint, aggregate The unit it's bought in (TON, GAL, …) Base price per unit
crews Crews and the people on them hour or day The crew's rate when it's priced as one total rate
equipment Owned or rented machines hour, day or fixed Base rate
trucking Haul trucks hour, day, ton or load Base rate
subcontractors Subcontracted services How the sub charges Base price
misc-items Anything else (permits, cones, dump fees) Its unit Cost per unit
overhead-items Overhead and indirect costs How it's charged (per_day, fixed, percent_direct, …) The amount

Some workspaces change their catalog only in TruTec (those on a separate estimating edition). Catalog writes there return 409 with that message.

Estimates keep their own copy. An estimate line copies the catalog values when it's added. Changing the catalog never changes an estimate that's already built; new lines (and an estimator's explicit "update prices") pick up the new values.

Archive, never delete. DELETE archives a catalog row: it drops out of lists and pickers, and estimates that already use it keep pricing exactly as before. include_archived=true shows archived rows.

Who can change the catalog. Writes act as the user who created the API key (see Authentication), with that user's estimating role. Only workspace owners and estimating admins can change the catalog; anyone else gets 403 with the reason.

Rates are fractions: 0.25 means 25%. Money is in dollars. Timestamps are UTC ISO 8601 with a Z.

Note

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

The catalog service object#

Field Type Description
id integer TruTec id. Use it as catalog_service_id when adding a line to an estimate.
name, description string Name, and the internal description crews see.
group object or null {id, name} of its catalog group.
unit string The service's unit, for example SF.
pricing_mode string detailed (priced from its recipe) or unit_costing (one price per unit).
unit_cost number Price per unit for unit_costing services.
minimum_price number or null The least this service is ever sold for on an estimate.
default_pricing_method, default_pricing_rate string, number markup or margin, and the rate new lines start with.
cost_code string or null Your job-cost code.
color string or null Hex color used on maps and takeoffs.
on_every_estimate boolean Lands on every new estimate with a blank quantity.
subservice_ids array Catalog services that come along when this one is added.
qualifiers array Questions the service asks, {name, options, required}.
scopes array The answer combinations it prices, {id, name, answers}. Only on GET /v1/catalog/services/{id}.
proposal_text, pricing_text, additional_info string or null Customer-facing wording. Only on the single-service read.
recipe array {id, type, catalog_item_id, quantity_formula, notes, position, vendor_id, scope_id}: what one unit takes. scope_id is the type (see scopes) a line belongs to, or null for lines every type uses. Only on the single-service read. Set it with Set a service's recipe.
approval_state string approved, or draft for rows waiting on an admin's review (drafts still price).
is_active, created_at, updated_at

The cost item object#

Field Type Description
id integer TruTec id. Use it as catalog_item_id when adding a cost line to an estimate.
type string The item_type it belongs to.
name, description, cost_code string
group object or null {id, name}.
unit, unit_cost string, number See the table above.
default_pricing_method, default_pricing_rate string, number Markup or margin new estimate lines start with.
details object The rest of the fields the catalog form edits for this type (for example waste_factor and taxable on materials, ownership on equipment, default_load_tons on trucking).
members array Crews only: {id, role, count, rate, rate_basis}.
vendor_prices array Materials only: {id, vendor_id, vendor_name, price, unit, effective_date}. Add one with Add a vendor price.
approval_state, is_active, created_at, updated_at

Catalog services#

List catalog services#

GET /v1/catalog/services

Permission: Read jobs

Sorted by name. With updated_after, sorted by updated_at, oldest first, so you can page forward.

Name Type Required Description
q string No Match part of the name (case-insensitive).
group_id integer No Only this group.
updated_after string No ISO 8601 timestamp. Only rows changed after it.
include_archived boolean No true to include archived services.
limit integer No Page size. Default 50, maximum 200.
offset integer No Rows to skip. Default 0.
curl "https://trutec.ai/api/v1/catalog/services?q=seal" \
  -H "X-API-Key: sk_your_api_key_here"
{
  "data": [
    {
      "id": 5120,
      "name": "Sealcoat, 2 coats",
      "description": null,
      "group": { "id": 88, "name": "Sealcoating" },
      "unit": "SF",
      "pricing_mode": "unit_costing",
      "unit_cost": 0.25,
      "minimum_price": 750.0,
      "default_pricing_method": "markup",
      "default_pricing_rate": 0.0,
      "cost_code": "02-SEAL",
      "color": "#1f6feb",
      "on_every_estimate": false,
      "subservice_ids": [],
      "qualifiers": [],
      "approval_state": "approved",
      "is_active": true,
      "created_at": "2026-08-03T15:20:11Z",
      "updated_at": "2026-10-01T18:44:02Z"
    }
  ],
  "pagination": { "total": 1, "limit": 50, "offset": 0 }
}

Errors: 400 updated_after must be an ISO 8601 timestamp. 403 API key lacks job read permission.

Get a catalog service#

GET /v1/catalog/services/{service_id}

Permission: Read jobs

Adds scopes, proposal_text, pricing_text, additional_info and recipe to the object above.

curl "https://trutec.ai/api/v1/catalog/services/5121" \
  -H "X-API-Key: sk_your_api_key_here"
{
  "data": {
    "id": 5121,
    "name": "Mill and overlay",
    "group": { "id": 89, "name": "Paving" },
    "unit": "SY",
    "pricing_mode": "detailed",
    "unit_cost": 0.0,
    "qualifiers": [{ "name": "Depth", "options": ["2\"", "3\""], "required": true }],
    "scopes": [
      { "id": "sc_1a2b", "name": "2\"", "answers": { "Depth": "2\"" } },
      { "id": "sc_3c4d", "name": "3\"", "answers": { "Depth": "3\"" } }
    ],
    "proposal_text": "Mill the existing surface and pave a new hot-mix overlay.",
    "pricing_text": null,
    "additional_info": null,
    "recipe": [
      { "id": 9001, "type": "crew", "catalog_item_id": 311, "quantity_formula": "qty", "notes": null, "position": 0 },
      { "id": 9002, "type": "material", "catalog_item_id": 742, "quantity_formula": "qty * 0.11", "notes": "HMA at 2 in", "position": 1 }
    ],
    "approval_state": "approved",
    "is_active": true,
    "created_at": "2026-08-03T15:20:11Z",
    "updated_at": "2026-10-01T18:44:02Z"
  }
}

(Response trimmed.)

Errors: 404 Catalog service not found.

Create a catalog service#

POST /v1/catalog/services

Permission: Write jobs, and the key's user must be a workspace owner or estimating admin

Name Type Required Description
name string Yes Up to 200 characters.
unit string No Default SF.
pricing_mode string No detailed or unit_costing.
unit_cost number No Price per unit for unit_costing.
minimum_price number or null No Selling-price floor.
group_id integer or null No A service group from GET /v1/catalog/groups?type=services.
description, cost_code, color string No
default_pricing_method string No markup or margin.
default_pricing_rate number No Fraction, for example 0.3.
proposal_text, pricing_text, additional_info string No Customer-facing wording.
on_every_estimate boolean No
subservice_ids array of integers No Other active catalog services to bring along.

Set the recipe with Set a service's recipe and the questions it asks with Set a service's qualifiers.

curl -X POST "https://trutec.ai/api/v1/catalog/services" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name": "Crack fill", "unit": "LF", "pricing_mode": "unit_costing", "unit_cost": 1.10, "group_id": 88}'

Returns 201 with the new service (the single-service shape). A warnings list is added when the setup saves but would price oddly.

Errors: 400 name is required. 400 Unknown fields: …. 403 when the key's user can't manage the catalog. 404 Group not found for this kind of catalog row. 409 when the workspace's catalog can't be changed through the API.

Update a catalog service#

PATCH /v1/catalog/services/{service_id}

Permission: Write jobs, as above

Same fields as create, all optional. Only the fields you send change.

curl -X PATCH "https://trutec.ai/api/v1/catalog/services/5120" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"unit_cost": 0.27}'

Returns 200 with the service.

Archive a catalog service#

DELETE /v1/catalog/services/{service_id}

Permission: Write jobs and Delete records, as above

Returns 204.

Cost items#

List cost items#

GET /v1/catalog/{item_type}

Permission: Read jobs

item_type is one of materials, crews, equipment, trucking, subcontractors, misc-items, overhead-items. Takes the same filters as List catalog services.

curl "https://trutec.ai/api/v1/catalog/materials?q=tack" \
  -H "X-API-Key: sk_your_api_key_here"
{
  "data": [
    {
      "id": 742,
      "type": "materials",
      "name": "Tack coat",
      "description": null,
      "group": { "id": 91, "name": "Liquids" },
      "cost_code": "03-MAT",
      "unit": "GAL",
      "unit_cost": 3.0,
      "default_pricing_method": "markup",
      "default_pricing_rate": 0.15,
      "details": { "taxable": true, "waste_factor": 0.05, "is_procured": true, "external_sku": "TK-100" },
      "vendor_prices": [
        { "id": 3310, "vendor_id": 41, "vendor_name": "Northside Asphalt Supply", "price": 2.85, "unit": "GAL", "effective_date": "2026-09-01" }
      ],
      "approval_state": "approved",
      "is_active": true,
      "created_at": "2026-08-03T15:20:11Z",
      "updated_at": "2026-09-30T13:02:40Z"
    }
  ],
  "pagination": { "total": 1, "limit": 50, "offset": 0 }
}

(details trimmed.)

Errors: 404 Unknown catalog type. Use one of: ….

Get a cost item#

GET /v1/catalog/{item_type}/{item_id}

Permission: Read jobs

Errors: 404 Catalog item not found.

Create a cost item#

POST /v1/catalog/{item_type}

Permission: Write jobs, and the key's user must be a workspace owner or estimating admin

Name Type Required Description
name string Yes
unit string No See the table at the top.
unit_cost number No See the table at the top.
description, cost_code string No
group_id integer or null No A group of this type (GET /v1/catalog/groups?type=materials).
default_pricing_method, default_pricing_rate string, number No
details object No Any of the type's details fields. Unknown names are a 400 that lists the allowed ones.
members array No Crews only. Replaces the crew's people: [{role, rate, rate_basis, count}].
curl -X POST "https://trutec.ai/api/v1/catalog/crews" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name": "Seal crew", "unit": "hour", "details": {"pricing_mode": "sum_of_individuals"},
       "members": [{"role": "Foreman", "rate": 38, "rate_basis": "hour"},
                   {"role": "Laborer", "rate": 24, "count": 2}]}'
{
  "data": {
    "id": 318,
    "type": "crews",
    "name": "Seal crew",
    "unit": "hour",
    "unit_cost": 0.0,
    "details": { "pricing_mode": "sum_of_individuals", "working_hours_per_day": null, "paid_full_day": false },
    "members": [
      { "id": 1201, "role": "Foreman", "count": 1, "rate": 38.0, "rate_basis": "hour" },
      { "id": 1202, "role": "Laborer", "count": 2, "rate": 24.0, "rate_basis": "hour" }
    ],
    "approval_state": "approved",
    "is_active": true,
    "created_at": "2026-10-12T15:27:17Z",
    "updated_at": "2026-10-12T15:27:17Z"
  }
}

(Response trimmed.)

Errors: 400 name is required, Unknown details: …, details.waste_factor must be a number, Each member needs a role. 403, 404, 409 as for services.

Update a cost item#

PATCH /v1/catalog/{item_type}/{item_id}

Permission: Write jobs, as above

Same fields as create; only what you send changes. Sending members replaces a crew's whole roster.

curl -X PATCH "https://trutec.ai/api/v1/catalog/materials/742" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"unit_cost": 3.15, "details": {"waste_factor": 0.07}}'

Archive a cost item#

DELETE /v1/catalog/{item_type}/{item_id}

Permission: Write jobs and Delete records, as above

Returns 204.

Groups, price lists and estimate templates#

List catalog groups#

GET /v1/catalog/groups

Permission: Read jobs

Every group and subgroup (parent_id). type filters to one catalog: services, materials, crews, equipment, trucking, subcontractors, misc-items or overhead-items.

{
  "data": [
    { "id": 88, "type": "services", "name": "Sealcoating", "parent_id": null, "position": 0 },
    { "id": 89, "type": "services", "name": "Paving", "parent_id": null, "position": 1 }
  ],
  "pagination": { "total": 2, "limit": 2, "offset": 0 }
}

List price lists#

GET /v1/catalog/price-lists

Permission: Read jobs

Price columns, for example one per county or customer tier. Each estimate prices from one; set it with price_list_id on PATCH /v1/estimates/{id}.

{
  "data": [
    { "id": 12, "name": "Non-county", "description": null, "group_name": null, "is_default": true },
    { "id": 13, "name": "Travis County", "description": "County bid rates", "group_name": "Counties", "is_default": false }
  ],
  "pagination": { "total": 2, "limit": 2, "offset": 0 }
}

List estimate templates#

GET /v1/estimate-templates

Permission: Read jobs

Saved starting points (areas, services and pricing) for new estimates. Pass template_id to POST /v1/estimates.

{
  "data": [
    { "id": 31, "name": "HOA reseal + restripe", "description": "Two coats, full restripe", "area_count": 1, "service_count": 4, "created_at": "2026-07-14T16:01:09Z", "updated_at": "2026-09-02T12:30:44Z" }
  ],
  "pagination": { "total": 1, "limit": 50, "offset": 0 }
}

Create a catalog group#

POST /v1/catalog/groups

Permission: Write jobs, and the key's user must be a workspace owner or estimating admin

Name Type Required Description
name string Yes Up to 160 characters.
type string No Which catalog: services (default; a service group or division), materials, crews, equipment, trucking, subcontractors, misc-items or overhead-items.
parent_id integer No A group of the same type, to make a subgroup.
curl -X POST "https://trutec.ai/api/v1/catalog/groups" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name": "Striping"}'
{ "data": { "id": 90, "type": "services", "name": "Striping", "parent_id": null, "position": 0 } }

Returns 201. Errors: 400 name is required, type must be one of …. 404 Parent group not found in this catalog.

Rename a service group#

PATCH /v1/catalog/groups/{group_id}

Permission: Write jobs, as above

Body: {"name": "Line striping"}. Only service groups can be renamed; settings that name the group follow the rename. Returns the group and changed ({"name": {"before", "after"}}).

Errors: 400 when the name is taken or the group isn't a service group. 404 Group not found.

Remove a service group#

DELETE /v1/catalog/groups/{group_id}

Permission: Write jobs and Delete records, as above

Removes an empty service group. Returns 204. A group that still holds services (active or archived), subgroups or anything else returns 409 with the reason; move what's in it first.

Recipes and qualifiers#

Set a service's recipe#

PUT /v1/catalog/services/{service_id}/recipe

Permission: Write jobs, and the key's user must be a workspace owner or estimating admin

The recipe is what one unit of the service takes. lines is the whole recipe:

  • A line with the id of a current line keeps that line and everything else set on it in TruTec (material links, takeoff ties, rounding); the fields you send change.
  • A line without an id is new and needs type, catalog_item_id and quantity_formula.
  • Current lines you leave out are removed.
Line field Type Description
id integer A current line's id, from recipe on Get a catalog service.
type string material, crew, equipment, trucking, subcontractor, misc or overhead.
catalog_item_id integer A cost item of that type in this workspace.
quantity_formula string How much per unit of the service, using qty (the service quantity): "qty * 0.0025" gallons of sealer per SF, "qty / 4000" crew hours. Up to 400 characters.
notes string Up to 300 characters.
vendor_id integer or null A vendor to price this line from.
scope_id string or null One of the service's types (scopes[].id) when only that type uses the line.
curl -X PUT "https://trutec.ai/api/v1/catalog/services/5121/recipe" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"lines": [
        {"id": 9001, "quantity_formula": "qty / 3500"},
        {"type": "material", "catalog_item_id": 742, "quantity_formula": "qty * 0.05", "notes": "Tack before paving"}
      ]}'

Returns 200 with the service (single-service shape) and changed.recipe (before and after lists). A warnings list is added when the recipe saves but would price oddly.

Errors: 400 for a bad line (unknown type, missing quantity_formula on a new line, a scope_id the service doesn't have). 404 Catalog item not found in …, Vendor not found, or a line id that isn't in this recipe. 400 with TruTec's reason for a recipe it can't save (for example a quantity linked to a material you removed).

Set a service's qualifiers#

PUT /v1/catalog/services/{service_id}/qualifiers

Permission: Write jobs, as above

Qualifiers are the questions a service asks when it's added to an estimate, for example Coats: 1 coat / 2 coats. Each answer combination can be a type of the service (scopes) with its own price and recipe lines.

Name Type Required Description
qualifiers array Yes The whole list of questions: {name, options, required, default_option, show_when}. Up to 6 questions, each with 1 to 8 answers of up to 40 characters.
add_missing_types boolean No Default true: every answer combination that isn't a type yet is added (like the service editor's Generate). Send false for a question that doesn't change the price by type.

To ask a question only for some answers of an earlier question, add show_when: {"question": "Curb type", "options": ["Vertical", "Barrier"]}. The earlier question must come before it in the list, and the options must be that question's own answers. A skipped question is left out of the type name ("Rollover · 12\"" instead of "Rollover · 6\" · 12\""), so only the real combinations become types.

Types that already exist keep their id, prices and recipe lines, and none are removed. A type whose name matches no answer combination after the change is listed in changed.types_not_matching_any_answers; rename or remove it in TruTec's service editor. A service can have up to 200 types.

curl -X PUT "https://trutec.ai/api/v1/catalog/services/5120/qualifiers" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"qualifiers": [{"name": "Coats", "options": ["1 coat", "2 coats"], "required": true}]}'

Returns 200 with the service and changed: {qualifiers: {before, after}, types_added, types_not_matching_any_answers}.

Errors: 400 for too many questions, answers or combinations, or a repeated answer. 409 when the service prices by individual answers and the change would rename, reorder or drop a question or answer; add questions or answers here, and make those changes in TruTec's service editor.

Vendors and vendor prices#

The vendor object#

Field Type Description
id integer TruTec id.
name string
kind string vendor, rental_house or subcontractor.
supplies array Any of material, equipment, trucking.
email, phone, address string or null The company's details; address is where crews pick up.
contacts array {name, title, email, phone, notes, is_primary}.
tax_rate number or null Their sales tax as a fraction. For reference only; no price uses it.
notes string or null
parent_vendor_id integer or null The company a location belongs to.
is_active, created_at, updated_at

List vendors#

GET /v1/catalog/vendors

Permission: Read jobs

Sorted by name. Takes q, include_archived, limit and offset. GET /v1/catalog/vendors/{vendor_id} returns one.

Create a vendor#

POST /v1/catalog/vendors

Permission: Write jobs, and the key's user must be a workspace owner or estimating admin

Takes name (required) and any of kind, email, phone, address, notes, tax_rate, supplies, contacts, parent_vendor_id. Returns 201 with the vendor.

curl -X POST "https://trutec.ai/api/v1/catalog/vendors" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name": "Northside Asphalt Supply", "supplies": ["material"], "phone": "512-555-0100"}'

Update a vendor#

PATCH /v1/catalog/vendors/{vendor_id}

Same fields; only what you send changes. Returns the vendor and changed.

Archive a vendor#

DELETE /v1/catalog/vendors/{vendor_id}

Permission: Write jobs and Delete records

The vendor leaves every list and picker; its quoted prices and request-for-quote history stay on record. Returns 204, or 409 while it still has active locations.

Add a vendor price#

POST /v1/catalog/materials/{material_id}/vendor-prices

Permission: Write jobs

Name Type Required Description
vendor_id integer Yes A vendor in this workspace.
price number Yes Dollars per unit, more than 0.
unit string No Defaults to the material's unit.
effective_date string No YYYY-MM-DD.
replace_current boolean No Default true: this replaces the vendor's current price for the material. false keeps the older quote active beside it.
delivery_terms, vendor_item_name, spec string No

The material's own unit_cost doesn't change. Returns 201 with the new price, the material, and changed.replaced (the prices it replaced).

curl -X POST "https://trutec.ai/api/v1/catalog/materials/742/vendor-prices" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"vendor_id": 41, "price": 2.95, "effective_date": "2026-10-01"}'

Stop using a vendor's prices#

DELETE /v1/catalog/materials/{material_id}/vendor-prices/{vendor_id}

Permission: Write jobs and Delete records

Archives that vendor's prices for the material. Returns 204.

Bulk import and approval#

Import catalog rows#

POST /v1/catalog/import

Permission: Write jobs, and the key's user must be a workspace owner or estimating admin

Creates or updates many rows at once with the same rules as TruTec's catalog import:

  • Rows match existing ones by exact name and update them; an archived row with that name comes back.
  • group names a group (or a list for a subgroup path); missing groups are created.
  • A service's components ([{type, name}], cost items by name) replace its recipe.
  • New rows land as drafts. Drafts still price; estimates flag them until an owner approves them (in TruTec or with Approve a draft).
  • The whole batch saves or nothing does.
Name Type Required Description
rows array Yes Up to 2,000 rows.
dry_run boolean No true previews what would change and saves nothing.

Each row:

Field Description
type services, materials, crews, equipment, trucking, subcontractors, misc-items, overhead-items or vendors.
name Required.
unit, unit_cost As on the cost item object (services take unit only).
description, cost_code, group Not on vendors.
components Services only.
Crews: pricing_mode, working_hours_per_day. Equipment: ownership. Trucking: default_load_tons. Materials: density_lb_per_cuft. Vendors: kind, supplies, email, phone, address, contacts, notes.
curl -X POST "https://trutec.ai/api/v1/catalog/import" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"dry_run": true, "rows": [
        {"type": "materials", "name": "Sealer", "unit": "GAL", "unit_cost": 2.4, "group": "Liquids"},
        {"type": "services", "name": "Sealcoat 2 coats", "unit": "SF",
         "components": [{"type": "materials", "name": "Sealer"}]}
      ]}'
{
  "data": {
    "dry_run": true,
    "applied": false,
    "summary": { "rows": { "materials": 1, "services": 1 }, "changes": { "materials": 1, "services": 1 } },
    "actions": [
      { "action": "create material", "name": "Sealer", "type": "materials" },
      { "action": "create service", "name": "Sealcoat 2 coats", "type": "services" }
    ],
    "warnings": [],
    "errors": []
  }
}

Returns 200, or 422 with errors when a row can't be saved (nothing is saved then). 400 for a row with an unknown type or field.

Approve a draft#

POST /v1/catalog/{item_type}/{item_id}/approve

Permission: Write jobs, and the key's user must be a workspace owner or estimating admin

item_type is services or a cost item type. Returns the row with approval_state: "approved".