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?
| Event | Fires when | What you should do |
|---|---|---|
contract.started | The employer hires an AI trainer on a linked job | Provision the AI trainer in your workspace (by Work Email) |
contract.ended | The contract ends (a human co-signed action in OpenTrain) | Remove the AI trainer’s access |
project_link.created | A project link is created on your install | Record the mapping; optionally reconcile existing contracts |
project_link.removed | A project link is deleted (permanent) | Stop associating that job’s contracts with your project |
install.revoked | The customer disconnects (or reconnects) your app | Cease work for that customer; mark the connection disconnected |
milestone.funded | The employer funds a milestone on a linked contract | Clear budget warnings; work can continue |
milestone.budget_low | Budget consumption crosses 80% of funded volume | Warn on the project; the employer is nudged to fund more |
milestone.budget_depleted | Budget 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.
The Event Record
Section titled “The Event Record”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 Events
Section titled “Contract Events”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.
When workEmail Is Included
Section titled “When workEmail Is Included”The freelancer.workEmail field appears only when all of these hold:
- The install was granted with the explicit PII consent checkbox checked
- The install’s scopes include
participants:email - The AI trainer has an active
@opentrain.workWork 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 Events
Section titled “Project Link Events”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" }}Install Revoked
Section titled “Install Revoked”{ "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 Budget Events
Section titled “Milestone Budget Events”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.
Webhooks Trigger, Pulls Reconcile
Section titled “Webhooks Trigger, Pulls Reconcile”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=activeand 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.