Change orders
Priced scope changes. Approval flows straight into budget, commitments, the next pay app and the forecast. One write, everything updates.
The change_order object
Every object carries an id, timestamps, and an audit_trail. Money is integer cents. Expand any node in the explorer to see how it links to the rest of the record.
- idstringUnique, stable identifier. Prefixed by object type.
- objectstringString naming the object type.
- projectstringProject ID.
- numberintegerSequential CO number on the project.
- titlestringShort description.
- statusenumdraft · pending_approval · approved · rejected · void.
- reasonenumowner_request · hidden_condition · design_error · allowance · other.
- source_rfistringRFI that triggered it, if any.
- amountintegerTotal, cents. Equals the sum of lines.
- schedule_impact_daysintegerRequested time extension.
- linesarrayCost code breakdown: cost_code, description, amount.
- approvalsarrayEvery approval decision, with role, user and time.
- pay_appstringPay app it was billed on, once billed.
- created_attimestampISO 8601, UTC.
- updated_attimestampISO 8601, UTC. Changes on every write.
- audit_trailstringPath to the append-only history of who changed what, and when.
object{17}
- id"co_14"id
- object"change_order"string
- project"prj_24118"id
- number14integer
- title"Add fire dampers — Level 3 corridor"string
- status"approved"string
- reason"hidden_condition"string
- source_rfi"rfi_212"id
- amount8640000integer$86,400.00
- currency"usd"string
- schedule_impact_days3integer
linesarray[3]
0object{3}
- cost_code"cc_233300"id
- description"Fire dampers, material"string
- amount4860000integer$48,600.00
1object{3}
- cost_code"cc_233300"id
- description"Install labor"string
- amount3120000integer$31,200.00
2object{3}
- cost_code"cc_233300"id
- description"Markup"string
- amount660000integer$6,600.00
approvalsarray[1]
0object{4}
- role"project_manager"string
- user"usr_dana_pm"id
- decision"approved"string
- at
- pay_app"pa_9"id
- created_at
- updated_at
- audit_trail"/v1/change-orders/co_14/audit"string
/v1/change-orders/{id}Retrieve a change order
One CO with lines and approvals.
Parameters
- idstringpathrequiredThe change order ID, e.g. co_14.
- expand[]stringqueryInline related objects, e.g. source_rfi.
Returns
The change_order object. Errors use the standard error shape.
curl https://api.os.construction/v1/change-orders/co_14 \
-H "Authorization: Bearer $OS_API_KEY"const res = await fetch("https://api.os.construction/v1/change-orders/co_14", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.OS_API_KEY}`,
},
});
const obj = await res.json();import os, uuid, requests
res = requests.get(
"https://api.os.construction/v1/change-orders/co_14",
headers={
"Authorization": f"Bearer {os.environ['OS_API_KEY']}",
},
)
res.raise_for_status()
data = res.json(){
"id": "co_14",
"object": "change_order",
"project": "prj_24118",
"number": 14,
"title": "Add fire dampers — Level 3 corridor",
"status": "approved",
"reason": "hidden_condition",
"source_rfi": "rfi_212",
"amount": 8640000,
"currency": "usd",
"schedule_impact_days": 3,
"lines": [
{
"cost_code": "cc_233300",
"description": "Fire dampers, material",
"amount": 4860000
},
{
"cost_code": "cc_233300",
"description": "Install labor",
"amount": 3120000
},
{
"cost_code": "cc_233300",
"description": "Markup",
"amount": 660000
}
],
"approvals": [
{
"role": "project_manager",
"user": "usr_dana_pm",
"decision": "approved",
"at": "2026-09-30T21:18:42Z"
}
],
"pay_app": "pa_9",
"created_at": "2026-09-24T16:40:09Z",
"updated_at": "2026-09-30T21:18:42Z",
"audit_trail": "/v1/change-orders/co_14/audit"
}/v1/change-ordersList change orders
Filter by project and status.
Parameters
- projectstringqueryID of the project, e.g. prj_24118.
- statusenumqueryStatus filter.
- limitintegerqueryPage size, 1 to 100. Default 25.
- cursorstringqueryCursor from a previous page’s next_cursor.
Returns
A paginated list of change_order objects. Errors use the standard error shape.
curl https://api.os.construction/v1/change-orders \
-H "Authorization: Bearer $OS_API_KEY"const res = await fetch("https://api.os.construction/v1/change-orders", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.OS_API_KEY}`,
},
});
const page = await res.json();import os, uuid, requests
res = requests.get(
"https://api.os.construction/v1/change-orders",
headers={
"Authorization": f"Bearer {os.environ['OS_API_KEY']}",
},
)
res.raise_for_status()
data = res.json(){
"object": "list",
"url": "/v1/change-orders",
"has_more": false,
"next_cursor": null,
"data": [
{
"id": "co_14",
"object": "change_order",
"project": "prj_24118",
"number": 14,
"title": "Add fire dampers — Level 3 corridor",
"status": "approved",
"reason": "hidden_condition",
"source_rfi": "rfi_212",
"amount": 8640000,
"currency": "usd",
"schedule_impact_days": 3,
"lines": [
{
"cost_code": "cc_233300",
"description": "Fire dampers, material",
"amount": 4860000
},
{
"cost_code": "cc_233300",
"description": "Install labor",
"amount": 3120000
},
{
"cost_code": "cc_233300",
"description": "Markup",
"amount": 660000
}
],
"approvals": [
{
"role": "project_manager",
"user": "usr_dana_pm",
"decision": "approved",
"at": "2026-09-30T21:18:42Z"
}
],
"pay_app": "pa_9",
"created_at": "2026-09-24T16:40:09Z",
"updated_at": "2026-09-30T21:18:42Z",
"audit_trail": "/v1/change-orders/co_14/audit"
}
]
}/v1/change-ordersCreate a change order
Creates a draft. Lines must sum to amount.
Idempotent. Send an Idempotency-Key; retries within 24 hours return the original result and never write twice.
Parameters
- Idempotency-KeystringheaderrequiredAny unique string. Replays return the first result instead of writing twice.
- projectstringbodyrequiredProject ID.
- titlestringbodyrequiredTitle.
- linesarraybodyrequiredcost_code, description, amount.
- source_rfistringbodyLinked RFI.
Returns
The change_order object. Errors use the standard error shape.
curl -X POST https://api.os.construction/v1/change-orders \
-H "Authorization: Bearer $OS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"project": "prj_24118",
"title": "Add fire dampers — Level 3 corridor",
"reason": "hidden_condition",
"source_rfi": "rfi_212",
"lines": [
{
"cost_code": "cc_233300",
"description": "Fire dampers, material",
"amount": 4860000
},
{
"cost_code": "cc_233300",
"description": "Install labor",
"amount": 3120000
},
{
"cost_code": "cc_233300",
"description": "Markup",
"amount": 660000
}
]
}'const res = await fetch("https://api.os.construction/v1/change-orders", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OS_API_KEY}`,
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
"project": "prj_24118",
"title": "Add fire dampers — Level 3 corridor",
"reason": "hidden_condition",
"source_rfi": "rfi_212",
"lines": [
{
"cost_code": "cc_233300",
"description": "Fire dampers, material",
"amount": 4860000
},
{
"cost_code": "cc_233300",
"description": "Install labor",
"amount": 3120000
},
{
"cost_code": "cc_233300",
"description": "Markup",
"amount": 660000
}
]
}),
});
const obj = await res.json();import os, uuid, requests
res = requests.post(
"https://api.os.construction/v1/change-orders",
headers={
"Authorization": f"Bearer {os.environ['OS_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"project": "prj_24118",
"title": "Add fire dampers — Level 3 corridor",
"reason": "hidden_condition",
"source_rfi": "rfi_212",
"lines": [
{
"cost_code": "cc_233300",
"description": "Fire dampers, material",
"amount": 4860000
},
{
"cost_code": "cc_233300",
"description": "Install labor",
"amount": 3120000
},
{
"cost_code": "cc_233300",
"description": "Markup",
"amount": 660000
}
]
},
)
res.raise_for_status()
data = res.json(){
"id": "co_14",
"object": "change_order",
"project": "prj_24118",
"number": 14,
"title": "Add fire dampers — Level 3 corridor",
"status": "draft",
"reason": "hidden_condition",
"source_rfi": "rfi_212",
"amount": 8640000,
"currency": "usd",
"schedule_impact_days": 3,
"lines": [
{
"cost_code": "cc_233300",
"description": "Fire dampers, material",
"amount": 4860000
},
{
"cost_code": "cc_233300",
"description": "Install labor",
"amount": 3120000
},
{
"cost_code": "cc_233300",
"description": "Markup",
"amount": 660000
}
],
"approvals": [],
"pay_app": null,
"created_at": "2026-09-24T16:40:09Z",
"updated_at": "2026-09-30T21:18:42Z",
"audit_trail": "/v1/change-orders/co_14/audit"
}/v1/change-orders/{id}/approveApprove a change order
Requires a user key with approver role. Agent keys cannot call this; they propose via agent actions.
Idempotent. Send an Idempotency-Key; retries within 24 hours return the original result and never write twice.
Parameters
- idstringpathrequiredThe change order ID, e.g. co_14.
- Idempotency-KeystringheaderrequiredAny unique string. Replays return the first result instead of writing twice.
- notestringbodyOptional note for the audit trail.
Returns
The change_order object. Errors use the standard error shape.
curl -X POST https://api.os.construction/v1/change-orders/co_14/approve \
-H "Authorization: Bearer $OS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"note": "Per RFI-212 response"
}'const res = await fetch("https://api.os.construction/v1/change-orders/co_14/approve", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OS_API_KEY}`,
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
"note": "Per RFI-212 response"
}),
});
const obj = await res.json();import os, uuid, requests
res = requests.post(
"https://api.os.construction/v1/change-orders/co_14/approve",
headers={
"Authorization": f"Bearer {os.environ['OS_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"note": "Per RFI-212 response"
},
)
res.raise_for_status()
data = res.json(){
"id": "co_14",
"object": "change_order",
"project": "prj_24118",
"number": 14,
"title": "Add fire dampers — Level 3 corridor",
"status": "approved",
"reason": "hidden_condition",
"source_rfi": "rfi_212",
"amount": 8640000,
"currency": "usd",
"schedule_impact_days": 3,
"lines": [
{
"cost_code": "cc_233300",
"description": "Fire dampers, material",
"amount": 4860000
},
{
"cost_code": "cc_233300",
"description": "Install labor",
"amount": 3120000
},
{
"cost_code": "cc_233300",
"description": "Markup",
"amount": 660000
}
],
"approvals": [
{
"role": "project_manager",
"user": "usr_dana_pm",
"decision": "approved",
"at": "2026-09-30T21:18:42Z"
}
],
"pay_app": "pa_9",
"created_at": "2026-09-24T16:40:09Z",
"updated_at": "2026-09-30T21:18:42Z",
"audit_trail": "/v1/change-orders/co_14/audit"
}/v1/change-orders/{id}/auditList audit trail
Every object has one. Append-only, newest first.
Parameters
- idstringpathrequiredThe change order ID, e.g. co_14.
Returns
A paginated list of change_order objects. Errors use the standard error shape.
curl https://api.os.construction/v1/change-orders/co_14/audit \
-H "Authorization: Bearer $OS_API_KEY"const res = await fetch("https://api.os.construction/v1/change-orders/co_14/audit", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.OS_API_KEY}`,
},
});
const page = await res.json();import os, uuid, requests
res = requests.get(
"https://api.os.construction/v1/change-orders/co_14/audit",
headers={
"Authorization": f"Bearer {os.environ['OS_API_KEY']}",
},
)
res.raise_for_status()
data = res.json(){
"object": "list",
"url": "/v1/change-orders/co_14/audit",
"has_more": false,
"next_cursor": null,
"data": [
{
"id": "aud_5521",
"object": "audit_entry",
"action": "approved",
"actor": "usr_dana_pm",
"at": "2026-09-30T21:18:42Z",
"changes": {
"status": [
"pending_approval",
"approved"
]
}
},
{
"id": "aud_5499",
"object": "audit_entry",
"action": "submitted",
"actor": "usr_lee_pe",
"at": "2026-09-25T14:03:10Z",
"changes": {
"status": [
"draft",
"pending_approval"
]
}
}
]
}Events
Webhooks fire on every state change of a change_order.