Esc

↑↓ move↵ openIndex · Pagefind
API preview · design draft
Webhooks

Every state change, pushed to you.

When a CO is approved, an invoice is coded, or an agent asks for permission, we POST a signed event to your endpoint. No polling, no nightly diff.

API preview · design draftSubject to change

One approval, fanned out

Approving CO #14 is one write. The record updates budget, commitments and Pay App #9, and each change becomes an event for whoever subscribed.

Approving CO #14 emits change_order.approved, budget.revised, pay_app.updated and commitment.updated, each delivered as a signed POST to its subscriber.TRIGGER · USER ACTIONCO #14 approved$86,400 · fire dampers · co_14ONE RECORDsign · deliver · retryERP syncchange_order.approved200Budget dashboardbudget.revised200Owner billing portalpay_app.updated200Slack #pm-alertscommitment.updated200Approving CO #14 emits change_order.approved, budget.revised, pay_app.updated and commitment.updated, each delivered as a signed POST to its subscriber.TRIGGER · USER ACTIONCO #14 approved$86,400 · fire dampers · co_14ONE RECORDsign · deliver · retryERP syncchange_order.approved200Budget dashboardbudget.revised200Owner billing portalpay_app.updated200Slack #pm-alertscommitment.updated200
One approval, four signed deliveries. Illustrative design draft.

Event catalog

Planned event types, grouped by object. Use the sidebar filter to jump to one. Events marked with a payload have a full example below.

Projects
  • project.created
  • project.updated
Cost codes
  • cost_code.created
  • cost_code.updated
Commitments
  • commitment.created
  • commitment.updated
Pay apps
Daily logs
  • daily_log.created

30 event types in this draft.

The event object

Every delivery is a POST with one event. data.object is the full object after the change, the same shape the REST API returns. previous_attributes holds only the fields that changed.

  • id
    string
    Unique per event. Dedupe on it; delivery is at-least-once.
  • type
    string
    Event type, e.g. change_order.approved.
  • api_version
    string
    Pinned on the endpoint at create.
  • project
    string
    Project ID, for routing.
  • data.object
    object
    Object after the change.
  • data.previous_attributes
    object
    Changed fields, before values.
POST to your endpoint
{
  "id": "evt_change_ord31",
  "object": "event",
  "type": "change_order.approved",
  "api_version": "2026-10-preview",
  "created_at": "2026-09-30T21:18:42Z",
  "project": "prj_24118",
  "data": {
    "object": {
      "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"
    },
    "previous_attributes": {
      "status": "pending_approval"
    }
  }
}
Changes

change_order.approved

A CO was approved. Budget, commitments and the next pay app have already been updated when you receive it.

data.object is a change_order. Changed: status.

Payload
{
  "id": "evt_change_ord31",
  "object": "event",
  "type": "change_order.approved",
  "api_version": "2026-10-preview",
  "created_at": "2026-09-30T21:18:42Z",
  "project": "prj_24118",
  "data": {
    "object": {
      "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"
    },
    "previous_attributes": {
      "status": "pending_approval"
    }
  }
}
Changes

change_order.submitted

A draft CO was sent for approval.

data.object is a change_order. Changed: status.

Payload
{
  "id": "evt_change_ord31",
  "object": "event",
  "type": "change_order.submitted",
  "api_version": "2026-10-preview",
  "created_at": "2026-09-30T21:18:42Z",
  "project": "prj_24118",
  "data": {
    "object": {
      "id": "co_14",
      "object": "change_order",
      "project": "prj_24118",
      "number": 14,
      "title": "Add fire dampers — Level 3 corridor",
      "status": "pending_approval",
      "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": "pa_9",
      "created_at": "2026-09-24T16:40:09Z",
      "updated_at": "2026-09-30T21:18:42Z",
      "audit_trail": "/v1/change-orders/co_14/audit"
    },
    "previous_attributes": {
      "status": "draft"
    }
  }
}
Job cost

budget.revised

A budget line changed: approved change, re-forecast, or transfer.

data.object is a budget_line. Changed: approved_changes, revised.

Payload
{
  "id": "evt_budgetrevi31",
  "object": "event",
  "type": "budget.revised",
  "api_version": "2026-10-preview",
  "created_at": "2026-09-30T21:18:42Z",
  "project": "prj_24118",
  "data": {
    "object": {
      "id": "bud_24118_233300",
      "object": "budget_line",
      "project": "prj_24118",
      "cost_code": "cc_233300",
      "original": 21400000,
      "approved_changes": 8640000,
      "revised": 30040000,
      "committed": 28720000,
      "cost_to_date": 19310000,
      "cost_to_complete": 10730000,
      "projected_final": 30040000,
      "variance": 0,
      "created_at": "2026-01-12T15:10:00Z",
      "updated_at": "2026-09-30T21:18:42Z",
      "audit_trail": "/v1/budgets/bud_24118_233300/audit"
    },
    "previous_attributes": {
      "approved_changes": 0,
      "revised": 21400000
    }
  }
}
Job cost

budget.variance_detected

Projected final cost crossed revised budget on a line. Early fade warning.

data.object is a budget_line. No previous attributes.

Payload
{
  "id": "evt_budgetvari31",
  "object": "event",
  "type": "budget.variance_detected",
  "api_version": "2026-10-preview",
  "created_at": "2026-09-30T21:18:42Z",
  "project": "prj_24118",
  "data": {
    "object": {
      "id": "bud_24118_260500",
      "object": "budget_line",
      "project": "prj_24118",
      "cost_code": "cc_260500",
      "original": 21400000,
      "approved_changes": 8640000,
      "revised": 30040000,
      "committed": 28720000,
      "cost_to_date": 19310000,
      "cost_to_complete": 10730000,
      "projected_final": 30040000,
      "variance": -2380000,
      "created_at": "2026-01-12T15:10:00Z",
      "updated_at": "2026-09-30T21:18:42Z",
      "audit_trail": "/v1/budgets/bud_24118_233300/audit"
    }
  }
}
Accounting

invoice.coded

An invoice got a cost code and commitment, by a person or an approved agent action.

data.object is a invoice. Changed: status, cost_code.

Payload
{
  "id": "evt_invoicecod31",
  "object": "event",
  "type": "invoice.coded",
  "api_version": "2026-10-preview",
  "created_at": "2026-09-29T13:12:04Z",
  "project": "prj_24118",
  "data": {
    "object": {
      "id": "inv_4471",
      "object": "invoice",
      "project": "prj_24118",
      "number": "INV-4471",
      "vendor": "vnd_volt_electric",
      "vendor_name": "Volt Electric",
      "commitment": "cmt_po118",
      "amount": 4820000,
      "retainage_held": 482000,
      "status": "needs_review",
      "cost_code": "cc_260500",
      "exceptions": [
        {
          "code": "exceeds_commitment",
          "message": "Exceeds PO-118 remaining by $6,150.00",
          "amount": 615000
        }
      ],
      "coded_by": "act_8812",
      "due_date": "2026-10-25",
      "created_at": "2026-09-29T13:11:50Z",
      "updated_at": "2026-09-29T13:12:04Z",
      "audit_trail": "/v1/invoices/inv_4471/audit"
    },
    "previous_attributes": {
      "status": "received",
      "cost_code": null
    }
  }
}
Accounting

invoice.exception

A check failed: over commitment, duplicate number, missing waiver.

data.object is a invoice. No previous attributes.

Payload
{
  "id": "evt_invoiceexc31",
  "object": "event",
  "type": "invoice.exception",
  "api_version": "2026-10-preview",
  "created_at": "2026-09-29T13:12:04Z",
  "project": "prj_24118",
  "data": {
    "object": {
      "id": "inv_4471",
      "object": "invoice",
      "project": "prj_24118",
      "number": "INV-4471",
      "vendor": "vnd_volt_electric",
      "vendor_name": "Volt Electric",
      "commitment": "cmt_po118",
      "amount": 4820000,
      "retainage_held": 482000,
      "status": "needs_review",
      "cost_code": "cc_260500",
      "exceptions": [
        {
          "code": "exceeds_commitment",
          "message": "Exceeds PO-118 remaining by $6,150.00",
          "amount": 615000
        }
      ],
      "coded_by": "act_8812",
      "due_date": "2026-10-25",
      "created_at": "2026-09-29T13:11:50Z",
      "updated_at": "2026-09-29T13:12:04Z",
      "audit_trail": "/v1/invoices/inv_4471/audit"
    }
  }
}
Accounting

pay_app.submitted

A pay app was locked and sent to the owner.

data.object is a pay_app. Changed: status.

Payload
{
  "id": "evt_pay_appsub31",
  "object": "event",
  "type": "pay_app.submitted",
  "api_version": "2026-10-preview",
  "created_at": "2026-10-01T15:30:00Z",
  "project": "prj_24118",
  "data": {
    "object": {
      "id": "pa_9",
      "object": "pay_app",
      "project": "prj_24118",
      "number": 9,
      "status": "submitted",
      "period_start": "2026-09-01",
      "period_end": "2026-09-30",
      "format": "aia_g702_g703",
      "work_this_period": 231040000,
      "retainage_pct": 10,
      "retainage_this_period": 23104000,
      "net_due_this_period": 207936000,
      "change_orders": [
        "co_14"
      ],
      "lien_waivers_missing": [
        "vnd_apex_steel",
        "vnd_coredry"
      ],
      "created_at": "2026-09-30T22:00:00Z",
      "updated_at": "2026-10-01T15:30:00Z",
      "audit_trail": "/v1/pay-apps/pa_9/audit"
    },
    "previous_attributes": {
      "status": "draft"
    }
  }
}
Accounting

lien_waiver.received

A signed waiver was attached for a vendor and pay app.

data.object is a lien_waiver. Changed: status.

Payload
{
  "id": "evt_lien_waive31",
  "object": "event",
  "type": "lien_waiver.received",
  "api_version": "2026-10-preview",
  "created_at": "2026-10-02T17:45:00Z",
  "project": "prj_24118",
  "data": {
    "object": {
      "id": "lw_9_volt",
      "object": "lien_waiver",
      "project": "prj_24118",
      "pay_app": "pa_9",
      "vendor": "vnd_volt_electric",
      "vendor_name": "Volt Electric",
      "type": "conditional_progress",
      "amount": 4338000,
      "status": "received",
      "file": "file_lw9volt",
      "received_at": "2026-10-02T17:45:00Z",
      "created_at": "2026-09-30T22:00:00Z",
      "updated_at": "2026-10-02T17:45:00Z",
      "audit_trail": "/v1/lien-waivers/lw_9_volt/audit"
    },
    "previous_attributes": {
      "status": "requested"
    }
  }
}
Field

rfi.answered

An RFI got its official response. cost_impact tells you if a CO is likely.

data.object is a rfi. Changed: status, answer.

Payload
{
  "id": "evt_rfianswere31",
  "object": "event",
  "type": "rfi.answered",
  "api_version": "2026-10-preview",
  "created_at": "2026-09-22T19:02:00Z",
  "project": "prj_24118",
  "data": {
    "object": {
      "id": "rfi_212",
      "object": "rfi",
      "project": "prj_24118",
      "number": 212,
      "subject": "Missing fire dampers at L3 corridor rated wall",
      "status": "answered",
      "ball_in_court": "gc",
      "question": "Drawings show a 1-hr rated corridor wall at L3 with no fire dampers at duct penetrations. Confirm dampers required and spec.",
      "answer": "Provide UL 555 fire dampers at all rated penetrations per M-301. Price as change.",
      "due_date": "2026-09-23",
      "answered_at": "2026-09-22T19:02:00Z",
      "cost_impact": true,
      "change_orders": [
        "co_14"
      ],
      "daily_log": "dl_20260918",
      "created_at": "2026-09-18T20:14:00Z",
      "updated_at": "2026-09-22T19:02:00Z",
      "audit_trail": "/v1/rfis/rfi_212/audit"
    },
    "previous_attributes": {
      "status": "open",
      "answer": null
    }
  }
}
Field

rfi.overdue

An RFI passed its due date without an answer.

data.object is a rfi. No previous attributes.

Payload
{
  "id": "evt_rfioverdue31",
  "object": "event",
  "type": "rfi.overdue",
  "api_version": "2026-10-preview",
  "created_at": "2026-09-22T19:02:00Z",
  "project": "prj_24118",
  "data": {
    "object": {
      "id": "rfi_219",
      "object": "rfi",
      "project": "prj_24118",
      "number": 219,
      "subject": "Missing fire dampers at L3 corridor rated wall",
      "status": "open",
      "ball_in_court": "gc",
      "question": "Drawings show a 1-hr rated corridor wall at L3 with no fire dampers at duct penetrations. Confirm dampers required and spec.",
      "answer": null,
      "due_date": "2026-09-23",
      "answered_at": null,
      "cost_impact": true,
      "change_orders": [],
      "daily_log": "dl_20260918",
      "created_at": "2026-09-18T20:14:00Z",
      "updated_at": "2026-09-22T19:02:00Z",
      "audit_trail": "/v1/rfis/rfi_212/audit"
    }
  }
}
Agents

agent.action.pending_approval

An agent proposed a write that needs a human. Route it to Slack, email or your own queue.

data.object is a agent_action. No previous attributes.

Payload
{
  "id": "evt_agentactio31",
  "object": "event",
  "type": "agent.action.pending_approval",
  "api_version": "2026-10-preview",
  "created_at": "2026-09-29T13:12:04Z",
  "project": "prj_24118",
  "data": {
    "object": {
      "id": "act_8812",
      "object": "agent_action",
      "project": "prj_24118",
      "agent": "agt_ap_coding",
      "kind": "invoice.code",
      "status": "pending_approval",
      "target": "inv_4471",
      "proposal": {
        "cost_code": "cc_260500",
        "commitment": "cmt_po118",
        "amount": 4820000
      },
      "reasoning": "Vendor, PO number and line descriptions match PO-118. Amount exceeds remaining commitment by $6,150.00, so a human must approve.",
      "confidence": 0.93,
      "requires_role": "project_accountant",
      "decided_by": null,
      "decided_at": null,
      "created_at": "2026-09-29T13:12:04Z",
      "updated_at": "2026-09-29T13:12:04Z",
      "audit_trail": "/v1/agents/actions/act_8812/audit"
    }
  }
}
Agents

agent.action.approved

A person approved an agent proposal and it was applied.

data.object is a agent_action. Changed: status.

Payload
{
  "id": "evt_agentactio31",
  "object": "event",
  "type": "agent.action.approved",
  "api_version": "2026-10-preview",
  "created_at": "2026-09-29T13:12:04Z",
  "project": "prj_24118",
  "data": {
    "object": {
      "id": "act_8812",
      "object": "agent_action",
      "project": "prj_24118",
      "agent": "agt_ap_coding",
      "kind": "invoice.code",
      "status": "applied",
      "target": "inv_4471",
      "proposal": {
        "cost_code": "cc_260500",
        "commitment": "cmt_po118",
        "amount": 4820000
      },
      "reasoning": "Vendor, PO number and line descriptions match PO-118. Amount exceeds remaining commitment by $6,150.00, so a human must approve.",
      "confidence": 0.93,
      "requires_role": "project_accountant",
      "decided_by": "usr_pat_pa",
      "decided_at": "2026-09-29T15:40:00Z",
      "created_at": "2026-09-29T13:12:04Z",
      "updated_at": "2026-09-29T13:12:04Z",
      "audit_trail": "/v1/agents/actions/act_8812/audit"
    },
    "previous_attributes": {
      "status": "pending_approval"
    }
  }
}

Verify signatures

Each request carries OS-Signature: t=…,v1=…: an HMAC-SHA256 of timestamp.raw_body with your endpoint secret. Verify against the raw bytes before parsing, reject anything older than five minutes, and compare in constant time.

Rotating a secret keeps the old one valid for 24 hours, and both signatures are sent during the overlap.

Handle it

Return any 2xx within 10 seconds, then do slow work async. Store event.id to ignore duplicates. Order is not guaranteed; compare data.object.updated_at if it matters.

Verify
import crypto from "node:crypto";

// Header: OS-Signature: t=1759267123,v1=5f2b…
export function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map(p => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // replay window
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
Express handler
app.post("/hooks/os", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body.toString(), req.get("OS-Signature"), process.env.OS_WHSEC)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body);
  if (seen(event.id)) return res.sendStatus(200); // deliveries are at-least-once

  switch (event.type) {
    case "change_order.approved":
      erp.postChangeOrder(event.data.object);       // amount is cents
      break;
    case "agent.action.pending_approval":
      slack.askForApproval(event.data.object);      // human gate
      break;
  }
  res.sendStatus(200); // ack fast, do slow work async
});

Retry policy planned

Anything other than a 2xx, or no answer in 10 seconds, is a failure. We retry with backoff for about a day and a half, then mark the delivery failed. You can replay any event from the last 30 days via the API.

If an endpoint fails every delivery for three days we disable it and email the account owner. Re-enable it and replay what you missed.

Proposed schedule. Tell us if it breaks your integration.

  1. ImmediatelyFirst attempt, 10 s timeout
  2. +1 minRetry 1
  3. +5 minRetry 2
  4. +30 minRetry 3
  5. +2 hRetry 4
  6. +8 hRetry 5
  7. +24 hFinal retry, then marked failed

Which events would you subscribe to first?

Founding contractors decide what ships in v1. Tell us what your ERP, owner portal or warehouse needs to hear.

Shape the API — get early access