Skip to main content
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.

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

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.

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

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.

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.

POST /contracts/{id}/usage

Entry validation, idempotency, and the full response shape.

GET /contracts/{id}/budget

The read-only budget view, field by field.