# Change orders

A **change order** changes a job after it is sold: added or removed scope, a contract change in dollars, and extra days. Change orders are numbered per job (CO #1, #2, ...).

There are three kinds:

| Kind | What it is |
|---|---|
| `quick` | A title, scope text and a fixed dollar amount. |
| `tm` | Time and materials, priced in the estimate editor from the job's estimate. |
| `full` | Full pricing in the estimate editor: new work only, or the whole job revised. |

A change order moves through the workspace's **stages** (for example Estimating, Review, Ready to send, Sent, Approved, Declined). Each stage has a meaning (`working`, `sent`, `approved` or `declined`) that sets the change order's `status`. Approving posts the change to the job's contract value and budget and, when the workspace syncs to QuickBooks Online or another accounting system, exports it, exactly as in TruTec.

Writes act as the person who created the API key and need operations turned on for the workspace (`403` `Operations not enabled` otherwise).

## The change order object

| Field | Type | Description |
|---|---|---|
| `id`, `co_number` | integer | Id, and the number within the job. |
| `kind`, `kind_label`, `pricing_mode` | string | See above. `pricing_mode` is `additive` or `revision` for priced change orders. |
| `title`, `description` | string | |
| `status` | string | `draft`, `sent`, `approved`, `declined` or `void`. |
| `stage`, `stage_label` | string | The workspace stage it sits in. |
| `job_id`, `job_number`, `job_name`, `customer`, `customer_name`, `customer_email` | | The job and who approves. |
| `amount_delta`, `tax_delta`, `cost_delta` | number | Contract, tax and cost change in dollars (+/-). |
| `days_delta` | integer | Schedule days added or removed. |
| `revised_contract` | number or null | The job's contract value including this change order. |
| `lines` | array | Lines by cost code (priced change orders). |
| `ep_estimate_id`, `est_proposal_id` | integer or null | The estimate and proposal behind a priced change order. |
| `approved_at`, `approval_source`, `signature_name`, `signed_at` | | How and when the customer or office approved it. |
| `sent_at`, `decision_comment`, `source` | | `source` is `office` or `field` (raised from the crew app). |
| `exported_at`, `export_ref` | | Accounting export. |
| `created_at`, `updated_at` | string | |

## Change orders

### List change orders

`GET /v1/change-orders`

**Permission:** Read jobs

Takes `job_id`, `status`, `limit` (default 50, maximum 200) and `offset`. This older endpoint returns `{"change_orders": [...], "total", "limit", "offset"}` instead of the usual `data` / `pagination`; each row is the object above without the workflow fields. See [Jobs](jobs.md).

### Get a change order

`GET /v1/change-orders/{id}`

**Permission:** Read jobs

```bash
curl "https://trutec.ai/api/v1/change-orders/5521" \
  -H "X-API-Key: sk_your_api_key_here"
```

```json
{
  "data": {
    "id": 5521,
    "co_number": 2,
    "kind": "quick",
    "kind_label": "Quick",
    "pricing_mode": null,
    "title": "Add 40 LF of curb at the entrance",
    "description": "Remove and replace 40 LF of damaged curb.",
    "status": "sent",
    "stage": "sent",
    "approved_at": null,
    "approval_source": null,
    "signature_name": null,
    "job_id": 70412,
    "job_number": "24-118",
    "job_name": "Northgate Plaza reseal",
    "customer": "Northgate Property Group",
    "customer_email": "pm@northgate.example",
    "amount_delta": 1850.5,
    "tax_delta": 0.0,
    "cost_delta": 0.0,
    "days_delta": 1,
    "revised_contract": 50350.5,
    "ep_estimate_id": null,
    "est_proposal_id": null,
    "exported_at": null,
    "export_ref": null,
    "lines": [],
    "stage_label": "Sent",
    "source": "office",
    "customer_name": "Dana Ortiz",
    "sent_at": "2026-11-04T15:12:40Z",
    "signed_at": null,
    "decision_comment": null,
    "created_at": "2026-11-04T14:58:02Z",
    "updated_at": "2026-11-04T15:12:40Z"
  }
}
```

### Create a change order

`POST /v1/change-orders`

**Permission:** Write jobs

| Name | Type | Required | Description |
|---|---|---|---|
| `job_id` | integer | Yes | |
| `title` | string | Yes for `quick` | Up to 200 characters. |
| `kind` | string | No | `quick` (default), `tm` or `full`. |
| `description` | string | No | The scope change. |
| `amount_delta` | number | No | Dollars, + or -. `quick` only; priced kinds take their amount from the estimate. |
| `days_delta` | integer | No | |
| `customer_name`, `customer_email` | string | No | Who approves it. |
| `pricing_mode` | string | No | `additive` (default) or `revision`, for `full`. |
| `request_key` | string | No | A UUID. Repeating a `tm` or `full` create with the same key returns the same change order. |

A `quick` change order starts as a draft in the first working stage. A `tm` or `full` change order copies the job's estimate into a new change-order estimate (the response's `ep_estimate_id`) to be priced; the key's creator needs the estimator or admin role, and the job needs an estimate (`400` otherwise).

```bash
curl -X POST "https://trutec.ai/api/v1/change-orders" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "job_id": 70412,
    "title": "Add 40 LF of curb at the entrance",
    "amount_delta": 1850.50,
    "days_delta": 1,
    "customer_email": "pm@northgate.example"
  }'
```

Returns `201` with the change order. Nothing is sent to the customer.

### Update a change order

`PATCH /v1/change-orders/{id}`

**Permission:** Write jobs

Send any of `title`, `description`, `amount_delta`, `days_delta`, `customer_name`, `customer_email`. Only draft and sent change orders can change; approved or declined ones return `400`.

### Send a change order to the customer

`POST /v1/change-orders/{id}/send`

**Permission:** Write jobs

**This emails the customer** (`customer_email`) a link to review and sign the change order online, from the key creator's connected mailbox when there is one. The status becomes `sent`. The response has `emailed: false` when there is no customer email; the link is still created and can be sent from TruTec. A decided change order returns `400`.

### Approve or decline

`POST /v1/change-orders/{id}/approve`

`POST /v1/change-orders/{id}/decline`

**Permission:** Write jobs

Records the office's decision, the same as moving the change order to the workspace's Approved or Declined stage. Approving updates the job's contract value and budget and runs any accounting export. A change order the customer already signed cannot be declined.

### Move to a stage

`POST /v1/change-orders/{id}/stage` with `{"stage": "review"}`

**Permission:** Write jobs

Moves to any stage key from `GET /v1/change-order-stages`. A stage that means approved or declined does what **Approve** or **Decline** does. An unknown key returns `400`.

### Void a change order

`POST /v1/change-orders/{id}/void`

**Permission:** Write jobs

Sets the status to `void`. A priced change order that is signed or approved cannot be voided (`409`).

### List stages

`GET /v1/change-order-stages`

**Permission:** Read jobs

```json
{
  "data": [
    { "key": "estimating", "label": "Estimating", "meaning": "working" },
    { "key": "review", "label": "Review", "meaning": "working" },
    { "key": "ready_to_send", "label": "Ready to send", "meaning": "working" },
    { "key": "sent", "label": "Sent", "meaning": "sent" },
    { "key": "approved", "label": "Approved", "meaning": "approved" },
    { "key": "declined", "label": "Declined", "meaning": "declined" }
  ]
}
```

## Deleting change orders

Change orders can't be deleted, in TruTec or through the API. Void one instead (`POST /v1/change-orders/{id}/void`): it stays on the job's list marked void and keeps its number.
