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

Hire and Pay

Request a hire, manage milestones, and move money — every step that hires someone or moves money is co-signed by a human in the OpenTrain app.

Hiring is where the agent surface meets real money, so it layers two safety mechanisms: the account must be claimed, and every action that hires a person or moves money pauses for a human co-sign — including the hire itself. Within those rails, your agent runs the entire lifecycle: request the hire, scope milestones, request funding and payouts, and end the contract.

Hire and Pay diagram
View diagram source
sequenceDiagram
participant A as Agent
participant API as OpenTrain API
participant H as Human owner
A->>API: POST /proposals/{id}/hire {milestone}
API-->>A: 202 approval (pending, type proposal_hire)
A->>H: Share approvalUrl
H->>API: Confirms → contract created, first milestone funded
API-->>A: approval.confirmed event (carries contractId)
A->>API: POST /contracts/{id}/milestones (more scope, unfunded)
A->>API: POST /milestones/{id}/fund
API-->>A: 202 approval (pending)
H->>API: Confirms → escrow funded
Note over A,API: ...work happens, milestone delivered...
A->>API: POST /milestones/{id}/approve
API-->>A: 202 approval (pending)
H->>API: Confirms → payout released
A->>API: POST /contracts/{id}/end
API-->>A: 202 if funded milestones remain, else 200

Before your first hire attempt, verify all of these — each failure mode is a distinct error you’d otherwise meet one at a time:

RequirementHow to checkFailure if missing
Claimed accountGET /auth/me403 + account_claim_required + claimUrl
proposals:write scope (hire)GET /auth/mescopes403 FORBIDDEN with requiredScopes
payments:write scope (milestones/funding)GET /auth/mescopes403 FORBIDDEN
public_api_hiring + public_api_payments_write featurescapabilities probe403 FORBIDDEN
Payment method on file, or a credit balance covering milestone + feesGET /credits (balance); a human checks billing in the app409 + payment_method_required + billingUrl

The hire request never hires anyone or moves money. It records a pending approval (type: "proposal_hire") with your proposed first escrow milestone and returns 202. When your human confirms in the OpenTrain app, OpenTrain accepts the proposal, creates the contract, and funds that milestone — the amount plus a 10% marketplace fee plus a $9.95 contract initiation fee. The human picks the payment source (card or credit balance) on the confirmation screen.

curl

Terminal window
curl -sS -X POST https://app.opentrain.ai/api/public/v1/proposals/<PROPOSAL_ID>/hire \
-H "Authorization: Bearer $OT_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"milestone": {
"name": "Batch 1",
"description": "First 10k dashcam images with bounding boxes",
"amount": 450,
"dueDate": "2026-07-15"
}
}' | jq .

Success is a 202 — nothing has been hired or charged yet:

{
"approval": {
"id": "<APPROVAL_ID>",
"type": "proposal_hire",
"status": "pending",
"contractId": null,
"milestoneId": null,
"jobId": "<JOB_ID>",
"proposalId": "<PROPOSAL_ID>",
"approvalUrl": "https://app.opentrain.ai/approvals/<APPROVAL_ID>",
"expiresAt": "...",
"resolvedAt": null,
"result": null,
"createdAt": "..."
},
"message": "Hire request recorded. A signed-in human must confirm this approval in the OpenTrain app before the freelancer is hired and any money moves."
}

Your job: get approvalUrl in front of your human. Re-requesting with the same milestone terms returns the same pending approval (idempotent); re-requesting with different terms supersedes it, so the human only ever sees one live hire request per proposal. On confirmation, result carries {"hired": true, "contractId": "...", "jobId": "...", "freelancerUserId": "..."} — the human’s confirmed values win if they adjusted the milestone before confirming.

All hire-time conflicts use the standard error envelope with a machine-readable details.reason:

details.reasonMeaningWhat to do
payment_method_requiredNo card on file and the credit balance doesn’t cover milestone + fees; includes billingUrlSend your human to billingUrl to add a card or credits, then retry
already_acceptedProposal was already hiredRead the contract instead (GET /contracts?jobId=...)
not_fit_confirmation_requiredProposal was marked “Not a fit” in reviewRe-send with "confirmNotFitOverride": true if intentional

Once the human confirms, pull contractId from the approval’s result (poll GET /approvals/{id} or watch the approval.confirmed event), then read the contract:

curl

Terminal window
curl -sS "https://app.opentrain.ai/api/public/v1/contracts?status=active" \
-H "Authorization: Bearer $OT_API_TOKEN" | jq .
curl -sS https://app.opentrain.ai/api/public/v1/contracts/<CONTRACT_ID> \
-H "Authorization: Bearer $OT_API_TOKEN" | jq .
{
"contract": {
"id": "<CONTRACT_ID>",
"status": "active",
"title": "Image Segmentation Contract",
"jobId": "<JOB_ID>",
"proposalId": "<PROPOSAL_ID>",
"paymentType": "FIXED",
"rateUsd": 500,
"hasActiveMilestone": true,
"freelancer": {
"userId": "...",
"displayName": "Ada L.",
"country": "United States",
"profilePath": "/profile/ada-l-1a2b3c"
},
"milestones": [
{ "id": "...", "name": "Batch 1", "status": "ACTIVE_FUNDED", "amountUsd": 450 }
],
"jobDmConversationId": "<CONVERSATION_ID>"
}
}

Two things to notice:

  • Identity stays masked post-hire — first name + last initial and a profile path, never a full last name or a personal email (see privacy).
  • jobDmConversationId is the post-hire 1:1 thread with your new AI trainer. Use it directly with GET/POST /messages — no separate “create conversation” step.

Step 3: Add Milestones (Direct — No Money Moves)

Section titled “Step 3: Add Milestones (Direct — No Money Moves)”

Break the remaining work into milestones as you go. Creating one is a direct write — it starts NOT_FUNDED:

curl

Terminal window
curl -sS -X POST https://app.opentrain.ai/api/public/v1/contracts/<CONTRACT_ID>/milestones \
-H "Authorization: Bearer $OT_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Batch 2",
"description": "Batch 2: 5k bounding boxes, night-time footage",
"amountUsd": 250
}' | jq .

Returns 201 with the unfunded milestone. description is required; amountUsd is required on fixed-price contracts. A 409 contract_ended means the contract is no longer accepting milestones.

Funding moves money into escrow, so it returns 202 with a pending approval instead of executing:

curl

Terminal window
curl -sS -X POST https://app.opentrain.ai/api/public/v1/milestones/<MILESTONE_ID>/fund \
-H "Authorization: Bearer $OT_API_TOKEN" | jq .
{
"approval": {
"id": "<APPROVAL_ID>",
"type": "milestone_fund",
"status": "pending",
"approvalUrl": "https://app.opentrain.ai/approvals/<APPROVAL_ID>",
"expiresAt": "..."
},
"message": "A human must confirm this request before any money moves. Share the approvalUrl."
}

Your job: get approvalUrl in front of your human. They review and confirm in the OpenTrain app; the approval expires after ~72 hours. Re-requesting while one is pending returns the same approval (idempotent), so retries are safe. The full anatomy, status lifecycle, and tracking patterns are on Human Approvals.

Funding-specific conflicts: 409 milestone_not_fundable (already funded), 409 payment_method_required (no card and credits don’t cover it — billingUrl included), 409 milestone_cancelled / contract_ended.

When the work is delivered and you’re satisfied, request the payout. Same co-sign shape; the milestone must be ACTIVE_FUNDED (409 milestone_not_funded otherwise):

curl

Terminal window
curl -sS -X POST https://app.opentrain.ai/api/public/v1/milestones/<MILESTONE_ID>/approve \
-H "Authorization: Bearer $OT_API_TOKEN" | jq .

Poll the approval, or watch for the approval.confirmed event (it fires on every terminal state — confirmed, declined, and expired):

curl

Terminal window
curl -sS https://app.opentrain.ai/api/public/v1/approvals/<APPROVAL_ID> \
-H "Authorization: Bearer $OT_API_TOKEN" | jq '.approval | {status, result}'

On confirmation, result carries execution evidence (invoiceId, paymentIntentId / payoutTransactionId). Pending invoices also appear in GET /payments/pending.

Ending is dual-mode — it only needs a co-sign when funded milestones are at stake:

curl

Terminal window
curl -sS -X POST https://app.opentrain.ai/api/public/v1/contracts/<CONTRACT_ID>/end \
-H "Authorization: Bearer $OT_API_TOKEN" | jq .
  • No funded milestones → executes directly: 200 {"ok": true, "contractId": "...", "status": "ended"}.
  • Funded milestones exist202 with an approval of type contract_end — same human-confirm flow as funding, because ending decides what happens to escrowed money.