One API. Every object on the job.
A resource-oriented REST API over the same record your PMs and accountants use. This is a design draft: nothing is live, every shape is open to change, and founding contractors are shaping it with us.
Conventions
Base URL
https://api.os.construction/v1. HTTPS only. JSON in, JSON out.
IDs
Every object has a stable, prefixed id: prj_24118, co_14, inv_4471. Safe to store.
Money
Integer cents with a currency. 8640000 is $86,400.00. No floats near a ledger.
Audit trail
Every object links to an append-only history: who, what, when, and the before/after.
Idempotent writes
Every POST and PATCH takes an Idempotency-Key. Retry freely; we write once.
Versioning
Dated versions pinned per key. Breaking changes ship as a new version, never in place.
Objects
Twelve resources, one data model. A change order points at its RFI, its cost codes and the pay app that billed it, so you never stitch exports together again.
Core
Job cost
The spine of job cost. CSI-style codes per project, shared by budgets, commitments, invoices and change orders.
One budget line per project and cost code. Original, approved changes, committed, actual and cost-to-complete in one row.
Subcontracts and purchase orders. Invoices draw them down; anything over the remaining balance raises an exception.
Changes
Accounting
Vendor bills. Coded to cost codes and commitments, checked against remaining balance, held for retainage.
Owner billings in AIA G702/G703 shape. Approved change orders land on the next pay app automatically.
Conditional and unconditional, progress and final. Tracked per vendor per pay app so nothing gets paid blind.
Field
Questions with a clock. Linked to the daily log that raised them and the change order they cause.
What happened on site today: crews, hours, weather, notes, photos. Where most RFIs and change orders start.
Agents
Platform
Pagination
List endpoints return a list object. Pass next_cursor back as cursor until has_more is false. Cursors are stable under concurrent writes, so a nightly sync never skips a row.
Expanding
Related objects come back as IDs. Ask for them inline with expand[], up to two levels deep.
{
"object": "list",
"url": "/v1/rfis",
"has_more": true,
"next_cursor": "cur_9f2c",
"data": [ { "id": "rfi_212", "object": "rfi", ... } ]
}curl "https://api.os.construction/v1/change-orders/co_14?expand[]=source_rfi&expand[]=pay_app" \
-H "Authorization: Bearer $OS_API_KEY"Errors
Conventional HTTP status codes, plus a machine-readable code and a sentence a PM could read.
- 400invalid_requestMissing or malformed parameter.
- 401unauthenticatedNo key, or a revoked key.
- 403forbiddenKey lacks the scope or role. Agent keys hitting approve endpoints land here.
- 404not_foundNo such object, or not visible to this key.
- 409conflictState does not allow it (e.g. approving a void CO) or idempotency key reused with a different body.
- 422validation_failedBusiness rule failed: CO lines must sum to amount, period already locked.
- 429rate_limitedSlow down. See Retry-After.
{
"error": {
"type": "invalid_request",
"code": "exceeds_commitment",
"message": "Invoice amount exceeds PO-118 remaining commitment by $6,150.00.",
"param": "amount",
"object": "inv_4471",
"request_id": "req_8XkQ2"
}
}All endpoints
- GET
/v1/projectsList projects - GET
/v1/projects/{id}Retrieve a project - PATCH
/v1/projects/{id}Update a project - GET
/v1/cost-codesList cost codes - POST
/v1/cost-codesCreate a cost code - GET
/v1/budgetsList budget lines - PATCH
/v1/budgets/{id}Update cost-to-complete - GET
/v1/commitmentsList commitments - GET
/v1/commitments/{id}Retrieve a commitment - POST
/v1/commitmentsCreate a commitment - GET
/v1/change-orders/{id}Retrieve a change order - GET
/v1/change-ordersList change orders - POST
/v1/change-ordersCreate a change order - POST
/v1/change-orders/{id}/approveApprove a change order - GET
/v1/change-orders/{id}/auditList audit trail - GET
/v1/invoicesList invoices - GET
/v1/invoices/{id}Retrieve an invoice - POST
/v1/invoicesCreate an invoice - PATCH
/v1/invoices/{id}Code an invoice - GET
/v1/pay-apps/{id}Retrieve a pay app - GET
/v1/pay-apps/{id}/linesList SOV lines - POST
/v1/pay-apps/{id}/submitSubmit a pay app - GET
/v1/lien-waiversList lien waivers - PATCH
/v1/lien-waivers/{id}Record a received waiver - GET
/v1/rfisList RFIs - POST
/v1/rfisCreate an RFI - GET
/v1/daily-logsList daily logs - POST
/v1/daily-logsCreate a daily log - GET
/v1/agents/actionsList agent actions - POST
/v1/agents/actions/{id}/approveApprove an action - POST
/v1/agents/actions/{id}/rejectReject an action - POST
/v1/webhooksCreate an endpoint - GET
/v1/webhooksList endpoints - DELETE
/v1/webhooks/{id}Delete an endpoint