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 MarkdownThe 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
idof 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
idis new and needstype,catalog_item_idandquantity_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.
groupnames 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".