Skip to main content
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.

Preconditions Checklist

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

Step 1: Request the Hire (Co-Signed)

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 -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.

Hire Request Conflicts (409)

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

Step 2: Read the Contract

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 -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)

Break the remaining work into milestones as you go. Creating one is a direct write — it starts NOT_FUNDED:
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.

Step 4: Fund a Milestone (Co-Signed)

Funding moves money into escrow, so it returns 202 with a pending approval instead of executing:
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.

Step 5: Release Payment (Co-Signed)

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

Tracking Either Approval to Completion

Poll the approval, or watch for the approval.confirmed event (it fires on every terminal state — confirmed, declined, and expired):
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.

Step 6: End the Contract

Ending is dual-mode — it only needs a co-sign when funded milestones are at stake:
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.

Human Approvals

The 202 pattern in depth: anatomy, lifecycle, expiry, idempotent re-requests.

Credits and Billing

Balances, holds, ledger entries, and the top-up flow that feeds hiring.

Stay in Sync

contract.created, milestone.status_changed, payment.pending, approval.confirmed.

API Reference: Contracts

Field-level detail for every endpoint used here.