Jobs
Create, find and update jobs, keep them in sync with your own system, and read their photos, proposal maps and change orders.
View as MarkdownA job is one piece of work for a customer at a site: a lead in the pipeline, an active bid, or won work being scheduled and built. Estimates, takeoffs, photos, change orders, costs and invoices all hang off a job.
Jobs link to a customer (client_account_id) and a site (location_id). See Customers & sites.
Syncing with your own system#
TruTec ids are integers that never change. To keep TruTec and your system in step, store your own id on each TruTec record and pull changes on a schedule.
Store your id in external_ref. Customers, sites and jobs each have an external_ref field. It is unique per workspace. A second record cannot take a ref another record already holds; you get a 409 naming the holder.
Write with upserts. These calls are safe to repeat:
| Record | Create-or-match call | Matches on |
|---|---|---|
| Customer | POST /v1/client-accounts |
external_ref, then name |
| Site | POST /v1/locations |
external_ref, then address |
| Job | POST /v1/jobs with external_ref |
external_ref |
A match returns 200 with "created": false. A new record returns 201 with "created": true.
Look up by your id. GET /v1/client-accounts, GET /v1/locations and GET /v1/jobs all accept ?external_ref=. A job can also link to its customer by your id: send client_account_external_ref instead of client_account_id.
Recommended order: customer, then site (with client_account_id), then job (with client_account_id or client_account_external_ref, and the site's address).
Pull changes with updated_after. These list endpoints accept an ISO 8601 updated_after timestamp:
| Endpoint | Returns |
|---|---|
GET /v1/jobs |
Jobs whose record changed after the timestamp |
GET /v1/estimates |
Estimates that changed after the timestamp |
GET /v1/sites/completed |
Sites with new or changed completed work |
With updated_after, results are sorted by updated_at, oldest first. To poll:
- Call with
updated_afterset to your saved cursor (start with a date far in the past). - Process the page. Save the
updated_atof the last row as your new cursor. - Repeat until a page comes back with fewer rows than
limit.
Upsert what you receive by id. You may see a row more than once, so make your writes idempotent.
curl "https://trutec.ai/api/v1/jobs?updated_after=2026-10-11T00:00:00Z&limit=100" \
-H "X-API-Key: sk_your_api_key_here"
Note
Customers, sites, users, change orders and job-costing lists do not take updated_after; photos do through GET /v1/photos?updated_after=... (Photos & files). For those, use the bulk export (GET /v1/exports/{dataset}.jsonl?updated_after=...), which covers customers, sites, jobs, estimates, change orders, invoices, costs, work orders and time entries, or subscribe to webhooks.
A job's updated_at moves when the job record itself changes. A new estimate, photo or cost entry on the job does not move it; track those through their own endpoints or webhooks.
The API has no endpoint for deleting jobs. Archived customers and sites drop out of list results, so run a full pull now and then to reconcile.
Response format#
List endpoints return:
{
"data": [ ... ],
"pagination": { "total": 1342, "limit": 50, "offset": 0 }
}
GET /v1/change-orders is the exception; see below. Errors return JSON with an error string.
Job and user timestamps are UTC in ISO 8601 without a timezone suffix, for example 2026-10-12T14:03:22.418211. Dates (start_date and similar) are YYYY-MM-DD.
Note
Partner keys that act on behalf of several workspaces must also name the target workspace with firm_id (query string, or JSON body on writes). Workspace keys never need it.
The job object#
| Field | Type | Description |
|---|---|---|
id |
integer | TruTec id. Never changes. |
name |
string | Job name. |
company |
string or null | Customer name as shown on the job. |
contact_name, contact_email, contact_phone |
string or null | Customer contact for this job. |
bill_to_name, bill_to_email |
string or null | "Bill to (if different)": the billing entity when it is not the job's customer. Invoices for the job go to this entity in QuickBooks Online. |
address |
string or null | Job site address. |
description, notes |
string or null | Free text. |
job_type |
string or null | Job types as one comma-separated string. |
job_types |
array of strings | The same values as a list. |
status |
string | Pipeline stage key: lead, active, won or lost, or one of the workspace's own stages (GET /v1/job-stages). |
status_label |
string | Display label for the status. |
source |
string or null | Where the job came from. Jobs created through the API default to api. |
priority |
string | low, normal, high or urgent. |
estimated_value |
number or null | Expected value in dollars. |
probability |
integer or null | Win probability, 0–100. |
start_date, end_date, next_follow_up |
string or null | Dates as YYYY-MM-DD. |
measurement_count |
integer | Number of takeoffs on the job. |
firm_id |
integer | Workspace id. |
team_id |
integer or null | Team within the workspace, if any. |
client_account_id |
integer or null | The job's customer. |
client_account_external_ref |
string or null | That customer's external_ref. |
location_id |
integer or null | The job's site. Set by TruTec from the address. Read-only. |
location_external_ref |
string or null | That site's external_ref. |
external_ref |
string or null | Your id for the job. Unique per workspace. |
custom_fields |
object | The workspace's custom job fields. Set them with PATCH /v1/jobs/{id}/custom-fields (see Custom fields). |
quickbooks_sync_error |
string or null | Why the job's latest invoice did not reach QuickBooks Online, if it failed. Null otherwise. |
assigned_to_user_id |
integer | The user who owns the job. |
assigned_to |
object | That user (see List users). |
assignees |
array | Other team members on the job. Only present with include_assignees=true. |
created_at, updated_at |
string | UTC timestamps. |
Jobs#
List jobs#
GET /v1/jobs
Permission: Read jobs
Returns the workspace's jobs, newest first. With updated_after, results are sorted by updated_at, oldest first.
| Name | Type | Required | Description |
|---|---|---|---|
updated_after |
string | No | ISO 8601 timestamp. Only jobs changed after it. Without a timezone it is read as UTC. |
external_ref |
string | No | Only the job with this external_ref (0 or 1 rows). |
status |
string | No | Only jobs in this stage, for example won. |
assigned_to_user_id |
integer | No | Only jobs owned by this user. |
q |
string | No | Search name, company, address and contact name (case-insensitive). |
include_assignees |
boolean | No | true to add the assignees list to each job. |
limit |
integer | No | Page size. Default 50, maximum 100. |
offset |
integer | No | Rows to skip. Default 0. |
curl "https://trutec.ai/api/v1/jobs?status=won&limit=2" \
-H "X-API-Key: sk_your_api_key_here"
{
"data": [
{
"id": 70412,
"name": "Northgate Plaza Sealcoat",
"company": "Northgate Property Group",
"contact_name": "Pat Sample",
"contact_email": "[email protected]",
"contact_phone": "512-555-0199",
"bill_to_name": null,
"bill_to_email": null,
"address": "1200 Commerce Dr, Austin, TX 78701",
"description": "Sealcoat and restripe the front lot.",
"notes": null,
"job_type": "sealcoating,striping",
"job_types": ["sealcoating", "striping"],
"status": "won",
"status_label": "Won",
"source": "api",
"priority": "normal",
"estimated_value": 48500.0,
"probability": 100,
"start_date": "2026-11-02",
"end_date": null,
"next_follow_up": null,
"measurement_count": 1,
"firm_id": 1201,
"team_id": null,
"client_account_id": 4812,
"client_account_external_ref": "crm-acct-20931",
"location_id": 9307,
"location_external_ref": "crm-site-5512",
"external_ref": "crm-deal-88120",
"custom_fields": {},
"quickbooks_sync_error": null,
"assigned_to_user_id": 311,
"assigned_to": {
"id": 311,
"name": "Chris Example",
"username": "chris",
"email": "[email protected]",
"role": "member",
"job_title": "Estimator",
"phone_number": "512-555-0110",
"headshot_url": null,
"is_firm_owner": false,
"created_at": "2026-03-02T16:20:11.902344"
},
"created_at": "2026-09-30T17:44:02.118003",
"updated_at": "2026-10-09T13:12:47.660215"
}
],
"pagination": { "total": 87, "limit": 2, "offset": 0 }
}
Errors: 400 updated_after must be an ISO 8601 timestamp. 403 API key lacks job read permission.
Get a job#
GET /v1/jobs/{id}
Permission: Read jobs
| Name | Type | Required | Description |
|---|---|---|---|
id |
integer (path) | Yes | Job id. |
include_assignees |
boolean | No | true to add the assignees list. |
curl "https://trutec.ai/api/v1/jobs/70412?include_assignees=true" \
-H "X-API-Key: sk_your_api_key_here"
Returns {"data": {job}}. With include_assignees=true the job also has an assignees array of user objects.
Errors: 404 Job not found.
Create a job#
POST /v1/jobs
Permission: Write jobs
Creates a job in the pipeline. With external_ref, this call is an upsert: if a job in the workspace already has that ref, TruTec updates it with the fields you sent (same rules as Update a job) and returns 200 with "created": false. Otherwise it creates the job and returns 201 with "created": true.
| Name | Type | Required | Description |
|---|---|---|---|
name |
string | Yes, when creating | Job name. Max 200 characters. |
external_ref |
string | No | Your id for the job. Max 255 characters. Makes the call an upsert. |
address |
string | No | Job site address. TruTec links the job to the site at this address, creating one if needed. |
company |
string | No | Customer name. Links to the workspace's customer with that name, or creates one. |
client_account_id |
integer | No | Link an existing customer by TruTec id. Wins over company. |
client_account_external_ref |
string | No | Link an existing customer by your id. Wins over company. |
contact_name |
string | No | Max 200 characters. |
contact_email |
string | No | Max 120 characters. |
contact_phone |
string | No | |
bill_to_name |
string | No | Billing entity, when not the customer. Max 200 characters. |
bill_to_email |
string | No | Max 255 characters. |
job_type |
string or array | No | One or more values from List job types. A comma-separated string or a list. |
status |
string | No | lead (default), active, won or lost. |
priority |
string | No | low, normal (default), high or urgent. |
source |
string | No | Defaults to api. Max 100 characters. |
description, notes |
string | No | Free text. |
estimated_value |
number | No | Dollars. |
probability |
integer | No | 0–100. |
start_date, end_date, next_follow_up |
string | No | YYYY-MM-DD. |
assigned_to_user_id |
integer | No | Owner. Defaults to the user who created the API key, then the workspace owner. |
allow_duplicate_name |
boolean | No | true to create a job even though one with the same name exists. |
Longer text values are cut to the maximum length.
Duplicate names. Without external_ref, a job whose name matches an existing job (case and spacing ignored) is refused with 409, so a retry never creates a twin. Send allow_duplicate_name: true for repeat work under the same name. With external_ref, the ref is the identity and the name check does not apply.
Workspace intake rules. A workspace can require fields on every new job (address, company, job type, source or contact email). Missing ones return 422. A linked client_account_id or client_account_external_ref counts as the company.
curl -X POST "https://trutec.ai/api/v1/jobs" \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"external_ref": "crm-deal-88120",
"name": "Northgate Plaza Sealcoat",
"address": "1200 Commerce Dr, Austin, TX 78701",
"client_account_external_ref": "crm-acct-20931",
"contact_name": "Pat Sample",
"contact_email": "[email protected]",
"job_type": ["sealcoating", "striping"],
"estimated_value": 48500,
"assigned_to_user_id": 311
}'
{
"data": {
"id": 70412,
"name": "Northgate Plaza Sealcoat",
"company": "Northgate Property Group",
"address": "1200 Commerce Dr, Austin, TX 78701",
"job_types": ["sealcoating", "striping"],
"status": "lead",
"client_account_id": 4812,
"client_account_external_ref": "crm-acct-20931",
"location_id": 9307,
"location_external_ref": "crm-site-5512",
"external_ref": "crm-deal-88120",
"assigned_to_user_id": 311,
"created_at": "2026-10-12T14:06:31.220145",
"updated_at": "2026-10-12T14:06:31.220145"
},
"created": true
}
(Response trimmed. data is the full job object.)
Creating or updating a job sends the job_created, job_updated or job_status_changed webhook, the same as an edit in the app.
Errors:
400name is required400client_account_id not found in this workspace/client_account_external_ref "…" not found in this workspace400for a bad value:status must be one of: lead, active, won, lost,priority must be one of: high, low, normal, urgent,start_date must be in YYYY-MM-DD format,probability must be between 0 and 100,estimated_value must be a number404assigned_to_user_id does not match a user in this firm409duplicate name. The body includes the existing job so you can link to it:
{
"error": "Job \"Northgate Plaza Sealcoat\" already exists. Add another takeoff to the existing job, or send allow_duplicate_name: true to create another job with this name.",
"duplicate_job_id": 70412,
"duplicate_job": { "id": 70412, "name": "Northgate Plaza Sealcoat" }
}
409{"error": "external_ref \"crm-deal-88120\" is already used by another job in this workspace", "code": "external_ref_conflict", "existing_job_id": 70412}when two requests race on the same ref. Retry and the second call updates the job.422{"error": "This workspace requires: Address, Company", "missing_fields": ["address", "company"]}422with"code": "bill_to_company_only"when the workspace sets billing on the customer only. Link the job to the customer that should be billed instead of sendingbill_to_nameorbill_to_email.
Update a job#
PATCH /v1/jobs/{id}
Permission: Write jobs
Changes only the fields you send. Accepts every field from Create a job except allow_duplicate_name, plus:
| Name | Type | Required | Description |
|---|---|---|---|
id |
integer (path) | Yes | Job id. |
close_reason |
string | No | Why the job was won or lost. Max 100 characters. |
close_notes |
string | No | Free text. |
Rules:
namecannot be blank.assigned_to_user_idcannot benull.- Send a blank
external_refto clear it. - Changing the customer. Send
client_account_idorclient_account_external_ref. Sendclient_account_id: nullto unlink the customer. On a job that already has a customer,companytext only changes the name shown on the job; it never switches or unlinks the customer. On a job with no customer yet,companytext links or creates one. - Changing the billing entity (a different
bill_to_name, or a different customer on a job with no bill-to) makes the next QuickBooks Online sync look up the billing customer again.
curl -X PATCH "https://trutec.ai/api/v1/jobs/70412" \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"status": "won", "close_reason": "Best price", "start_date": "2026-11-02"}'
Returns {"data": {job}}.
Errors:
404Job not found400name cannot be empty,assigned_to_user_id cannot be null, and the same value errors as create409external_ref_conflictwhen another job holds the ref (body includesexisting_job_id)403with"locked": truewhen the workspace locks won jobs and this move is not allowed (for example, moving a won job back tolead). Nothing in the request is saved.422bill_to_company_only, as on create
Pipeline#
A workspace's sales pipeline is a list of stages (columns on the jobs board). Workspaces rename them and add their own; each stage has a stage_type that says what it means for reports: open, won, lost or excluded (no-bid, cancelled).
PATCH /v1/jobs/{id} accepts the four built-in statuses. To move a job to any stage the workspace uses, with the same rules as dragging it on the board, use Move a job to a stage.
List pipeline stages#
GET /v1/job-stages
Permission: Read jobs
The stages in board order. Lost and excluded stages list the outcome_questions a move into them must answer.
curl "https://trutec.ai/api/v1/job-stages" \
-H "X-API-Key: sk_your_api_key_here"
{
"data": [
{"key": "lead", "label": "New lead", "position": 0, "stage_type": "open", "is_warranty": false, "color": "#6366f1", "default_probability": 10, "outcome_questions": []},
{"key": "bid_sent", "label": "Bid sent", "position": 1, "stage_type": "open", "is_warranty": false, "color": "#3b82f6", "default_probability": 40, "outcome_questions": []},
{"key": "won", "label": "Won", "position": 2, "stage_type": "won", "is_warranty": false, "color": "#10b981", "default_probability": 100, "outcome_questions": []},
{
"key": "lost", "label": "Lost", "position": 3, "stage_type": "lost", "is_warranty": false, "color": "#ef4444", "default_probability": 0,
"outcome_questions": [
{"key": "reason", "label": "Why did we lose this one?", "field_type": "select", "options": ["Price", "Schedule", "Relationship", "Response time", "Cancelled", "No reason given", "Work not done"], "required": true},
{"key": "competitor", "label": "Which vendor did they go with?", "field_type": "text", "options": [], "required": false},
{"key": "percent_high", "label": "How much were we high (%)?", "field_type": "percent", "options": [], "required": false}
]
}
]
}
Move a job to a stage#
POST /v1/jobs/{id}/move
Permission: Write jobs
Moves the job the way dragging it on the board does:
- the won-job lock applies (when the workspace turned it on, only permitted people can move a job out of a won stage); the key's creator must be allowed;
- a move into a lost or excluded stage must answer that stage's questions (why it was lost, who won it);
- a move into a completed stage completes the job's open work orders, after you confirm;
- the stage's follow-up automations and sales sequences run, teammates on the job get the usual email, and webhooks fire.
The activity log records "Status changed from ... to ... via API".
| Name | Type | Required | Description |
|---|---|---|---|
status |
string | Yes | Stage key from GET /v1/job-stages. |
outcome_answers |
object | For lost / excluded stages | Answers by question key, e.g. {"reason": "Price", "percent_high": 12}. |
reason, competitor |
string | No | Shortcuts for outcome_answers.reason and outcome_answers.competitor. |
confirm_schedule_completion |
boolean | No | Confirm completing the job's open work orders. |
confirm_sub_pricing_override |
boolean | No | Owners: move past the subcontractor pricing hold, when the workspace uses it. |
curl -X POST "https://trutec.ai/api/v1/jobs/70412/move" \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"status": "lost", "reason": "Price", "competitor": "Acme Sealcoat"}'
{
"data": {"id": 70412, "name": "Northgate Plaza Sealcoat", "status": "lost", "status_label": "Lost", "...": "..."},
"moved": true,
"previous_status": "bid_sent",
"schedule_completion": null
}
data is the full job object. Moving a job to the stage it is already in returns "moved": false and changes nothing.
Errors:
400invalid_statuswith the list ofstatuses403status_locked: the won-job lock refused the move409confirmation_required: the move would completeactive_work_order_countopen work orders; sendconfirm_schedule_completion: true409sub_pricing_override_required: subcontractor pricing is still held; an owner can resend withconfirm_sub_pricing_override: true422outcome_answers_required: the stage asks questions; the body lists them inquestions. Nothing is saved.
Job team#
Every job has one sales owner (assigned_to_user_id, changed with PATCH /v1/jobs/{id}) and can have more teammates on it. Teammates whose access is limited to their assigned jobs can then see it.
List the job team#
GET /v1/jobs/{id}/assignees
Permission: Read jobs
{
"data": [
{"user_id": 131, "name": "Riley Ford", "email": "[email protected]", "is_sales_owner": false, "assigned_by_user_id": 118, "assigned_at": "2026-10-12T14:10:00Z"}
],
"sales_owner": {"user_id": 118, "name": "Dana Whitfield", "email": "[email protected]"}
}
Add a teammate to a job#
POST /v1/jobs/{id}/assignees
Permission: Write jobs
Body: {"user_id": 131}. They get the usual "you were assigned" email and the job's activity log records it. Returns 201 with the row.
Errors: 400 user_id is required; 404 for a user outside the workspace; 409 already_assigned.
Remove a teammate from a job#
DELETE /v1/jobs/{id}/assignees/{user_id}
Permission: Write jobs
Returns 204. Errors: 404 when the user is not on the job; 409 sales_owner_required: give the job a new owner first.
People, notes, follow-ups and custom fields#
- People on a job (
GET/POST /v1/jobs/{id}/contacts): Contacts - Activity log, notes and logged calls (
/v1/jobs/{id}/activity,/v1/jobs/{id}/notes) and follow-ups (/v1/tasks?job_id=): Notes & tasks - Custom field values (
PATCH /v1/jobs/{id}/custom-fields): Custom fields - New requests from your website: Leads
Job types and users#
List job types#
GET /v1/job-types
Permission: Any active key
Returns the job types configured for the workspace (the platform defaults when none are configured). Send the value strings as job_type when you create or update jobs.
curl "https://trutec.ai/api/v1/job-types" \
-H "X-API-Key: sk_your_api_key_here"
{
"data": [
{ "value": "sealcoating", "label": "Sealcoating" },
{ "value": "striping", "label": "Striping" },
{ "value": "paving", "label": "Paving (New Pavement)" },
{ "value": "mill_overlay", "label": "Mill & Overlay" }
]
}
This list is not paginated.
List users#
GET /v1/users
Permission: Any active key
Returns the workspace's team members. Use the id values as assigned_to_user_id on jobs.
| Name | Type | Required | Description |
|---|---|---|---|
role |
string | No | Only users with this role, for example owner or member. |
q |
string | No | Search username, email and job title (case-insensitive). |
limit |
integer | No | Page size. Default 50, maximum 200. |
offset |
integer | No | Rows to skip. Default 0. |
curl "https://trutec.ai/api/v1/users?q=estimator" \
-H "X-API-Key: sk_your_api_key_here"
{
"data": [
{
"id": 311,
"name": "Chris Example",
"username": "chris",
"email": "[email protected]",
"role": "member",
"job_title": "Estimator",
"phone_number": "512-555-0110",
"headshot_url": null,
"is_firm_owner": false,
"created_at": "2026-03-02T16:20:11.902344"
}
],
"pagination": { "total": 1, "limit": 50, "offset": 0 }
}
Photos#
List job photos#
GET /v1/jobs/{id}/photos
Permission: Read jobs
Returns the site photos and videos on a job, in the order the office arranged them.
| Name | Type | Required | Description |
|---|---|---|---|
id |
integer (path) | Yes | Job id. |
stage |
string | No | before, during, after or general. Other values are ignored. |
visibility |
string | No | external (customer-visible) or internal (team only). |
include_in_report |
boolean | No | Only photos that are (or are not) marked for the customer report. |
limit |
integer | No | Page size. Default 50, maximum 100. |
offset |
integer | No | Rows to skip. Default 0. |
curl "https://trutec.ai/api/v1/jobs/70412/photos?stage=before" \
-H "X-API-Key: sk_your_api_key_here"
{
"data": [
{
"id": 55120,
"job_id": 70412,
"url": "https://files.example/photos/55120.jpg",
"thumbnail_url": "https://files.example/photos/55120_thumb.jpg",
"annotated_url": null,
"caption": "Alligator cracking at the east entrance",
"description": "Alligator cracking at the east entrance",
"title": null,
"stage": "before",
"include_in_report": true,
"visibility": "external",
"is_internal": false,
"taken_at": "2026-10-01T15:40:12",
"taken_by": {
"id": 311,
"name": "Chris Example",
"username": "chris",
"email": "[email protected]"
},
"lat": 30.2711,
"lng": -97.7437,
"compass_heading": 182.0,
"original_filename": "IMG_4471.jpg",
"is_video": false,
"annotations": {},
"tags": [],
"created_at": "2026-10-01T15:41:03.882190",
"updated_at": "2026-10-01T15:41:03.882190"
}
],
"pagination": { "total": 14, "limit": 50, "offset": 0 }
}
url is the original file. annotated_url is a full-size copy with the office's markup drawn on, when one exists. caption and description hold the same text. tags lists the photo's tags (id, name, color and more).
Errors: 400 visibility must be external or internal. 404 Job not found.
Proposal map#
Download the proposal map#
GET /v1/jobs/{id}/exports/proposal_map
Permission: Read jobs and Read measurements
Returns the job's takeoff map as a file: aerial imagery with the measured areas drawn on, your workspace logo, and takeoff notes. This is the same map the proposal uses.
Without measurement_id, TruTec picks the most recently updated takeoff on the job that has a saved proposal-map layout, or else the most recent takeoff with drawn areas.
| Name | Type | Required | Description |
|---|---|---|---|
id |
integer (path) | Yes | Job id. |
format |
string | No | pdf (default) or png. |
measurement_id |
integer | No | Which takeoff to render, when the job has more than one. |
curl "https://trutec.ai/api/v1/jobs/70412/exports/proposal_map?format=pdf" \
-H "X-API-Key: sk_your_api_key_here" \
-o proposal-map.pdf
The response is the file (application/pdf or image/png), sent as an attachment named after the job, for example Northgate Plaza Sealcoat - Proposal Map.pdf. Two headers tell you what was rendered:
| Header | Description |
|---|---|
X-TruTec-Job-Id |
The job id. |
X-TruTec-Measurement-Id |
The takeoff used. |
Errors:
400format must be either pdf or png/measurement_id must be an integer403API key lacks measurement read permission404Job not found/Measurement not found for this job/No proposal map export is available for this job422Unable to render proposal map export for this job
Change orders#
List change orders#
GET /v1/change-orders
Permission: Read jobs
Returns change orders across the workspace, or for one job, most recently updated first. Each row is ready for accounting: the contract change, the approval, and the lines grouped by cost code.
| Name | Type | Required | Description |
|---|---|---|---|
job_id |
integer | No | Only this job's change orders. |
status |
string | No | draft, sent, approved, declined or void. Default: all. |
limit |
integer | No | Page size. Default 50, maximum 200. |
offset |
integer | No | Rows to skip. Default 0. |
This endpoint uses a different envelope from the other lists: change_orders, total, limit and offset at the top level.
curl "https://trutec.ai/api/v1/change-orders?job_id=70412&status=approved" \
-H "X-API-Key: sk_your_api_key_here"
{
"change_orders": [
{
"id": 318,
"co_number": 2,
"kind": "full",
"kind_label": "Full detail pricing",
"pricing_mode": "revision",
"title": "Add ADA stall at east entrance",
"description": "Add one van-accessible stall with signage.",
"status": "approved",
"stage": null,
"approved_at": "2026-10-03T18:12:44.210933",
"approval_source": "customer_link",
"signature_name": "Pat Sample",
"job_id": 70412,
"job_number": 1186,
"job_name": "Northgate Plaza Sealcoat",
"customer": "Northgate Property Group",
"customer_email": "[email protected]",
"amount_delta": 2450.0,
"tax_delta": 0.0,
"cost_delta": 1610.0,
"days_delta": 1,
"revised_contract": 50950.0,
"ep_estimate_id": 9921,
"est_proposal_id": null,
"exported_at": null,
"export_ref": null,
"lines": [
{
"cost_code": "02-STRIPE",
"name": "ADA stall",
"qty": 1.0,
"unit": "EA",
"price": 2450.0,
"cost": 1610.0,
"lines": 3
}
]
}
],
"total": 1,
"limit": 50,
"offset": 0
}
| Field | Description |
|---|---|
co_number |
Change order number within the job. |
kind |
quick (a typed price), tm (time and materials) or full (priced from services like an estimate). |
pricing_mode |
revision (the whole job re-priced), additive (only the new work priced) or null for quick change orders. |
status |
draft, sent, approved, declined or void. stage is the workspace's own label for it, if set. |
approved_at |
When it was approved. Null unless status is approved. |
approval_source |
How approval came in, for example customer_link, proposal_signature or office. |
amount_delta, tax_delta, cost_delta |
Change to contract price, sales tax and cost, in dollars. |
days_delta |
Change to the schedule, in days. |
revised_contract |
Original award plus every approved change order up to and including this one. Null for quick change orders. |
ep_estimate_id |
The change-order estimate behind a tm or full change order. Read it with GET /v1/estimates/{id}. |
lines |
Lines by cost code. For revision, each line is the difference from the original award. For additive, every line of the change order. For quick, one line with the typed price. Lines without a cost code are grouped under UNCODED-… codes. |
exported_at, export_ref |
When and how the approved change order reached your accounting system, if it has. |
Errors: 400 status must be one of draft, sent, approved, declined, void.
Clone a job#
POST /v1/jobs/{job_id}/clone
Permission: Write jobs. Returns 201.
Starts a new job from an existing one, for a re-bid next season or repeat work, exactly like Clone in TruTec. The new job copies the customer, contact, address, job types, source, value, priority, description, notes and bid details (customer and property type, bond, liquidated damages, prevailing wage), but not the bid due date, and it lands on the same property. The source job's takeoffs and site assessments are copied onto it too, unless you turn that off. Copies never use a credit, and takeoffs TruTec is still reworking stay behind (takeoffs_held_back).
It acts as the person who created the API key and follows the workspace's job rules (required fields and questions, who owns new jobs). When a rule stops it, the 422 says what is missing. Workspaces whose jobs come in from another system don't take new jobs here (403). Nothing is emailed.
| Field | Type | Description |
|---|---|---|
name |
string | Default: the source name plus " (Copy)". |
status |
string | Pipeline stage key. Default: lead (or the first stage). |
bid_due_date |
string | YYYY-MM-DD. |
assigned_to_user_id |
integer | Who owns the new job. |
copy_takeoffs, copy_assessments |
boolean | Default true. |
allow_duplicate_name |
boolean | A job with the same name is a 409 with duplicate_job_id unless this is true. |
curl -X POST "https://trutec.ai/api/v1/jobs/70412/clone" \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"name": "Northgate Plaza reseal 2027", "bid_due_date": "2027-03-01"}'
{
"data": { "id": 70988, "name": "Northgate Plaza reseal 2027", "status": "lead", "location_id": 2210 },
"created": true,
"cloned_from_job_id": 70412,
"copied": { "takeoffs": 2, "site_assessments": 1, "takeoffs_held_back": 0 },
"message": "Job created — brought over 2 takeoffs and 1 assessment from \"Northgate Plaza reseal\"."
}
data is the full job, as GET /v1/jobs/{job_id} returns it. warnings appears if the copies could not be made (the job is still created).