Job operations & materials
Move a won job through your operational stages, record material orders and supplier availability, tick off supplier confirmations, and move cards on the Operations Pipeline.
View as MarkdownOnce a job is won, the office takes it through the workspace's operational stages (for example Prepare, Execute, Close out), orders its materials, and confirms supply before the crew arrives. These endpoints read and change that record, the same way the job's Operations view does in TruTec.
Writes are made as the person who created the API key, need permission to edit the schedule (403 view_only otherwise) and operations turned on for the workspace (403 Operations not enabled). Material order and supplier records are office notes: nothing here contacts a supplier.
Revisions. The job's records are shared by everyone in the office, so each write carries the revision you read, and a stale one returns 409 instead of overwriting someone else's change. There are three, all in GET /v1/jobs/{id}/operations:
| Revision | Send it with |
|---|---|
revision |
/operations/stage (and optionally /operations/handoff) |
material_orders.revision |
the three material-orders actions |
supply_confirmations.revision |
supply-confirmations confirm and undo |
On 409, read the job again and retry with the new revision.
Get a job's operations status#
GET /v1/jobs/{id}/operations
Permission: Read jobs
{
"data": {
"job_id": 70412,
"job": { "id": 70412, "name": "Northgate Plaza reseal" },
"stage": "prep",
"stage_label": "Prepare",
"revision": 1,
"handed_off_at": "2026-10-20T15:44:02Z",
"stage_entered_at": "2026-10-20T15:44:02Z",
"sales_status": "won",
"handoff_sales_status": "won",
"can_hand_off": false,
"stages": [
{ "key": "prep", "label": "Prepare", "color": "#f59e0b" },
{ "key": "execution", "label": "Execute", "color": "#3b82f6" },
{ "key": "closeout", "label": "Close out", "color": "#10b981" }
],
"material_orders": {
"revision": 2,
"estimate_name": "Northgate reseal",
"total": 1,
"fully_ordered": 1,
"received": 0,
"complete": true,
"items": [
{
"key": "4f0c2a9e1b7d3c6a8e5f1a2b",
"name": "Sealer",
"unit": "gal",
"quantity": 1200.0,
"status": "ordered",
"status_label": "Ordered",
"supplier": "Local supplier",
"po": "PO-1042",
"expected_on": "2026-11-02",
"ordered_quantity": 1200.0,
"received_quantity": 0.0,
"fully_ordered": true,
"estimate_changed": false,
"special_load_out": false,
"scope": "9b1e0c4d2a7f5e3c1b8d6a0e",
"availability_scope": "availability-v1:…",
"availability": { "state": "confirmed", "label": "Supplier availability confirmed", "supplier": "Local supplier", "fulfillment": "delivery", "confirmed_on": "2026-11-02", "confirmed_time": "07:00", "available_quantity": 1200.0 },
"updated_by": "Office Manager",
"updated_at": "2026-10-28T16:02:11Z"
}
]
},
"supply_confirmations": {
"revision": 0,
"total": 1,
"open": 1,
"overdue": 0,
"due_today": 0,
"items": [
{
"key": "c71d5e0a9b3f2e8d4a6c1b0f",
"kind": "material",
"name": "Hot mix asphalt",
"unit": "ton",
"quantity": 180.0,
"lead_days": 2,
"work_date": "2026-11-09",
"due_date": "2026-11-05",
"due_text": "2 business days before the work on Mon, Nov 9",
"status": "upcoming",
"status_label": "Upcoming",
"recheck": null,
"confirmed_by": null,
"confirmed_at": null,
"note": null
}
]
}
}
}
Materials come from the job's awarded estimate (the latest saved one before it is awarded); materials the catalog marks as not purchased, or below their order-above quantity, are left out. estimate_changed means the estimate's quantity changed after the order was recorded, so the item counts as not ordered until you record it again. unit_price appears in availability only for keys whose creator can see prices.
Supply confirmations are the catalog items set to "confirm with the supplier N business days before the work". work_date is the first scheduled day of the work that uses the item, due_date the day the confirmation is due. A confirmation goes back to open (with recheck saying why) when the work moves or the quantity changes.
Stages#
Hand a won job to operations#
POST /v1/jobs/{id}/operations/handoff
Permission: Write jobs
Like Start operations workflow in TruTec: the job enters the first operational stage and that stage's automations run (tasks, checklists and in-app notices the owner set up). The job must be in the sales stage set for handoff (handoff_sales_status), otherwise 409. A job already in operations comes back unchanged. Takes an optional revision. Returns the operations status.
Move a job to another stage#
POST /v1/jobs/{id}/operations/stage with {"stage": "execution", "revision": 1}
Permission: Write jobs
stage is a key from stages. That stage's automations run once. Returns the operations status.
Material orders#
The three actions take the material's key in the path and material_orders.revision in the body. You may also send the item's scope as you read it; if the estimate changed since, the response is 409.
Record an order#
POST /v1/jobs/{id}/material-orders/{key}/order-status
Permission: Write jobs
| Field | Type | Required | Description |
|---|---|---|---|
status |
string | Yes | unconfirmed, partially_ordered, ordered, partially_received or received. |
supplier |
string | Unless unconfirmed |
Up to 200 characters. |
po |
string | No | Purchase order or confirmation number. |
expected_on |
string | No | Expected delivery date. |
ordered_quantity |
number | For partial orders | Total ordered so far. Defaults to the full quantity. |
received_quantity |
number | For partial deliveries | Total received so far. |
revision |
integer | Yes | material_orders.revision. |
The quantities must agree with the status (for example ordered means the full quantity is ordered and none received); otherwise 400 says what's wrong.
Record a supplier availability check#
POST /v1/jobs/{id}/material-orders/{key}/availability
Permission: Write jobs
| Field | Type | Required | Description |
|---|---|---|---|
availability_state |
string | Yes | not_requested, pending, confirmed or unavailable. |
supplier |
string | Unless not_requested |
|
contact, notes |
string | No | Who you spoke to, and anything worth keeping. |
fulfillment |
string | No | delivery (default) or pickup. |
requested_on, requested_time |
string | No | When you need it. |
confirmed_on, confirmed_time, available_quantity |
When confirmed |
What the supplier confirmed. | |
unit_price |
number | No | Dollars. Needs a key whose creator can see prices (403 otherwise). |
revision |
integer | Yes | material_orders.revision. |
Order quantities and status don't change.
Record what it actually cost#
POST /v1/jobs/{id}/material-orders/{key}/actual-cost
Permission: Write jobs
| Field | Type | Required | Description |
|---|---|---|---|
actual_cost |
number | Yes | Dollars, up to 2 decimals. |
incurred_on |
string | Yes | Date on the invoice or ticket. |
cost_reference |
string | Yes | The supplier invoice or delivery-ticket number. |
revision |
integer | Yes | material_orders.revision. |
Adds a material cost to the job's costing, pending review, and returns its cost_entry_id with the operations status. The same material and reference twice is 409. Needs estimating and a key whose creator can see prices.
Supply confirmations#
POST /v1/jobs/{id}/supply-confirmations/{key}/confirm with {"revision": 0, "note": "Plant confirmed 180 t for 7am"}
POST /v1/jobs/{id}/supply-confirmations/{key}/undo with {"revision": 1}
Permission: Write jobs
Confirm records that you confirmed the item with the supplier for its current work date and quantity (a note up to 300 characters is optional). Undo puts it back to open. Both take supply_confirmations.revision; if the work date or quantity changed since you read it, the response is 409.
Operations Pipeline#
The Operations Pipeline board shows every visit (and won work waiting to be scheduled) as a card in a column: Ready to schedule, Scheduled, Crew Dispatched, In progress, Completed and Invoiced, plus any columns the owner added between them.
List the columns#
GET /v1/operations-pipeline/columns
Permission: Read jobs
{
"data": [
{ "key": "ready", "label": "Ready to schedule", "color": "#94a3b8", "custom": false, "can_drop_here": true, "moves_here_when": "A job is won on the Sales Pipeline." },
{ "key": "c_1a2b3c4d", "label": "Waiting on permit", "color": "#64748b", "custom": true, "can_drop_here": true, "moves_here_when": "You drag a card here." },
{ "key": "scheduled", "label": "Scheduled", "color": "#60a5fa", "custom": false, "can_drop_here": true, "moves_here_when": "A service is put on the Schedule." }
]
}
Move a card#
POST /v1/operations-pipeline/move
Permission: Write jobs
Like dragging a card on the board.
| Field | Type | Required | Description |
|---|---|---|---|
work_order_id |
integer | One of these | A visit. |
queue_id |
string | One of these | Won work not yet scheduled, from GET /v1/schedule/queue ("es-5512", "job-70412"). |
to |
string | Yes | A column key. |
scheduled_date |
string | To scheduled |
The day. |
crew_id |
integer or null | No | With scheduled: the crew. |
What each move does:
- One of the owner's columns: the card is parked there and that column's automations run once for the job. Nothing on the schedule changes.
ready: unschedules the visit (or moves a parked card back).scheduled: puts it onscheduled_date(draft until published). Queued work becomes a visit.done: completes the visit.sentis refused (400): moving a card to Crew Dispatched sends it to the crew, so use send-dispatch instead.in_progressandinvoicedmove by themselves (the crew clocks in; an invoice is created).
Moves the board doesn't allow return 400 saying why. Returns the card's column, parked_from (for an owner's column, the column its events put it in) and the work_order, if there is one.