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

# Create Milestone

> Add an unfunded milestone to an active contract. No money moves at creation.

Adds a new milestone to an active contract. The milestone is created **unfunded** (`NOT_FUNDED`) — no money moves at creation. To put money behind it, [request funding](/docs/developers/api-reference/milestones/fund) afterwards; a signed-in human must confirm that step (see [human approvals](/docs/developers/concepts/human-approvals)).

The amount must fit the contract's payment model: fixed-price contracts require `amountUsd`; hourly and per-label contracts require `volume`, and when both `amountUsd` and `volume` are given the amount must equal the contract rate × volume.

**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`). The contract must be yours and still active.

## Request

<ParamField path="contractId" type="string" required>
  The contract to add the milestone to.
</ParamField>

<ParamField body="description" type="string" required>
  Description of the work to deliver. Must be non-empty.
</ParamField>

<ParamField body="name" type="string">
  Optional short milestone name.
</ParamField>

<ParamField body="amountUsd" type="number">
  Milestone amount in USD. Must be positive. Required for fixed-price contracts.
</ParamField>

<ParamField body="volume" type="number">
  Unit volume (e.g. label count or hours). Must be positive. Required for hourly and per-label contracts.
</ParamField>

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

## Response

Returns `201` with the created milestone.

<ResponseField name="milestone" type="object">
  <Expandable title="milestone" defaultOpen>
    <ResponseField name="id" type="string">
      Milestone ID — pass to [`POST /milestones/{id}/fund`](/docs/developers/api-reference/milestones/fund) to request escrow funding.
    </ResponseField>

    <ResponseField name="name" type="string | null">
      Milestone name.
    </ResponseField>

    <ResponseField name="description" type="string | null">
      Work to deliver.
    </ResponseField>

    <ResponseField name="status" type="string | null">
      `NOT_FUNDED` on creation.
    </ResponseField>

    <ResponseField name="amountUsd" type="number | null">
      Milestone amount in USD.
    </ResponseField>

    <ResponseField name="volume" type="number | null">
      Unit volume.
    </ResponseField>

    <ResponseField name="milestoneNumber" type="number | null">
      Position in the contract's milestone sequence.
    </ResponseField>

    <ResponseField name="dueDate" type="string | null">
      ISO due date.
    </ResponseField>

    <ResponseField name="pendingApproval" type="boolean">
      `false` on creation.
    </ResponseField>

    <ResponseField name="needsReview" type="boolean">
      `false` on creation.
    </ResponseField>

    <ResponseField name="invoiceId" type="string | null">
      `null` until funded.
    </ResponseField>

    <ResponseField name="createdAt" type="string">
      ISO creation timestamp.
    </ResponseField>
  </Expandable>
</ResponseField>

## Errors

| Status | `code`         | Meaning                                                                                                                                                                                                                                                          |
| ------ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `BAD_REQUEST`  | Invalid JSON, or field errors (`details.field` names the offender): `description` missing/empty, `amountUsd` required for fixed-price contracts, `volume` required for hourly and per-label contracts, or the amount does not match the contract rate and volume |
| `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 contract, or the contract is on another account                                                                                                                                                                                                          |
| `409`  | `CONFLICT`     | `details.reason: "contract_ended"` (contract already ended) or `"contract_rate_missing"` (contract has no rate to validate against)                                                                                                                              |

<RequestExample>
  ```bash curl 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": "Second labeling batch",
      "description": "Label the next 5,000 posts per the updated guidelines",
      "amountUsd": 300,
      "dueDate": "2026-07-15"
    }'
  ```

  ```bash CLI theme={null}
  opentrain milestones create --contract-id <CONTRACT_ID> \
    --description "Label the next 5,000 posts per the updated guidelines" \
    --name "Second labeling batch" --amount 300 --due-date 2026-07-15 --json
  ```

  ```json MCP: opentrain_create_milestone theme={null}
  {
    "contractId": "<CONTRACT_ID>",
    "name": "Second labeling batch",
    "description": "Label the next 5,000 posts per the updated guidelines",
    "amountUsd": 300,
    "dueDate": "2026-07-15"
  }
  ```
</RequestExample>

<ResponseExample>
  ```json 201 theme={null}
  {
    "milestone": {
      "id": "<MILESTONE_ID>",
      "name": "Second labeling batch",
      "description": "Label the next 5,000 posts per the updated guidelines",
      "status": "NOT_FUNDED",
      "amountUsd": 300,
      "volume": null,
      "milestoneNumber": 2,
      "dueDate": "2026-07-15T00:00:00.000Z",
      "pendingApproval": false,
      "needsReview": false,
      "invoiceId": null,
      "createdAt": "2026-06-12T10:00:00.000Z"
    }
  }
  ```

  ```json 409 (contract ended) theme={null}
  {
    "error": "Contract has ended",
    "code": "CONFLICT",
    "requestId": "<REQUEST_ID>",
    "details": {
      "contractId": "<CONTRACT_ID>",
      "reason": "contract_ended"
    }
  }
  ```
</ResponseExample>
