Skip to content
TruTecDevelopers
Get an API key

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 Markdown

A 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:

  1. Call with updated_after set to your saved cursor (start with a date far in the past).
  2. Process the page. Save the updated_at of the last row as your new cursor.
  3. 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:

  • 400 name is required
  • 400 client_account_id not found in this workspace / client_account_external_ref "…" not found in this workspace
  • 400 for 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 number
  • 404 assigned_to_user_id does not match a user in this firm
  • 409 duplicate 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"]}
  • 422 with "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 sending bill_to_name or bill_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:

  • name cannot be blank. assigned_to_user_id cannot be null.
  • Send a blank external_ref to clear it.
  • Changing the customer. Send client_account_id or client_account_external_ref. Send client_account_id: null to unlink the customer. On a job that already has a customer, company text only changes the name shown on the job; it never switches or unlinks the customer. On a job with no customer yet, company text 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:

  • 404 Job not found
  • 400 name cannot be empty, assigned_to_user_id cannot be null, and the same value errors as create
  • 409 external_ref_conflict when another job holds the ref (body includes existing_job_id)
  • 403 with "locked": true when the workspace locks won jobs and this move is not allowed (for example, moving a won job back to lead). Nothing in the request is saved.
  • 422 bill_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:

  • 400 invalid_status with the list of statuses
  • 403 status_locked: the won-job lock refused the move
  • 409 confirmation_required: the move would complete active_work_order_count open work orders; send confirm_schedule_completion: true
  • 409 sub_pricing_override_required: subcontractor pricing is still held; an owner can resend with confirm_sub_pricing_override: true
  • 422 outcome_answers_required: the stage asks questions; the body lists them in questions. 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:

  • 400 format must be either pdf or png / measurement_id must be an integer
  • 403 API key lacks measurement read permission
  • 404 Job not found / Measurement not found for this job / No proposal map export is available for this job
  • 422 Unable 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).