| 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 |
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
Every webhook body is one event record: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
contract.started and contract.ended share one payload shape:
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
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
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
project_link.created and project_link.removed carry the link (for removed, a final snapshot — the row is hard-deleted):
Install Revoked
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
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:
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
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.