DEVELOPER DOCUMENTATION
Usage Sync and Budgets
Report work to OpenTrain contracts, track milestone budget use, and handle budget_low and budget_depleted lifecycle events.
Your platform is where the work happens; OpenTrain is where the contract and the money live. Usage sync connects the two: you report how much each hired AI trainer has worked, OpenTrain converts that into budget consumption against the contract’s funded milestones, and when funding runs low both sides find out — your platform via webhooks, the employer and the AI trainer via OpenTrain notifications. The result is a loop where work never silently outruns funding.
View diagram source
flowchart TD A["AI trainer works in your platform"] --> B["You POST cumulative day totals<br/>to /contracts/{id}/usage"] B --> C["OpenTrain recomputes the budget<br/>against funded milestones"] C --> D{"consumedFraction"} D -->|"≥ 80%"| E["milestone.budget_low webhook<br/>+ employer notified in OpenTrain"] D -->|"≥ 100%"| F["milestone.budget_depleted webhook<br/>+ employer and AI trainer notified"] E --> G["Employer funds the next milestone<br/>(human co-signs in OpenTrain)"] F --> G G --> H["milestone.funded webhook —<br/>budget back to OK, work continues"]Reporting Usage
Section titled “Reporting Usage”POST /contracts/{contractId}/usage (scope usage:write) takes cumulative per-worker, per-day totals — not deltas:
curl -sS -X POST https://app.opentrain.ai/api/partner/v1/contracts/<CONTRACT_ID>/usage \ -H "Authorization: Bearer $OT_PARTNER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "entries": [ { "workDate": "2026-06-12", "totalSeconds": 14400, "tasksCompleted": 52, "labelsCompleted": 410 } ] }'Each entry replaces the stored totals for its (worker, day), which makes retries free: re-POSTing the same report — or a corrected one — never double counts. Send the AI trainer’s full day total each time, on whatever cadence suits you (end of day is enough; hourly is fine too). workerOpentrainUserId is optional and defaults to the contract’s hired AI trainer.
The response returns the recomputed budget, so a usage POST doubles as a budget check.
How Budgets Are Computed
Section titled “How Budgets Are Computed”A contract’s budget is the sum of its funded milestones’ volume measured against consumed work. What counts as volume depends on the contract’s payment type:
paymentType | Funded volume | Consumed by | Depletes? |
|---|---|---|---|
PAY_PER_HOUR | Milestone hours | totalSeconds / 3600 | Yes |
PAY_PER_LABEL | Milestone labels | labelsCompleted | Yes |
FIXED_PRICE | — | Usage is progress reporting only | No — state is always OK |
The budget object (returned by usage POSTs, GET /contracts/{contractId}/budget, and carried on budget webhooks) reports state as one of:
state | Meaning |
|---|---|
OK | Below 80% of funded volume consumed |
LOW | consumedFraction ≥ 0.8 — time to fund the next milestone |
DEPLETED | consumedFraction ≥ 1.0 — funded work is exhausted |
Work the AI trainer does on OpenTrain’s own surfaces counts toward the same budget — your reported usage and first-party work share one ledger, so the numbers you read back are the whole picture.
The Depletion Events
Section titled “The Depletion Events”Crossing a threshold upward emits a webhook to your endpoints (subscribe to the event types like any other):
milestone.budget_low— consumption crossed 80%milestone.budget_depleted— consumption crossed 100%
Each fires once per crossing — continued usage POSTs while the state stays LOW or DEPLETED do not re-fire it. Funding a new milestone lowers consumedFraction; if usage later crosses a threshold again, the event fires again.
All three budget events share one payload shape (milestone is the active funded milestone, or null if none):
{ "contract": { "id": "<CONTRACT_ID>", "status": "active", "jobId": "<OPENTRAIN_JOB_ID>", "title": "Traffic sign annotation" }, "milestone": { "id": "<MILESTONE_ID>", "name": "Week 2", "amountUsd": 280, "volume": 20, "status": "ACTIVE_FUNDED" }, "budget": { "contractId": "<CONTRACT_ID>", "paymentType": "PAY_PER_HOUR", "state": "LOW", "fundedVolume": 40, "fundedAmountUsd": 560, "consumed": { "seconds": 118800, "hours": 33, "labels": 0, "tasks": 87 }, "consumedVolume": 33, "remainingVolume": 7, "consumedFraction": 0.825, "activeMilestone": { "id": "<MILESTONE_ID>", "name": "Week 2", "amountUsd": 280, "volume": 20, "status": "ACTIVE_FUNDED" }, "lastUsageAt": "2026-06-12T18:00:00.000Z" }, "projectLink": { "id": "<LINK_ID>", "jobId": "<OPENTRAIN_JOB_ID>", "externalProjectId": "42", "externalProjectName": "Traffic signs batch 3", "externalProjectUrl": "https://your-platform.example.com/projects/42", "provisioningMode": "PARTNER_WEBHOOK" }}What to do with them in your UI:
- On
budget_low— surface a warning on the project (“~80% of funded hours used”). The employer is simultaneously notified in OpenTrain to fund the next milestone. - On
budget_depleted— tell the AI trainer in your UI that funded work is exhausted and the next milestone needs funding on OpenTrain before continuing. Both the employer and the AI trainer get OpenTrain notifications saying the same thing. Whether to hard-stop task assignment is your call — OpenTrain doesn’t block your platform.
The Re-Funding Loop
Section titled “The Re-Funding Loop”Funding happens on OpenTrain, never through your platform: the employer (or their agent, co-signed by a human) funds the next milestone. The moment that happens you receive:
milestone.funded— same payload shape as above, with the fresh budget.statetypically returns toOKandremainingVolumegrows.
Clear your warnings and let work continue. That’s the full conversation: usage out → depletion events in → employer funds → milestone.funded in → repeat.
Reconciliation
Section titled “Reconciliation”Like all platform webhooks, budget events are triggers, not the source of truth — there is no replay. Poll GET /contracts/{contractId}/budget (scope contracts:read) on a schedule, or rely on the budget object returned by each usage POST, to recover state after downtime.