Skip to content
TruTecDevelopers
Get an API key

Production work orders

Turn a won estimate into the crew's production sheet — header, one row per service with tons, gallons and square feet, crew plan — edit it, mark it issued or done, and put it in the crew packet.

View as Markdown

A production work order is the one-page sheet the office hands crews after winning a job: the job and site header (customer, address, gate code, contacts, permit, flags such as certified payroll), one scope row per service with the production quantities its trade needs (paving: square feet, thickness, tons, rock tons, mix, tack; sealcoat: square feet, gallons, application, sealer; milling: square feet, depth, tons), and the crew plan (crew, days, hours per day, trucks, an alternate crew).

It is generated from an estimate and kept in step with it: refresh re-derives the rows from the estimate while keeping anything someone typed by hand.

These are not the crew schedule's visits. Those are /v1/work-orders. When scheduling is on, a new sheet's rows go to the schedule's "waiting to schedule" queue by themselves (no crew is notified until the schedule is published), and each row's schedule_work_order_ids points at its visits.

Writes act as the user who created the API key, with that user's estimating role. Nothing on this page emails or texts anyone.

Note

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

The production work order object#

Field Type Description
id integer
number integer or null The sheet number.
job_id, estimate_id integer
status string issued or done.
customer_name, job_name, job_address, sales_rep, project_manager string or null Header.
issued_at, generated_at, created_at, updated_at string or null generated_at is when the rows were last derived from the estimate.
contacts array Up to two: name, email, phone, cell. Single sheet only, like the fields below.
gate_code, summary, location_type, permit_number, directions_url, notes string or null
flags object certified_payroll, osha_certified, badging, drone, nto (true/false).
is_subcontracted, sub_name, sub_phone When the work goes to a subcontractor.
estimate_changed_since_generated boolean The estimate changed after the rows were derived; a refresh would pick it up.
totals object tons, rock_tons, gallons, crew_days, sqft.
lines array The scope rows (below).

A row: id, line_item_id (the estimate line), position, trade (paving, sealcoat, milling, striping, concrete, generic), label, quantities (by name), edited_quantities (names typed by hand, kept on refresh), crew_id, days, hours_per_day, trucks, alt_crew_id, alt_days, alt_hours_per_day, notes, schedule_work_order_ids.

Read#

GET /v1/production-work-orders lists sheets, newest first. Filters: job_id, estimate_id, status, updated_after (then oldest change first), limit, offset. Permission: Read jobs.

GET /v1/production-work-orders/{id} returns one with its rows and totals.

Create#

POST /v1/jobs/{job_id}/production-work-orders

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

Generates a sheet from one of the job's estimates.

Name Type Description
estimate_id integer Default: the job's newest estimate.
line_item_id integer Only this service's work.
crew_id integer Only this crew's work.

Send neither for every service. Like the app, the new rows go to the crew schedule's queue (when scheduling is on) and the sheet's PDF goes to the job's crew packet. Nobody is notified.

curl -X POST "https://trutec.ai/api/v1/jobs/70412/production-work-orders" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"estimate_id": 9840}'
{
  "data": {
    "id": 615, "number": 1042, "job_id": 70412, "estimate_id": 9840, "status": "issued",
    "customer_name": "Northgate Properties", "job_name": "Northgate Plaza", "job_address": "4100 Commerce Dr",
    "sales_rep": null, "project_manager": null,
    "issued_at": null, "generated_at": "2026-10-13T19:02:51Z",
    "created_at": "2026-10-13T19:02:51Z", "updated_at": "2026-10-13T19:02:51Z",
    "contacts": [], "gate_code": null, "summary": null, "location_type": null,
    "flags": {"certified_payroll": false, "osha_certified": false, "badging": false, "drone": false, "nto": false},
    "permit_number": null, "is_subcontracted": false, "sub_name": null, "sub_phone": null,
    "directions_url": null, "notes": null, "estimate_changed_since_generated": false,
    "totals": {"tons": 0.0, "rock_tons": 0.0, "gallons": 1450.0, "crew_days": 2.0, "sqft": 51000.0},
    "lines": [
      {"id": 3301, "line_item_id": 140261, "position": 0, "trade": "sealcoat", "label": "Sealcoat",
       "quantities": {"sqft": 51000, "raw_gallons": 1450, "application": "2 coats"},
       "edited_quantities": [], "crew_id": 12, "days": 2.0, "hours_per_day": 8.0, "trucks": null,
       "alt_crew_id": null, "alt_days": null, "alt_hours_per_day": null, "notes": null,
       "schedule_work_order_ids": [88213]}
    ]
  }
}

Errors: 400 Build the estimate before creating a work order., That service has no work on this estimate. 404 Job not found, Estimate not found, Crew not found.

Change the header#

PATCH /v1/production-work-orders/{id}

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

Only the fields you send change: any header text field above, is_subcontracted, flags (any of the five), contacts (replaces the list). The crew-packet PDF refreshes.

Refresh, issue, done#

POST /v1/production-work-orders/{id}/refresh re-derives the rows from the estimate. Quantities and crew plans typed by hand are kept; rows for services added since are added and go to the schedule queue. The response adds refresh with what changed. 409 when the sheet isn't linked to an estimate.

POST /v1/production-work-orders/{id}/issue marks it issued (stamping issued_at the first time). POST /v1/production-work-orders/{id}/mark-done marks it done.

Rows#

POST /v1/production-work-orders/{id}/lines adds a row (label required). PATCH /v1/production-work-orders/{id}/lines/{line_id} changes one. Both take label, trade, crew_id, days, hours_per_day, trucks, alt_crew_id, alt_days, alt_hours_per_day, notes, quantities (merged by name; names you set are kept through refreshes) and reset_quantities (names to hand back to the estimate on the next refresh). They return data (the row) and the sheet's totals. A workspace with "Scope changes only in the estimate" on refuses new rows (403); change the estimate and refresh instead. Existing rows stay editable.

DELETE /v1/production-work-orders/{id}/lines/{line_id} removes a row (204).

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

Crew packet#

POST /v1/production-work-orders/{id}/attach-to-dispatch renders the sheet as a PDF and saves it as the job's crew-packet document, replacing the earlier copy. Crews see it in the field app. Returns document_id; 503 when PDF rendering is unavailable right now.

Delete#

DELETE /v1/production-work-orders/{id}

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

Deletes the sheet. Visits already on the crew schedule stay. Returns 204.