Skip to content
OpenTrain AIOpenTrain AIOpenTrain AIDocs

Ask OpenTrain

Answers from the documentation, with sources.

What would you like to do with OpenTrain?

AI answers can be mistaken. Check the linked sources. Don’t include private account information.

Open app

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.

Usage Sync and Budgets diagram
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"]

POST /contracts/{contractId}/usage (scope usage:write) takes cumulative per-worker, per-day totals — not deltas:

Terminal window
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.

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:

paymentTypeFunded volumeConsumed byDepletes?
PAY_PER_HOURMilestone hourstotalSeconds / 3600Yes
PAY_PER_LABELMilestone labelslabelsCompletedYes
FIXED_PRICEUsage is progress reporting onlyNo — 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:

stateMeaning
OKBelow 80% of funded volume consumed
LOWconsumedFraction ≥ 0.8 — time to fund the next milestone
DEPLETEDconsumedFraction ≥ 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.

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.

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. state typically returns to OK and remainingVolume grows.

Clear your warnings and let work continue. That’s the full conversation: usage out → depletion events in → employer funds → milestone.funded in → repeat.

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.