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

Lifecycle Events

Explore OpenTrain's eight contract, project, install, and budget lifecycle events, including payloads, work email rules, and handling guidance.

The platform integration emits eight event types. They are delivered as signed POSTs to your webhook endpoints and answer two questions: who should have access to your workspace right now, and is there funded budget for the work?

EventFires whenWhat you should do
contract.startedThe employer hires an AI trainer on a linked jobProvision the AI trainer in your workspace (by Work Email)
contract.endedThe contract ends (a human co-signed action in OpenTrain)Remove the AI trainer’s access
project_link.createdA project link is created on your installRecord the mapping; optionally reconcile existing contracts
project_link.removedA project link is deleted (permanent)Stop associating that job’s contracts with your project
install.revokedThe customer disconnects (or reconnects) your appCease work for that customer; mark the connection disconnected
milestone.fundedThe employer funds a milestone on a linked contractClear budget warnings; work can continue
milestone.budget_lowBudget consumption crosses 80% of funded volumeWarn on the project; the employer is nudged to fund more
milestone.budget_depletedBudget consumption crosses 100%Tell the AI trainer funded work is exhausted pending re-funding

Events are emitted only for ACTIVE installs, and contract events only for installs holding a project link whose jobId matches the contract’s job. Emission is best-effort and happens after the underlying business action commits — it never blocks a hire or contract end.

Every webhook body is one event record:

{
"id": "<EVENT_ID>",
"type": "contract.started",
"apiVersion": "v1",
"createdAt": "2026-06-12T10:00:00.000Z",
"resourceId": "<CONTRACT_ID>",
"jobId": "<OPENTRAIN_JOB_ID>",
"data": { "...": "event-type-specific payload below" }
}

id is monotonically increasing — safe to use for ordering. resourceId is the contract ID for contract events, the link ID for project-link events, the install ID for install.revoked, and the milestone ID for milestone-budget events (the contract ID when no milestone applies). jobId is null where no job applies. Always verify the X-OpenTrain-Signature header before trusting any of it.

contract.started and contract.ended share one payload shape:

{
"contract": {
"id": "<CONTRACT_ID>",
"status": "active",
"jobId": "<OPENTRAIN_JOB_ID>",
"title": "Traffic sign annotation",
"startDate": "2026-06-12T10:00:00.000Z",
"endDate": null
},
"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"
},
"freelancer": {
"opentrainUserId": "<USER_ID>",
"displayName": "Maria S.",
"country": "Philippines",
"profileUrl": "https://app.opentrain.ai/profile/<SLUG>",
"workEmail": "maria.1234@opentrain.work"
}
}

On contract.ended, contract.status is "ended" and endDate carries the end timestamp. If multiple links on your install reference the same job, you receive one event per link, each carrying its own projectLink.

The freelancer.workEmail field appears only when all of these hold:

  1. The install was granted with the explicit PII consent checkbox checked
  2. The install’s scopes include participants:email
  3. The AI trainer has an active @opentrain.work Work Email account

Otherwise the field is simply omitted. Personal email addresses are never included under any circumstances — see Privacy and Work Email. If workEmail is missing, you can retry via GET /contracts/{contractId}/participants (same consent gates apply) or skip provisioning and surface the gap to the customer.

project_link.created and project_link.removed carry the link (for removed, a final snapshot — the row is hard-deleted):

{
"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"
}
}
{
"installId": "<INSTALL_ID>",
"partnerAppId": "<APP_ID>",
"partnerAppSlug": "your-app"
}

After this event your token returns 401 on every request. What to stop or archive on your side is your call — OpenTrain does not delete anything in your workspace. The full revocation sequence is described in Consent and Installs.

milestone.funded, milestone.budget_low, and milestone.budget_depleted close the funding loop for platforms that report usage. All three share one payload shape — the contract, the active funded milestone (or null), the full budget object, and the project link:

{
"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"
}
}

The threshold events fire once per upward crossing — repeated usage reports while the state stays LOW or DEPLETED do not re-fire them. The usage-sync guide covers how budgets are computed and what to do on each event.

Treat webhooks as triggers, not as your source of truth:

  • No historical replay. A webhook endpoint only receives events created after it was registered, and contract events are only emitted for jobs linked at hire/end time. Anything earlier must come from a pull.
  • Reconcile on a schedule. Periodically list GET /contracts?status=active and diff against who currently has access in your workspace. Provision anyone missing, offboard anyone whose contract ended while your endpoint was down or auto-disabled.
  • Resolve details by pulling. On any contract event you can re-fetch participants for the authoritative current state rather than relying solely on the payload snapshot.

The reference integration shows this pattern: webhook-driven provisioning with a participants-endpoint fallback.