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

# End Contract

> End a contract — direct when nothing is funded, co-signed by a human when funded escrow is at stake.

Ends a contract. The outcome depends on whether funded escrow is at stake:

* **No funded milestones** — the end executes immediately and returns `200` with `status: "ended"`.
* **Funded milestones exist** — ending releases or refunds money, so the call returns `202` with a pending [approval](/docs/developers/concepts/human-approvals) (`type: "contract_end"`). A signed-in human must open `approval.approvalUrl` and confirm before the contract ends. Approvals expire after \~72 hours. Track the outcome via [`GET /approvals/{id}`](/docs/developers/api-reference/approvals/get) or the `approval.confirmed` event on [`GET /updates`](/docs/developers/api-reference/updates/poll).

The request has no body. Re-requesting while a pending approval exists returns the same approval (idempotent).

**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 end.
</ParamField>

## Response — `200` (ended directly)

<ResponseField name="ok" type="boolean">
  `true`.
</ResponseField>

<ResponseField name="contractId" type="string">
  The ended contract's ID.
</ResponseField>

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

## Response — `202` (human co-sign required)

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

<ResponseField name="message" type="string">
  Explains that a signed-in human must confirm ending the contract in the OpenTrain app.
</ResponseField>

## Errors

| Status | `code`         | Meaning                                                                                                                                                       |
| ------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `BAD_REQUEST`  | Missing `contractId`                                                                                                                                          |
| `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`     | Contract has already ended (`details.reason: "contract_ended"`)                                                                                               |

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

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

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

<ResponseExample>
  ```json 200 (ended directly) theme={null}
  {
    "ok": true,
    "contractId": "<CONTRACT_ID>",
    "status": "ended"
  }
  ```

  ```json 202 (co-sign required) theme={null}
  {
    "approval": {
      "id": "<APPROVAL_ID>",
      "type": "contract_end",
      "status": "pending",
      "contractId": "<CONTRACT_ID>",
      "milestoneId": null,
      "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": "This contract has funded milestones, so a signed-in human must confirm ending it in the OpenTrain app."
  }
  ```
</ResponseExample>
