Skip to content
TruTecDevelopers
Get an API key

Job costing

Read and record a job's actual costs, and read its crew labor, invoices and crew and equipment allocation, plus a feed of every site with completed work.

View as Markdown

These endpoints cover what happened on a job after it was won: the costs recorded against it, crew clock time, invoices, and which crews, people and equipment were on each work order. A separate feed lists every site where your workspace has finished work.

Reads need the Read jobs permission. You can also add, change, review and delete cost entries (see Record costs); to create and send invoices, see Invoices & payments, and for crew time, Timesheets. For every job's revenue and direct cost in a month, see Month-end job cost.

Response format#

Most of these endpoints return {"data": [...], "pagination": {"total", "limit", "offset"}}. Labor and allocation return every row for the job with no pagination. Errors return JSON with an error string. A job that is not in your workspace returns 404 Job not found.

Timestamps are UTC in ISO 8601 with a Z suffix, for example 2026-10-14T21:30:00Z. Plain dates are YYYY-MM-DD.

Note

Partner keys that act on behalf of several workspaces must also pass firm_id in the query string.

Costs#

Get job costs#

GET /v1/jobs/{id}/costs

Permission: Read jobs

Returns the job's cost entries (oldest first) and a summary. The summary always covers every entry on the job, even when you filter or page the rows.

Name Type Required Description
id integer (path) Yes Job id.
category string No labor, material, equipment, trucking, subcontractor, overhead or other.
review_status string No pending, approved or rejected.
limit integer No Page size. Default 100, maximum 500.
offset integer No Rows to skip. Default 0.
curl "https://trutec.ai/api/v1/jobs/70412/costs?category=material" \
  -H "X-API-Key: sk_your_api_key_here"
{
  "job_id": 70412,
  "job_external_ref": "crm-deal-88120",
  "data": [
    {
      "id": 88310,
      "job_id": 70412,
      "work_order_id": 4120,
      "estimate_service_id": null,
      "category": "material",
      "cost_code": "01-SEAL",
      "source": "invoice",
      "description": "Sealer concentrate, 1,100 gal",
      "qty": 1100.0,
      "unit": "GAL",
      "unit_cost": 4.85,
      "total_cost": 5335.0,
      "amount": 5335.0,
      "incurred_on": "2026-11-03",
      "is_committed": false,
      "variance_reason": null,
      "review_status": "approved",
      "reviewed_at": "2026-11-04T15:02:11Z",
      "reconciled_at": null,
      "has_attachment": true,
      "created_at": "2026-11-03T22:40:19Z"
    }
  ],
  "pagination": { "total": 1, "limit": 100, "offset": 0 },
  "summary": {
    "total_amount": 31820.44,
    "entry_count": 14,
    "by_category": {
      "labor": { "count": 6, "amount": 9840.0 },
      "material": { "count": 1, "amount": 5335.0 },
      "equipment": { "count": 3, "amount": 6120.0 },
      "trucking": { "count": 2, "amount": 2900.0 },
      "subcontractor": { "count": 1, "amount": 7200.0 },
      "overhead": { "count": 0, "amount": 0.0 },
      "other": { "count": 1, "amount": 425.44 }
    },
    "by_review_status": {
      "pending": { "count": 4, "amount": 6210.44 },
      "approved": { "count": 10, "amount": 25610.0 },
      "rejected": { "count": 0, "amount": 0.0 }
    },
    "original_contract": 48500.0,
    "revised_contract": 50950.0
  }
}
Field Description
amount The entry's cost: total_cost when set, otherwise qty × unit_cost, otherwise 0.
source How the entry was recorded, for example manual (office), field (crew app), invoice (uploaded invoice or ticket) or clock (from crew time entries).
is_committed Whether the entry is marked as committed.
review_status, reviewed_at, reconciled_at Office review and reconciliation state.
has_attachment Whether a receipt or ticket image is attached. The file itself is not available through the API.
original_contract, revised_contract The job's contract value at award, and after approved change orders. Null when TruTec cannot compute it.

Record costs#

Cost writes run the same rules as the Job costing page in TruTec. They act as the person who created the API key, who must be able to see pricing. Job costing must be turned on for the workspace; otherwise writes return 403 with the reason. Budget alerts go out when a new cost pushes a job past its budget, as in TruTec.

Add a cost#

POST /v1/jobs/{id}/costs

Permission: Write jobs

Records an actual cost (an invoice, a ticket, a rental) as source: "manual", pending office review.

Name Type Required Description
description string Yes Up to 255 characters.
category string No labor, material, equipment, trucking, subcontractor, overhead or other (default).
cost_code string No Defaults to the workspace's code for the category.
qty, unit, unit_cost No Quantity, unit (for example TON) and cost per unit.
total_cost number No The total in dollars. When left out, amount is qty × unit_cost.
incurred_on string No YYYY-MM-DD.
work_order_id integer No Tie the cost to one visit of the job.
estimate_service_id integer No Tie it to an estimate line, for budget-vs-actual by service.
variance_reason string No Why it differs from the budget.
is_committed boolean No A committed (ordered, not yet billed) cost.
curl -X POST "https://trutec.ai/api/v1/jobs/70412/costs" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "category": "material",
    "description": "Sealer concentrate, 1,100 gal",
    "qty": 1100,
    "unit": "GAL",
    "unit_cost": 4.85,
    "incurred_on": "2026-11-03",
    "work_order_id": 4120
  }'

Returns 201 with the cost entry, in the same shape as Get job costs. A work_order_id or estimate_service_id from another job returns 400.

Update a cost#

PATCH /v1/jobs/{id}/costs/{cost_id}

Permission: Write jobs

Changes the fields you send: description, category, cost_code, qty, unit_cost, total_cost, work_order_id, estimate_service_id, variance_reason, is_committed, review_status (pending, approved or rejected) and reconciled (boolean). unit and incurred_on can't be changed after the entry is recorded (400).

Review a cost#

POST /v1/jobs/{id}/costs/{cost_id}/review

Permission: Write jobs

Name Type Required Description
status string Yes approved, rejected or pending.
reconciled boolean No Mark it matched to the bill. Rejecting clears it.
resolution object No For a crew-app record that needs a price: {"kind": "price", "unit_cost": 18.5, "reason": "..."}, {"kind": "link", "entry_id": 88310, "reason": "..."} (a duplicate of another cost) or {"kind": "delivery_only", "reason": "..."}.

The reviewer is recorded as the key's creator. A crew-app record with no verified price can't be approved until it is resolved (400, needs_resolution: true).

Delete a cost#

DELETE /v1/jobs/{id}/costs/{cost_id}

Permission: Write jobs and Delete records

Returns 204. Labor costs that come from crew time entries (source: "clock") are rebuilt from the timesheet; change the time entries instead.

Labor#

Get job labor#

GET /v1/jobs/{id}/labor

Permission: Read jobs

Returns every crew clock-in and clock-out on the job's work orders, oldest first, with totals. Voided entries are left out unless you ask for them. Not paginated.

Name Type Required Description
id integer (path) Yes Job id.
include_voided boolean No true to include voided entries.
curl "https://trutec.ai/api/v1/jobs/70412/labor" \
  -H "X-API-Key: sk_your_api_key_here"
{
  "data": [
    {
      "id": 602118,
      "job_id": 70412,
      "work_order_id": 4120,
      "user_id": 344,
      "category": "work",
      "paid": true,
      "clock_in": "2026-11-03T13:00:00Z",
      "clock_out": "2026-11-03T22:30:00Z",
      "hours": 9.0,
      "unpaid_lunch_minutes": 30,
      "wage_rate": 28.5,
      "wage_rate_kind": "standard",
      "is_prevailing_wage": false,
      "approved_at": "2026-11-04T14:10:02Z",
      "voided_at": null
    }
  ],
  "summary": {
    "hours_by_category": { "work": 42.5, "travel": 6.0 },
    "total_hours": 48.5,
    "open_entries": 0,
    "labor_cost_at_wage_rate": 1374.25
  }
}
Field Description
category work, travel, break or shop. Breaks are unpaid (paid: false).
hours Clock time minus the unpaid lunch. Null while the person is still clocked in.
wage_rate Hourly cost rate captured at clock-in.
wage_rate_kind Where the rate came from: standard (the person's default rate), prevailing (the job's prevailing-wage rate), ep_person (the catalog rate for that person), crew_derived (crew day rate ÷ headcount ÷ hours) or actual_pay (the person's actual pay rate).
open_entries Entries with no clock-out yet.
total_hours Sum of hours_by_category, breaks included.
labor_cost_at_wage_rate Paid hours × wage_rate, summed. Straight time only: no overtime or labor burden.

Invoices#

List job invoices#

GET /v1/jobs/{id}/invoices

Permission: Read jobs

Returns the job's invoices, oldest first. Payment links and customer share links are never included.

Name Type Required Description
id integer (path) Yes Job id.
status string No open, paid, void or uncollectible.
limit integer No Page size. Default 100, maximum 500.
offset integer No Rows to skip. Default 0.
curl "https://trutec.ai/api/v1/jobs/70412/invoices" \
  -H "X-API-Key: sk_your_api_key_here"
{
  "job_id": 70412,
  "job_external_ref": "crm-deal-88120",
  "data": [
    {
      "id": 7781,
      "job_id": 70412,
      "doc_number": 1412,
      "status": "paid",
      "billing_type": "deposit",
      "payment_method": "check",
      "description": "Deposit, 30%",
      "currency": "usd",
      "amount_cents": 1455000,
      "amount": 14550.0,
      "subtotal_cents": 1455000,
      "subtotal": 14550.0,
      "discount_cents": 0,
      "discount": 0.0,
      "tax_cents": 0,
      "tax": 0.0,
      "due_date": "2026-10-24T00:00:00Z",
      "issued_at": "2026-10-10T16:20:41Z",
      "paid_at": "2026-10-18T19:02:13Z",
      "updated_at": "2026-10-18T19:02:13Z",
      "quickbooks": {
        "has_invoice": true,
        "has_payment": true,
        "invoice_synced_at": "2026-10-10T16:21:05Z",
        "sync_error": null
      }
    }
  ],
  "pagination": { "total": 1, "limit": 100, "offset": 0 }
}

Amounts come in both cents (amount_cents) and dollars (amount). billing_type is one of full, deposit, milestone, custom, line_items or recurring. payment_method is, for example, stripe (paid online) or check. The quickbooks object shows whether the invoice and its payment reached QuickBooks Online, and the error if a sync failed.

Allocation#

Get job allocation#

GET /v1/jobs/{id}/allocation

Permission: Read jobs

Returns each of the job's work orders, by scheduled date, with the crew, people and equipment on it. Not paginated.

curl "https://trutec.ai/api/v1/jobs/70412/allocation" \
  -H "X-API-Key: sk_your_api_key_here"
{
  "data": [
    {
      "work_order_id": 4120,
      "job_id": 70412,
      "title": "Sealcoat front lot",
      "status": "completed",
      "scheduled_date": "2026-11-03",
      "completed_at": "2026-11-03T23:05:40Z",
      "crew": { "id": 18, "name": "Seal Crew 1" },
      "people": [
        { "user_id": 344, "name": "Crew Lead", "source": "base" },
        { "user_id": null, "name": "Laborer 2", "source": "manual" }
      ],
      "equipment": [
        { "id": 902, "name": "Seal rig 2", "ownership_type": "owned", "group": "Sealcoating" }
      ]
    }
  ]
}

status is unscheduled, scheduled, in_progress, completed or cancelled. Crew members without a TruTec login have a name and a null user_id. A person's source says how they were placed: base (their own crew), utility (the utility crew), manual (picked by the office) or connected (copied from a linked work order).

Completed sites#

List completed sites#

GET /v1/sites/completed

Permission: Read jobs

Returns every site (not archived) where the workspace has completed work, with the customer, coordinates and the last completion date. Use it to mark properties as active customers in your CRM.

A site counts when at least one of its jobs sits in a pipeline stage the workspace treats as finished, the same rule the crew schedule uses.

By default rows are sorted by last_completed_at, newest first. With updated_after, they are sorted by updated_at, oldest first, so you can page forward and save the last row's updated_at as your next cursor.

Name Type Required Description
client_account_id integer No Only sites under this customer.
external_ref string No Only sites under the customer with this external_ref. This is the customer's ref, not the site's.
completed_after string No ISO 8601. Only sites whose last completion is after it.
updated_after string No ISO 8601. Only sites whose record, completed jobs or last completion changed after it.
limit integer No Page size. Default 100, maximum 500.
offset integer No Rows to skip. Default 0.
curl "https://trutec.ai/api/v1/sites/completed?updated_after=2026-10-01T00:00:00Z" \
  -H "X-API-Key: sk_your_api_key_here"
{
  "data": [
    {
      "location_id": 9307,
      "external_ref": "crm-site-5512",
      "name": "Northgate Plaza",
      "address": "1200 Commerce Dr, Austin, TX 78701",
      "latitude": 30.2711,
      "longitude": -97.7437,
      "client_account": {
        "id": 4812,
        "name": "Northgate Property Group",
        "external_ref": "crm-acct-20931"
      },
      "last_completed_at": "2026-11-03T23:05:40Z",
      "completed_at_source": "work_order",
      "completed_job_count": 3,
      "last_completed_job_id": 70412,
      "updated_at": "2026-11-03T23:05:40Z"
    }
  ],
  "pagination": { "total": 1, "limit": 100, "offset": 0 }
}

completed_at_source tells you how precise last_completed_at is:

Value Meaning
work_order When the job's last work order was marked complete. Most precise.
end_date The job's end date (midnight UTC).
updated_at When the job record last changed. Least precise.

Errors: 400 client_account_id must be an integer. 400 completed_after must be an ISO 8601 timestamp / updated_after must be an ISO 8601 timestamp.