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

# Fund Milestone

> Request escrow funding for an unfunded milestone — returns a pending approval a human must co-sign before money moves.

Requests escrow funding for a `NOT_FUNDED` milestone. **This call never moves money.** It records a pending [approval](/docs/developers/concepts/human-approvals) (`type: "milestone_fund"`) and returns `202`; a signed-in human must open `approval.approvalUrl` and confirm in the OpenTrain app before the milestone is funded. Approvals expire after \~72 hours.

Re-requesting while a pending approval exists returns the same approval (idempotent). 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). The request has no body. Funding is drawn per the account's billing setup — see [credits & billing](/docs/developers/concepts/credits-and-billing).

**Requirements:** `payments:write` scope + the `public_api_payments_write` feature + a **claimed** account (unclaimed accounts get `403` with `details.reason: "account_claim_required"` and a `claimUrl`) + a payment method on file (`409` with `details.reason: "payment_method_required"` otherwise).

## Request

<ParamField path="milestoneId" type="string" required>
  The milestone to fund. Must be `NOT_FUNDED`, not cancelled, on an active contract you own.
</ParamField>

## Response

Returns `202` — the request is recorded, nothing has been 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: "milestone_fund", status: "pending", contractId, milestoneId, jobId, proposalId: null, approvalUrl, expiresAt, resolvedAt, result, createdAt}`.
</ResponseField>

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

## Errors

| Status | `code`         | Meaning                                                                                                                                                       |
| ------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | `UNAUTHORIZED` | Missing or invalid token                                                                                                                                      |
| `403`  | `FORBIDDEN`    | Missing `payments:write` scope, `public_api_payments_write` disabled, or account not claimed (`details.reason: "account_claim_required"`, `details.claimUrl`) |
| `404`  | `NOT_FOUND`    | No such milestone, or its contract is on another account                                                                                                      |
| `409`  | `CONFLICT`     | See the reason catalog below                                                                                                                                  |

### `409` reason catalog

| `details.reason`          | Meaning                                                                 | Extra `details` fields |
| ------------------------- | ----------------------------------------------------------------------- | ---------------------- |
| `milestone_not_fundable`  | Milestone is not `NOT_FUNDED` (e.g. already funded)                     | `status`               |
| `payment_method_required` | No payment method on file; a human must add a card in the OpenTrain app | `billingUrl`           |
| `milestone_cancelled`     | Milestone has been cancelled                                            | —                      |
| `contract_ended`          | The contract has ended                                                  | `contractId`           |

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

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

  ```json MCP: opentrain_request_milestone_funding theme={null}
  {
    "milestoneId": "<MILESTONE_ID>"
  }
  ```
</RequestExample>

<ResponseExample>
  ```json 202 theme={null}
  {
    "approval": {
      "id": "<APPROVAL_ID>",
      "type": "milestone_fund",
      "status": "pending",
      "contractId": "<CONTRACT_ID>",
      "milestoneId": "<MILESTONE_ID>",
      "jobId": "<JOB_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": "Funding request recorded. A signed-in human must confirm this approval in the OpenTrain app before any money moves."
  }
  ```

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