> ## Documentation Index
> Fetch the complete documentation index at: https://opentrain.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Usage Sync and Budgets

> Report work done on your platform back to OpenTrain contracts, track milestone budget depletion, and close the funding loop with budget_low / budget_depleted 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.

```mermaid theme={null}
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

[`POST /contracts/{contractId}/usage`](/docs/developers/annotation-platforms/api-reference/contracts/usage) (scope `usage:write`) takes **cumulative per-worker, per-day totals** — not deltas:

```bash theme={null}
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:

| `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`](/docs/developers/annotation-platforms/api-reference/contracts/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

Crossing a threshold **upward** emits a webhook to your [endpoints](/docs/developers/annotation-platforms/webhooks) (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):

```json theme={null}
{
  "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](/docs/developers/concepts/human-approvals)) 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](/docs/developers/annotation-platforms/lifecycle-events#webhooks-trigger-pulls-reconcile), budget events are triggers, not the source of truth — there is no replay. Poll [`GET /contracts/{contractId}/budget`](/docs/developers/annotation-platforms/api-reference/contracts/budget) (scope `contracts:read`) on a schedule, or rely on the budget object returned by each usage POST, to recover state after downtime.

<CardGroup cols={2}>
  <Card title="POST /contracts/{id}/usage" href="/docs/developers/annotation-platforms/api-reference/contracts/usage" icon="upload">
    Entry validation, idempotency, and the full response shape.
  </Card>

  <Card title="GET /contracts/{id}/budget" href="/docs/developers/annotation-platforms/api-reference/contracts/budget" icon="gauge">
    The read-only budget view, field by field.
  </Card>
</CardGroup>
