> ## Documentation Index
> Fetch the complete documentation index at: https://opentrain.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 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](/docs/developers/concepts/authentication), and every action that hires a person or moves money pauses for a [human co-sign](/docs/developers/concepts/human-approvals) — 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.

```mermaid theme={null}
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
```

## 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:

| Requirement                                                           | How to check                                                       | Failure if missing                               |
| --------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------ |
| Claimed account                                                       | `GET /auth/me`                                                     | `403` + `account_claim_required` + `claimUrl`    |
| `proposals:write` scope (hire)                                        | `GET /auth/me` → `scopes`                                          | `403 FORBIDDEN` with `requiredScopes`            |
| `payments:write` scope (milestones/funding)                           | `GET /auth/me` → `scopes`                                          | `403 FORBIDDEN`                                  |
| `public_api_hiring` + `public_api_payments_write` features            | [capabilities probe](/docs/developers/concepts/scopes-and-capabilities) | `403 FORBIDDEN`                                  |
| Payment method on file, or a credit balance covering milestone + fees | `GET /credits` (balance); a human checks billing in the app        | `409` + `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](/docs/developers/concepts/credits-and-billing)) on the confirmation screen.

<Tabs>
  <Tab title="curl">
    ```bash theme={null}
    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 .
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    opentrain proposals hire --proposal-id "<PROPOSAL_ID>" \
      --amount 450 \
      --milestone-name "Batch 1" \
      --milestone-description "First 10k dashcam images with bounding boxes" \
      --due-date 2026-07-15 \
      --json
    ```
  </Tab>

  <Tab title="MCP">
    Call `opentrain_hire_proposal`:

    ```json theme={null}
    {
      "proposalId": "<PROPOSAL_ID>",
      "milestone": {
        "name": "Batch 1",
        "description": "First 10k dashcam images with bounding boxes",
        "amount": 450,
        "dueDate": "2026-07-15"
      }
    }
    ```
  </Tab>
</Tabs>

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

```json theme={null}
{
  "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](/docs/developers/concepts/errors-pagination-limits) with a machine-readable `details.reason`:

| `details.reason`                | Meaning                                                                                      | What to do                                                           |
| ------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `payment_method_required`       | No card on file and the credit balance doesn't cover milestone + fees; includes `billingUrl` | Send your human to `billingUrl` to add a card or credits, then retry |
| `already_accepted`              | Proposal was already hired                                                                   | Read the contract instead (`GET /contracts?jobId=...`)               |
| `not_fit_confirmation_required` | Proposal was marked "Not a fit" in review                                                    | Re-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}`](/docs/developers/api-reference/approvals/get) or watch the `approval.confirmed` event), then read the contract:

<Tabs>
  <Tab title="curl">
    ```bash theme={null}
    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 .
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    opentrain contracts list --status active --json
    opentrain contracts get --contract-id "<CONTRACT_ID>" --json
    ```
  </Tab>

  <Tab title="MCP">
    Call `opentrain_list_contracts` (optional `jobId`, `status`), then `opentrain_get_contract` with `{ "contractId": "<CONTRACT_ID>" }`.
  </Tab>
</Tabs>

```json theme={null}
{
  "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](/docs/developers/concepts/privacy-and-work-email)).
* **`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`:

<Tabs>
  <Tab title="curl">
    ```bash theme={null}
    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 .
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    opentrain milestones create --contract-id "<CONTRACT_ID>" \
      --name "Batch 2" \
      --description "Batch 2: 5k bounding boxes, night-time footage" \
      --amount 250 \
      --json
    ```
  </Tab>

  <Tab title="MCP">
    Call `opentrain_create_milestone`:

    ```json theme={null}
    {
      "contractId": "<CONTRACT_ID>",
      "name": "Batch 2",
      "description": "Batch 2: 5k bounding boxes, night-time footage",
      "amountUsd": 250
    }
    ```
  </Tab>
</Tabs>

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:

<Tabs>
  <Tab title="curl">
    ```bash theme={null}
    curl -sS -X POST https://app.opentrain.ai/api/public/v1/milestones/<MILESTONE_ID>/fund \
      -H "Authorization: Bearer $OT_API_TOKEN" | jq .
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    opentrain milestones fund --milestone-id "<MILESTONE_ID>" --json
    ```
  </Tab>

  <Tab title="MCP">
    Call `opentrain_request_milestone_funding` with `{ "milestoneId": "<MILESTONE_ID>" }`.
  </Tab>
</Tabs>

```json theme={null}
{
  "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](/docs/developers/concepts/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):

<Tabs>
  <Tab title="curl">
    ```bash theme={null}
    curl -sS -X POST https://app.opentrain.ai/api/public/v1/milestones/<MILESTONE_ID>/approve \
      -H "Authorization: Bearer $OT_API_TOKEN" | jq .
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    opentrain milestones approve --milestone-id "<MILESTONE_ID>" --json
    ```
  </Tab>

  <Tab title="MCP">
    Call `opentrain_request_milestone_approval` with `{ "milestoneId": "<MILESTONE_ID>" }`.
  </Tab>
</Tabs>

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

<Tabs>
  <Tab title="curl">
    ```bash theme={null}
    curl -sS https://app.opentrain.ai/api/public/v1/approvals/<APPROVAL_ID> \
      -H "Authorization: Bearer $OT_API_TOKEN" | jq '.approval | {status, result}'
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    opentrain approvals get --approval-id "<APPROVAL_ID>" --json
    ```
  </Tab>

  <Tab title="MCP">
    Call `opentrain_get_approval` with `{ "approvalId": "<APPROVAL_ID>" }`.
  </Tab>
</Tabs>

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:

<Tabs>
  <Tab title="curl">
    ```bash theme={null}
    curl -sS -X POST https://app.opentrain.ai/api/public/v1/contracts/<CONTRACT_ID>/end \
      -H "Authorization: Bearer $OT_API_TOKEN" | jq .
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    opentrain contracts end --contract-id "<CONTRACT_ID>" --json
    ```
  </Tab>

  <Tab title="MCP">
    Call `opentrain_end_contract` with `{ "contractId": "<CONTRACT_ID>" }`.
  </Tab>
</Tabs>

* **No funded milestones** → executes directly: `200 {"ok": true, "contractId": "...", "status": "ended"}`.
* **Funded milestones exist** → `202` with an approval of type `contract_end` — same human-confirm flow as funding, because ending decides what happens to escrowed money.

## Related

<CardGroup cols={2}>
  <Card title="Human Approvals" href="/docs/developers/concepts/human-approvals" icon="user-check">
    The 202 pattern in depth: anatomy, lifecycle, expiry, idempotent re-requests.
  </Card>

  <Card title="Credits and Billing" href="/docs/developers/concepts/credits-and-billing" icon="credit-card">
    Balances, holds, ledger entries, and the top-up flow that feeds hiring.
  </Card>

  <Card title="Stay in Sync" href="/docs/developers/guides/stay-in-sync" icon="refresh-cw">
    contract.created, milestone.status\_changed, payment.pending, approval.confirmed.
  </Card>

  <Card title="API Reference: Contracts" href="/docs/developers/api-reference/contracts/list" icon="file-code">
    Field-level detail for every endpoint used here.
  </Card>
</CardGroup>
