Skip to content
TruTecDevelopers
Get an API key

Schedule & crews

Read and build the crew schedule, schedule won work from the queue, move and staff visits, publish to crews, and manage crews and the people on them.

View as Markdown

The schedule is made of work orders: one scheduled visit of a crew to a job. A visit has a start date, a length in working days, an optional start and finish time, a crew, and the people and equipment on it. A job can have many visits (phases, return trips, repeat service).

These endpoints run the same rules as the Schedule page in TruTec: crew-day locks, double-booking checks, skipped days and work weeks, and the undo history all apply.

How the schedule works#

Edits are drafts until you publish. Creating, moving or re-crewing a visit changes the office schedule only. Crews see the change on their phones after POST /v1/schedule/publish, the same as pressing Publish in TruTec. Publishing is a one-way step: it sends the visits to crews and cannot be taken back. Moving a visit that was already published makes a new draft change that the next publish sends.

Who the API acts as. Writes are made as the person who created the API key. Their own TruTec permissions apply: if they can only view the schedule, schedule writes return 403 view_only. Changes show in TruTec's history under their name. If that person has left the workspace, been suspended or switched to another workspace, writes return 403 Key owner unavailable; create a new key. Partner keys act as the workspace owner.

Operations must be on. Scheduling, crews and timesheets are part of TruTec's operations tools. If they are not turned on for the workspace, writes return 403 Operations not enabled.

Dates are YYYY-MM-DD and times are 24-hour local times ("07:30") in the workspace's time zone. Timestamps are UTC with a Z suffix.

Note

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

The work order object#

Field Type Description
id integer Work order id.
job_id, job integer, object The job, with {id, name}.
title string What the crew sees, for example "Sealcoat front lot".
status string unscheduled, scheduled, in_progress, completed or cancelled.
is_hold boolean A tentative "hold a date" block. Holds never publish.
scheduled_date string or null First working day. Null while unscheduled.
work_dates array Every working day the visit covers, after skipped days, days off and the crew's work week.
estimated_days number or null Length in working days.
start_time, end_time string or null Local times. ends_next_day is true for a finish after midnight.
skip_dates array Days this visit skips.
work_weekdays array Weekdays the visit may land on, 1 (Monday) to 7 (Sunday). Empty means any working day.
crew_id, crew integer, object The crew, with {id, name}.
foreman object or null {id, name}.
people array Who is on the visit: the crew's members plus or minus people moved on or off it. Each has a person_key and name.
quantity, unit number, string How much work, for example 42000 SF.
completed_quantity, completed_unit, completion_note What the crew reported.
scope, service_types string, array Scope text and the types of work folded into this visit.
address, latitude, longitude Where the crew goes.
notes, gate_code, staging_notes string or null Dispatch notes for the crew.
color, sequence string, integer Calendar color and the order within the crew's day.
equipment array {id, name} for each machine on the visit.
estimate_id, estimate_service_id integer or null The estimate line the visit was scheduled from.
customer_notify string or null none, email, sms or both. Null follows the workspace default.
customer_notified_at, customer_confirmed_at string or null When the customer was told, and when they confirmed.
dispatched_at, completed_at string or null When the packet was sent, and when the visit was finished.
has_unpublished_changes boolean The next publish would send this visit.
created_at, updated_at string UTC timestamps.

Work orders#

List work orders#

GET /v1/work-orders

Permission: Read jobs

Returns visits ordered by date (unscheduled last). With a date range, a multi-day visit is included when any of its working days falls in the range. With updated_after, results are sorted by updated_at, oldest first.

Name Type Required Description
start_date, end_date string No Date range, up to 366 days.
crew_id integer No Only this crew's visits.
unassigned boolean No true for visits with no crew.
job_id integer No Only this job's visits.
status string No One status, or several separated by commas.
include_holds boolean No Include "hold a date" blocks. Default false.
updated_after string No ISO 8601. Only visits changed after it.
limit, offset integer No Page size (default 50, maximum 200) and rows to skip.
curl "https://trutec.ai/api/v1/work-orders?start_date=2026-11-02&end_date=2026-11-08&crew_id=18" \
  -H "X-API-Key: sk_your_api_key_here"
{
  "data": [
    {
      "id": 4120,
      "job_id": 70412,
      "job": { "id": 70412, "name": "Northgate Plaza reseal" },
      "title": "Sealcoat front lot",
      "status": "scheduled",
      "is_hold": false,
      "phase": null,
      "scheduled_date": "2026-11-03",
      "work_dates": ["2026-11-03", "2026-11-04"],
      "estimated_days": 2.0,
      "start_time": "07:00",
      "end_time": "15:30",
      "ends_next_day": false,
      "skip_dates": [],
      "work_weekdays": [],
      "crew_id": 18,
      "crew": { "id": 18, "name": "Seal Crew 1" },
      "foreman": { "id": 344, "name": "Crew Lead" },
      "people": [
        { "person_key": "person:51", "name": "Crew Lead" },
        { "person_key": "name:laborer 2", "name": "Laborer 2" }
      ],
      "quantity": 42000.0,
      "unit": "SF",
      "completed_quantity": null,
      "completed_unit": null,
      "completion_note": null,
      "scope": "Clean, crack fill and two coats of sealer on the front lot.",
      "service_types": [],
      "address": "1200 Commerce Dr, Austin, TX 78701",
      "latitude": 30.2711,
      "longitude": -97.7437,
      "notes": "Gate opens at 6:45",
      "gate_code": "4412",
      "staging_notes": "Stage the rig behind building C",
      "color": null,
      "sequence": 0,
      "equipment": [{ "id": 902, "name": "Seal rig 2" }],
      "estimate_id": 9840,
      "estimate_service_id": 55120,
      "customer_notify": "email",
      "customer_notified_at": "2026-10-28T16:02:11Z",
      "customer_confirmed_at": null,
      "dispatched_at": null,
      "completed_at": null,
      "created_at": "2026-10-20T15:44:02Z",
      "updated_at": "2026-10-28T16:02:11Z",
      "has_unpublished_changes": false
    }
  ],
  "pagination": { "total": 1, "limit": 50, "offset": 0 }
}

Errors: 400 for a bad date, a range over 366 days, or an unknown status.

Get a work order#

GET /v1/work-orders/{id}

Permission: Read jobs

Returns one work order. A work order in another workspace returns 404 Work order not found.

Create a work order#

POST /v1/work-orders

Permission: Write jobs

Adds a visit. There are two ways:

  • New work on a job: send job_id and title.
  • Queued work from a won estimate: send a queue_id from List the queue (or estimate_service_id). TruTec builds the visit from the estimate line (title, quantity, scope, equipment) exactly as when you drag a queue card onto the board. The job must be in a stage that can be scheduled.

Add crew_id and scheduled_date to place it at the same time; without them the visit waits in the unscheduled queue. The visit is a draft until you publish.

Name Type Required Description
job_id integer Yes, unless queue_id The job.
title string Yes, unless queue_id What the crew sees.
queue_id string No From the queue, for example es-55120 or job-70412.
crew_id integer No One of the workspace's active crews.
scheduled_date string No First working day.
estimated_days number No 0.5 to 365.
work_dates array No Exact working days instead of a start date and length (1 to 120 dates).
start_time, end_time string No Local times. A finish at or before the start ends the next day.
phase, notes, unit, gate_code, staging_notes string No Details for the crew.
color string No #rrggbb.
foreman_user_id integer No A user in the workspace.
customer_notify string No none, email, sms or both. Alerts go out when the visit is published.
equipment_ids array No Equipment ids from your catalog.
skip_dates array No Days to skip.
work_weekdays array No 1 (Monday) to 7 (Sunday).
curl -X POST "https://trutec.ai/api/v1/work-orders" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "job_id": 70412,
    "title": "Sealcoat front lot",
    "crew_id": 18,
    "scheduled_date": "2026-11-03",
    "estimated_days": 2,
    "start_time": "07:00",
    "notes": "Gate opens at 6:45"
  }'

Returns 201 with the work order (see the object above).

Errors:

  • 400 for a missing title or job_id, a malformed value, or an unknown field.
  • 404 when the job, crew or queue item is not in your workspace.
  • 409 crew_day_locked when that crew day is locked, or a crew rule (for example a required co-worker) blocks the assignment; the response lists the locks or violations.
  • If the visit was created but could not be placed, the error includes work_order_id: the visit exists in the queue, so fix the request and PATCH it instead of creating another.

Update or reschedule a work order#

PATCH /v1/work-orders/{id}

Permission: Write jobs

Changes only the fields you send. To move a visit, send a new scheduled_date and, if it changes, crew_id. Send "scheduled_date": null to return it to the unscheduled queue. A move of a published visit can carry a reason (for example "Rain"), which TruTec keeps with the change.

Accepts every field from Create except job_id and queue_id, plus reason. Status is not editable here; use the actions below.

curl -X PATCH "https://trutec.ai/api/v1/work-orders/4120" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "scheduled_date": "2026-11-05", "reason": "Rain" }'

Returns 200 with the updated work order. Errors are the same as Create.

Delete a work order#

DELETE /v1/work-orders/{id}

Permission: Write jobs and Delete records

Removes a visit and frees its crew days. Returns 204. A visit with recorded crew time or workflow history returns 409 workflow_history_recorded; cancel it instead.

Complete, reopen, cancel or unschedule#

POST /v1/work-orders/{id}/complete

POST /v1/work-orders/{id}/reopen

POST /v1/work-orders/{id}/cancel

POST /v1/work-orders/{id}/unschedule

Permission: Write jobs

Action What it does
complete Marks the visit complete. Send {"whole_job": true} to finish every open visit on the job; the response lists the others in also_completed_ids.
reopen Puts a completed visit back to scheduled (or unscheduled if it has no date).
cancel Cancels the visit. If the customer was already told about it, they get a cancellation alert, as in TruTec.
unschedule Returns the whole visit to the queue. Send {"date": "2026-11-04"} to drop only that working day.

Each returns 200 with the work order. Completed visits and holds cannot be unscheduled (400).

Put a person on a visit, or take them off#

POST /v1/work-orders/{id}/people

Permission: Write jobs

Moves one person onto or off a visit without changing the crew, the same as dragging a person on the Dispatch board. Someone already on another visit that day is moved off it (moved_from_work_order_id). By default the change also applies to later visits of the same estimate line (future_work_orders_changed); send carry_forward: false for this visit only. People changes reach crews right away and do not need a publish.

Name Type Required Description
action string Yes assign or remove.
user_id integer One of these A crew member with a TruTec login.
person_key string One of these From List people or the visit's people.
date string No The day you are changing, for a multi-day visit. Default today.
just_this_day boolean No Change only date on a multi-day visit.
carry_forward boolean No Default true.
curl -X POST "https://trutec.ai/api/v1/work-orders/4120/people" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "action": "assign", "user_id": 377, "date": "2026-11-03" }'
{
  "data": { "id": 4120, "people": [ { "person_key": "person:51", "name": "Crew Lead" }, { "person_key": "name:pat rivera", "name": "Pat Rivera" } ] },
  "changed": true,
  "scope": "visit",
  "future_work_orders_changed": 0
}

(data is the full work order; shortened here.)

Queue and publishing#

List the queue#

GET /v1/schedule/queue

Permission: Read jobs

Work that is sold but not on the schedule yet: unscheduled work orders, plus estimate lines from won jobs that have no visit yet. Schedule a queue item with POST /v1/work-orders and its queue_id; an item that is already a work order has a work_order_id instead, so PATCH it.

Name Type Required Description
job_id integer No Only this job's work.
limit, offset integer No Paging.
{
  "data": [
    {
      "queue_id": "es-55120",
      "work_order_id": null,
      "kind": "estimate_service",
      "job_id": 70412,
      "job": { "id": 70412, "name": "Northgate Plaza reseal" },
      "title": "Sealcoating",
      "quantity": 42000.0,
      "unit": "SF",
      "estimated_days": 2.0,
      "suggested_crew_id": 18,
      "estimate_id": 9840,
      "estimate_service_id": 55120,
      "service_types": []
    }
  ],
  "pagination": { "total": 1, "limit": 50, "offset": 0 }
}

kind is estimate_service, task, job (a won job with no estimate lines) or work_order.

Get publish status#

GET /v1/schedule/publish-status

Permission: Read jobs

What the next publish would send: visits in the next 30 days with draft changes.

{
  "data": {
    "status": "draft",
    "unpublished_count": 2,
    "unpublished_work_order_ids": [4120, 4133],
    "unpublished_jobs": [{ "job_id": 70412, "work_order_count": 2 }],
    "last_publication": {
      "id": 812,
      "published_at": "2026-10-30T21:14:05Z",
      "published_by": { "id": 12, "name": "Office Manager" },
      "note": "Week of Nov 2",
      "scope": "all",
      "job_id": null,
      "work_order_count": 14,
      "crew_notified_count": 9
    }
  }
}

Publish the schedule#

POST /v1/schedule/publish

Permission: Write jobs

Sends draft changes to crews, exactly like Publish in TruTec. This reaches people: crew members get an in-app notification for their visits, and customers get the email or text alert set on each visit (customer_notify). Publishing cannot be undone.

Name Type Required Description
job_id integer No Publish only this job's visits. Other drafts stay drafts.
work_order_ids array No Publish only these visits.
note string No Shown in the schedule's publish history.
curl -X POST "https://trutec.ai/api/v1/schedule/publish" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "note": "Week of Nov 2" }'
{
  "data": {
    "published": true,
    "published_count": 14,
    "crew_notified_count": 9,
    "customers_alerted_count": 3,
    "publication": { "id": 813, "published_at": "2026-11-01T18:00:12Z", "published_by": { "id": 12, "name": "Office Manager" }, "note": "Week of Nov 2", "scope": "all", "job_id": null, "work_order_count": 14, "crew_notified_count": 9 }
  }
}

With nothing to send, published is false and the counts are 0.

Schedule notes#

Short notes on a date that are not tied to a job ("Safety meeting 6:30am", "Plant closed"), for the whole workspace, one crew or one foreman.

List schedule notes#

GET /v1/schedule/notes?start_date=2026-11-02&end_date=2026-11-08

Permission: Read jobs

start_date and end_date are required (at most 120 days apart). Optional crew_id.

{
  "data": [
    {
      "id": 301,
      "date": "2026-11-03",
      "text": "Safety meeting 6:30am",
      "color": "amber",
      "crew": { "id": 18, "name": "Seal Crew 1" },
      "foreman": null,
      "created_at": "2026-10-30T14:02:00Z",
      "updated_at": "2026-10-30T14:02:00Z"
    }
  ],
  "pagination": { "total": 1, "limit": 50, "offset": 0 }
}

Create, update or delete a schedule note#

POST /v1/schedule/notes

PATCH /v1/schedule/notes/{id}

DELETE /v1/schedule/notes/{id}

Permission: Write jobs (delete also needs Delete records)

Name Type Required Description
date string Yes on create The day.
text string Yes on create Up to 140 characters.
color string No blue, navy, amber, red, purple or slate.
crew_id integer No One crew.
foreman_user_id integer No One foreman. Not with crew_id.

Create returns 201, update 200, delete 204.

Crews#

The crew object#

Field Type Description
id, name
is_active boolean Archived crews are false and can't be scheduled.
group_name, description string or null From your crew catalog.
headcount, hours_per_day, cost_per_day, working_days_per_week number Crew size and cost used by estimating and capacity.
role_slots array The crew's line-up of roles, in order, for example ["Paver", "Screed", "Roller"].
members array The people on the crew (see below).
created_at, updated_at string UTC timestamps.

A member has id (the membership id), crew_id, user_id (null for an employee with no TruTec login), name, role (lead or member), work_role (their slot, for example Roller), work_classification and has_app_access.

List crews#

GET /v1/crews

Permission: Read jobs

Active crews in the order set on the Crews page. include_inactive=true adds archived crews. Also takes q (name contains), updated_after, limit and offset.

{
  "data": [
    {
      "id": 18,
      "name": "Seal Crew 1",
      "is_active": true,
      "group_name": "Sealcoating",
      "description": null,
      "headcount": 4,
      "hours_per_day": 10.0,
      "cost_per_day": 2480.0,
      "working_days_per_week": 5,
      "role_slots": ["Lead", "Squeegee", "Sprayer"],
      "members": [
        {
          "id": 77,
          "crew_id": 18,
          "user_id": 344,
          "name": "Crew Lead",
          "role": "lead",
          "work_role": "Lead",
          "work_classification": null,
          "has_app_access": true,
          "created_at": "2026-08-02T15:10:00Z"
        }
      ],
      "created_at": "2026-03-11T19:20:41Z",
      "updated_at": "2026-09-30T13:02:10Z"
    }
  ],
  "pagination": { "total": 1, "limit": 50, "offset": 0 }
}

Get a crew#

GET /v1/crews/{id}

Permission: Read jobs

Create a crew#

POST /v1/crews

Permission: Write jobs. The key's creator must be a workspace owner.

Name Type Required Description
name string Yes Up to 200 characters.
role_slots array No Role names, up to 40.

Returns 201. A crew with the same name (any case) is not duplicated: you get that crew back with 200, reactivated if it was archived.

Update a crew#

PATCH /v1/crews/{id}

Permission: Write jobs (owner)

Send name (1 to 120 characters) and/or role_slots (the full new line-up; an empty list clears it). Members keep their work_role when a slot goes away.

Archive a crew#

DELETE /v1/crews/{id}

Permission: Write jobs and Delete records (owner)

Archives the crew (history and past visits keep it). Returns 204. A crew with upcoming scheduled work returns 409; move that work first.

Add a crew member#

POST /v1/crews/{id}/members

Permission: Write jobs (owner)

Name Type Required Description
user_id integer One of these A TruTec login in the workspace.
ep_individual_id or ep_person_id integer One of these An employee from your estimating labor catalog. They join without a login.
role string No lead or member (default). Logins only.
work_role string No Their slot on the crew, for example Roller.

Returns 201 with the member, or 200 if they were already on the crew. A crew rule that blocks the move returns 409 with violations.

Update or remove a crew member#

PATCH /v1/crews/{id}/members/{membership_id} with {"work_role": "Screed"} (blank clears it).

DELETE /v1/crews/{id}/members/{membership_id} (needs Delete records) takes the person off the crew. Returns 204.

People#

List people#

GET /v1/people

Permission: Read jobs

Everyone on any crew roster, one row per person, with the crews they sit on. person_key is the identity the schedule uses for people on visits. Filters: q (name contains), crew_id.

{
  "data": [
    {
      "person_key": "person:51",
      "name": "Crew Lead",
      "user_id": 344,
      "has_app_access": true,
      "license": "cdl_b",
      "skills": ["Screed", "Spanish"],
      "crews": [
        { "id": 18, "name": "Seal Crew 1", "membership_id": 77, "role": "lead", "work_role": "Lead", "is_active": true }
      ]
    }
  ],
  "pagination": { "total": 1, "limit": 50, "offset": 0 }
}

To record time for someone, they need a TruTec login (user_id). See Timesheets. Workspace logins are listed by GET /v1/users.