> ## 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 From Proposal

> Request a hire — records a pending approval a human must co-sign in the OpenTrain app before the contract is created and any money moves.

Requests a hire of the candidate behind one of your proposals. **This call never hires anyone or moves money.** It records a pending [approval](/docs/developers/concepts/human-approvals) (`type: "proposal_hire"`) and returns `202`; a signed-in human must open `approval.approvalUrl` and confirm in the OpenTrain app. Approvals expire after \~72 hours.

When the human confirms, OpenTrain accepts the proposal, creates the contract, creates the first escrow milestone, and funds it — the milestone amount plus a **10% marketplace fee** plus a **\$9.95 contract initiation fee** (see [credits & billing](/docs/developers/concepts/credits-and-billing)). The human chooses the payment source (card or credit balance) at confirm time. Funds are held, not paid out; payout happens when you [approve the milestone](/docs/developers/api-reference/milestones/approve) later.

Re-requesting with the same milestone terms while a pending approval exists returns the same approval (idempotent). Re-requesting with **different** terms supersedes the old approval, so the human only ever sees one live hire request per proposal. Learn the outcome by polling [`GET /approvals/{id}`](/docs/developers/api-reference/approvals/get) or watching for the `approval.confirmed` event on [`GET /updates`](/docs/developers/api-reference/updates/poll); once confirmed, the contract appears in [`GET /contracts`](/docs/developers/api-reference/contracts/list) with the post-hire job conversation. The AI trainer's identity stays masked — first name + last initial, never an email (see [privacy](/docs/developers/concepts/privacy-and-work-email)).

**Requirements:** `proposals:write` scope + the `public_api_hiring` feature + a **claimed** account (unclaimed accounts get `403` with `details.reason: "account_claim_required"` and a `claimUrl`) + a payment method on file or a [credit balance](/docs/developers/api-reference/credits/balance) covering the full charge (`409` with `details.reason: "payment_method_required"` otherwise). The proposal must be on a job you own.

## Request

<ParamField path="proposalId" type="string" required>
  The proposal to hire from.
</ParamField>

<ParamField body="milestone" type="object" required>
  The first escrow milestone for the new contract. At least one of `name` or `description` must be non-empty.

  <Expandable title="milestone">
    <ParamField body="name" type="string">
      Short milestone name (max 500 chars).
    </ParamField>

    <ParamField body="description" type="string">
      Description of the work to deliver (max 2,000 chars).
    </ParamField>

    <ParamField body="amount" type="number" required>
      Milestone amount in USD. Must be positive. The escrow hold at confirm time covers this amount plus fees.
    </ParamField>

    <ParamField body="dueDate" type="string">
      Optional due date (ISO 8601).
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="confirmNotFitOverride" type="boolean">
  Set `true` to confirm hiring a proposal you previously marked **Not a fit**, after receiving a `409` with `details.reason: "not_fit_confirmation_required"`.
</ParamField>

## Response

Returns `202` — the request is recorded; nothing has been hired or charged yet.

<ResponseField name="approval" type="object">
  The pending approval, in the same shape as [`GET /approvals/{id}`](/docs/developers/api-reference/approvals/get): `{id, type: "proposal_hire", status: "pending", contractId: null, milestoneId: null, jobId, proposalId, approvalUrl, expiresAt, resolvedAt, result, createdAt}`. `contractId` stays `null` until the human confirms and the contract is created.
</ResponseField>

<ResponseField name="message" type="string">
  Explains that a signed-in human must confirm the approval before the freelancer is hired and any money moves.
</ResponseField>

## Errors

| Status | `code`         | Meaning                                                                                                                                                                                          |
| ------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400`  | `BAD_REQUEST`  | Empty body, invalid JSON, or invalid `milestone` (`details.issues` carries zod details — e.g. missing `amount`, or neither `name` nor `description` provided)                                    |
| `401`  | `UNAUTHORIZED` | Missing or invalid token                                                                                                                                                                         |
| `403`  | `FORBIDDEN`    | Missing `proposals:write` scope, `public_api_hiring` disabled, account not claimed (`details.reason: "account_claim_required"`, `details.claimUrl`), or the proposal is on another account's job |
| `404`  | `NOT_FOUND`    | No such proposal                                                                                                                                                                                 |
| `409`  | `CONFLICT`     | Hire request blocked — see the `details.reason` catalog below                                                                                                                                    |

### `409` reason catalog

| `details.reason`                | Meaning                                                                                                                                 | Extra `details` fields |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
| `payment_method_required`       | No payment method on file and the credit balance doesn't cover the full charge; a human must add a card or credits in the OpenTrain app | `billingUrl`           |
| `already_accepted`              | The proposal was already hired                                                                                                          | —                      |
| `not_fit_confirmation_required` | Proposal is marked Not a fit; retry with `confirmNotFitOverride: true`                                                                  | —                      |

<RequestExample>
  ```bash curl 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": "First labeling batch",
        "description": "Label the first 5,000 posts per the guidelines",
        "amount": 300,
        "dueDate": "2026-07-01"
      }
    }'
  ```

  ```bash CLI theme={null}
  opentrain proposals hire --proposal-id <PROPOSAL_ID> \
    --amount 300 \
    --milestone-name "First labeling batch" \
    --milestone-description "Label the first 5,000 posts per the guidelines" \
    --due-date 2026-07-01 --json
  ```

  ```json MCP: opentrain_hire_proposal theme={null}
  {
    "proposalId": "<PROPOSAL_ID>",
    "milestone": {
      "name": "First labeling batch",
      "description": "Label the first 5,000 posts per the guidelines",
      "amount": 300,
      "dueDate": "2026-07-01"
    }
  }
  ```
</RequestExample>

<ResponseExample>
  ```json 202 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": "2026-06-15T10:00:00.000Z",
      "resolvedAt": null,
      "result": null,
      "createdAt": "2026-06-12T10:00:00.000Z"
    },
    "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."
  }
  ```

  ```json 409 (payment method required) theme={null}
  {
    "error": "No payment method on file. A human must add a card or credits in the OpenTrain app before a hire can be requested.",
    "code": "CONFLICT",
    "requestId": "<REQUEST_ID>",
    "details": {
      "proposalId": "<PROPOSAL_ID>",
      "reason": "payment_method_required",
      "billingUrl": "https://app.opentrain.ai/employer-settings?tab=billing"
    }
  }
  ```
</ResponseExample>
