Skip to main content
This page walks through a complete, working platform integration: a single TypeScript file that links one of your annotation projects to an OpenTrain job, subscribes to lifecycle webhooks, and then provisions and offboards AI trainers automatically as contracts start and end. It was validated end-to-end against a self-hosted open-source annotation tool. The integration touches only generic user/organization APIs on the tool side — find-or-create a user by email, remove an organization membership — so the same structure maps directly onto any annotation platform or internal tool with a members API.

Architecture

The integration is a pure webhook consumer, platform-side only:
  • It talks to OpenTrain exclusively through the Platform API (Authorization: Bearer ot_ptk_…) and signed webhooks.
  • Its only imports are node:crypto (signature verification) and node:http (the listener) — no SDKs, no frameworks.
  • It has three subcommands: link, subscribe, and run.

Prerequisites

  • An annotation tool (or workspace) whose API you administer.
  • A tool-side API token with administrator rights over organization membership. In many tools, a plain member’s token can read and create users but gets 403 when removing memberships — offboarding silently breaks. Test the removal path with your token before going live.
  • An OpenTrain platform token with project-links:write, contracts:read, participants:read, participants:email, and webhooks:manage — and an install granted with PII consent, since provisioning keys off the AI trainer’s Work Email.

Configuration

Every setting is an environment variable with a matching CLI flag:
Env varFlagUsed byValue
OPENTRAIN_BASE_URL--opentrain-urlallhttps://app.opentrain.ai
OPENTRAIN_PARTNER_TOKEN--partner-tokenlink, subscribe (optional for run)ot_ptk_… from the consent screen
OPENTRAIN_WEBHOOK_SECRET--webhook-secretrunwhsec_… from endpoint creation
TOOL_BASE_URL--tool-urllink, runyour tool’s API origin, e.g. http://localhost:8080
TOOL_API_TOKEN--tool-tokenlink, runtool-side admin API token

Step 1: Connect

The customer (an OpenTrain employer) opens your consent deep link, approves the scopes plus the PII consent checkbox, and pastes the one-time ot_ptk_… token into your configuration.
The link command reads the project from your tool’s API and registers it as an OpenTrain project link, so contract events for that job route to this integration:

Step 3: Subscribe

This registers a webhook endpoint for contract.started, contract.ended, project_link.removed, and install.revoked, then prints the one-time whsec_… secret:

Step 4: Run the Listener

The listener does four things in order on every delivery: verify, dedupe, handle, acknowledge.

Verify the Signature

Raw bytes, constant-time compare, 300-second drift window — rejected deliveries get 401:

Dedupe and Acknowledge

Retries reuse the same X-OpenTrain-Delivery ID, so the listener keeps a bounded set of processed IDs and answers 200 duplicate for repeats. Successful handling returns 200; a thrown error returns 500, which makes OpenTrain retry with backoff (1m, 5m, 30m, 120m).

Handle Contract Events

contract.started finds or creates the workspace user by Work Email; contract.ended removes their organization membership:

Fall Back to the Participants Endpoint

If the payload lacks workEmail (or arrived before consent was sorted out), the handler pulls GET /contracts/{contractId}/participants — the same consent gates apply, but this makes the handler robust to redelivered or hand-replayed events:

Failure Handling

FailureWhat happensWhat you do
Handler throws (e.g. your tool’s API briefly down)Listener returns 500; OpenTrain retries up to 5 attemptsUsually nothing — a later retry succeeds
Listener down past the retry windowDeliveries marked FAILED; 10 consecutive failures auto-disable the endpointPATCH {"status": "ACTIVE"}, then redeliver all FAILED
Events from before the subscription existedNever delivered — no backfillReconcile via GET /contracts?status=active

Tool-Side Gotchas

These came up validating the consumer against a real self-hosted tool — check the equivalents in yours:
  • User-list response shapes vary across versions. Some builds return a plain array, others { "results": [...] }. Handle both when searching for a user by email.
  • “Cannot remove self” surfaces oddly. The tool refused to remove the token owner from their own organization with a 405 rather than a clear error — treat unexpected statuses on the removal path as a distinct outcome, not a generic failure.
  • Use an organization admin’s token. Membership removal returned 403 for plain members, which breaks offboarding while provisioning appears to work.
  • Check which auth scheme your token uses. Some tools ship multiple token systems (e.g. a legacy Authorization: Token <key> header that must be explicitly enabled) — verify the scheme your integration uses is switched on.