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

# Get Contract

> Read one contract in detail: status, milestone timeline, the hired AI trainer, budget state, and the post-hire conversation ID.

Reads one contract in full, in the same shape as the entries on [`GET /contracts`](/docs/developers/api-reference/contracts/list) plus two detail-only fields: `jobDmConversationId` — the post-hire 1:1 thread with the hired AI trainer, usable with [`GET /messages`](/docs/developers/api-reference/messages/list) and [`POST /messages`](/docs/developers/api-reference/messages/send) — and `budget`, the funded-vs-consumed budget snapshot.

Unknown contract IDs and contracts on other accounts both return `404`, so contract IDs cannot be probed.

**Requirements:** `payments:read` scope. Works pre-claim.

## Request

<ParamField path="contractId" type="string" required>
  The contract to read.
</ParamField>

## Response

<ResponseField name="contract" type="object">
  <Expandable title="contract" defaultOpen>
    <ResponseField name="id" type="string">
      Contract ID.
    </ResponseField>

    <ResponseField name="status" type="string">
      `active` or `ended`.
    </ResponseField>

    <ResponseField name="title" type="string | null">
      Contract display title.
    </ResponseField>

    <ResponseField name="jobId" type="string | null">
      The job this contract belongs to.
    </ResponseField>

    <ResponseField name="proposalId" type="string | null">
      The proposal the hire came from.
    </ResponseField>

    <ResponseField name="paymentType" type="string | null">
      `FIXED_PRICE`, `PAY_PER_HOUR`, or `PAY_PER_LABEL`.
    </ResponseField>

    <ResponseField name="rateUsd" type="number | null">
      Contract rate in USD.
    </ResponseField>

    <ResponseField name="estimatedTotalUsd" type="number | null">
      Estimated total contract value in USD.
    </ResponseField>

    <ResponseField name="estimatedVolume" type="number | null">
      Estimated unit volume for per-unit contracts.
    </ResponseField>

    <ResponseField name="hasActiveMilestone" type="boolean">
      `true` when a funded milestone is in progress.
    </ResponseField>

    <ResponseField name="startDate" type="string | null">
      ISO contract start timestamp.
    </ResponseField>

    <ResponseField name="endDate" type="string | null">
      ISO contract end timestamp, `null` while active.
    </ResponseField>

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

    <ResponseField name="updatedAt" type="string">
      ISO last-change timestamp.
    </ResponseField>

    <ResponseField name="freelancer" type="object">
      The hired AI trainer: `{userId, displayName, country, profilePath}`. `displayName` is masked to first name + last initial (`"Maria G."`) — full last names and emails never appear (see [privacy](/docs/developers/concepts/privacy-and-work-email)).
    </ResponseField>

    <ResponseField name="milestones" type="object[]">
      Milestone timeline — same entry shape as on [`GET /contracts`](/docs/developers/api-reference/contracts/list): `{id, name, description, status, amountUsd, volume, milestoneNumber, dueDate, pendingApproval, needsReview, invoiceId, createdAt}`.
    </ResponseField>

    <ResponseField name="jobDmConversationId" type="string | null">
      The post-hire 1:1 conversation with the hired AI trainer. Pass to [`GET /messages`](/docs/developers/api-reference/messages/list) to read it or [`POST /messages`](/docs/developers/api-reference/messages/send) to message them.
    </ResponseField>

    <ResponseField name="budget" type="object | null">
      Funded milestones vs work consumed: `{contractId, paymentType, state, fundedVolume, fundedAmountUsd, consumed: {seconds, hours, labels, tasks}, consumedVolume, remainingVolume, consumedFraction, activeMilestone, lastUsageAt}`. `state` is `OK`, `LOW` (≥ 80% of funded volume consumed), or `DEPLETED` (≥ 100%); volume is hours for `PAY_PER_HOUR`, labels for `PAY_PER_LABEL`; `FIXED_PRICE` contracts track progress only and always report `OK`. State changes emit the [`contract.budget_state_changed`](/docs/developers/api-reference/updates/poll) event — when it lands on `LOW` or `DEPLETED`, the usual move is to [propose funding the next milestone](/docs/developers/api-reference/milestones/fund). `null` when the budget cannot be computed. Only this detail endpoint returns it, not the list.
    </ResponseField>
  </Expandable>
</ResponseField>

## Errors

| Status | `code`         | Meaning                                                                                                  |
| ------ | -------------- | -------------------------------------------------------------------------------------------------------- |
| `401`  | `UNAUTHORIZED` | Missing or invalid token                                                                                 |
| `403`  | `FORBIDDEN`    | Missing `payments:read` scope                                                                            |
| `404`  | `NOT_FOUND`    | No such contract, or the contract is on another account (`details: {resource: "contracts", contractId}`) |

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

  ```bash CLI theme={null}
  opentrain contracts get --contract-id <CONTRACT_ID> --json
  ```

  ```json MCP: opentrain_get_contract theme={null}
  {
    "contractId": "<CONTRACT_ID>"
  }
  ```
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {
    "contract": {
      "id": "<CONTRACT_ID>",
      "status": "active",
      "title": "Spanish Sentiment Labeling Contract",
      "jobId": "<JOB_ID>",
      "proposalId": "<PROPOSAL_ID>",
      "paymentType": "FIXED_PRICE",
      "rateUsd": 300,
      "estimatedTotalUsd": 300,
      "estimatedVolume": null,
      "hasActiveMilestone": true,
      "startDate": "2026-06-12T09:30:00.000Z",
      "endDate": null,
      "createdAt": "2026-06-12T09:30:00.000Z",
      "updatedAt": "2026-06-12T09:30:00.000Z",
      "freelancer": {
        "userId": "<FREELANCER_ID>",
        "displayName": "Maria G.",
        "country": "Spain",
        "profilePath": "/profile/maria-g"
      },
      "milestones": [
        {
          "id": "<MILESTONE_ID>",
          "name": "First labeling batch",
          "description": "Label the first 5,000 posts per the guidelines",
          "status": "ACTIVE_FUNDED",
          "amountUsd": 300,
          "volume": null,
          "milestoneNumber": 1,
          "dueDate": "2026-07-01T00:00:00.000Z",
          "pendingApproval": false,
          "needsReview": false,
          "invoiceId": "<INVOICE_ID>",
          "createdAt": "2026-06-12T09:30:00.000Z"
        }
      ],
      "jobDmConversationId": "<CONVERSATION_ID>",
      "budget": {
        "contractId": "<CONTRACT_ID>",
        "paymentType": "FIXED_PRICE",
        "state": "OK",
        "fundedVolume": 0,
        "fundedAmountUsd": 300,
        "consumed": { "seconds": 0, "hours": 0, "labels": 0, "tasks": 0 },
        "consumedVolume": 0,
        "remainingVolume": 0,
        "consumedFraction": 0,
        "activeMilestone": {
          "id": "<MILESTONE_ID>",
          "name": "First labeling batch",
          "amountUsd": 300,
          "volume": null,
          "status": "ACTIVE_FUNDED"
        },
        "lastUsageAt": null
      }
    }
  }
  ```
</ResponseExample>
