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

# Human Approvals (Co-Sign)

> How money-moving API actions pause for human confirmation, and how to track approvals to completion.

Agents operate; humans authorize spend. Any API action that would move money does not execute immediately — it returns `202 Accepted` with a pending **approval**, and a signed-in human confirms it in the OpenTrain app before anything happens. **Money never moves from an API call alone.**

## Which Actions Are Co-Signed

| Action                                           | Endpoint                                 | Approval `type`     |
| ------------------------------------------------ | ---------------------------------------- | ------------------- |
| Hire from a proposal (creates contract + escrow) | `POST /proposals/{proposalId}/hire`      | `proposal_hire`     |
| Fund a milestone (escrow)                        | `POST /milestones/{milestoneId}/fund`    | `milestone_fund`    |
| Approve a milestone (release payout)             | `POST /milestones/{milestoneId}/approve` | `milestone_approve` |
| End a contract that has funded milestones        | `POST /contracts/{id}/end`               | `contract_end`      |

Non-money writes — creating an *unfunded* milestone, or ending a contract with **no** funded milestones — execute directly with a `200`. Contract end is the dual-mode case:

```json theme={null}
// No funded milestones → direct
{ "ok": true, "contractId": "...", "status": "ended" }
```

```json theme={null}
// Funded milestones exist → co-signed (202)
{
  "approval": { "type": "contract_end", "...": "..." },
  "message": "This contract has funded milestones, so a signed-in human must confirm ending it in the OpenTrain app."
}
```

## Anatomy of the 202 Response

```json theme={null}
{
  "approval": {
    "id": "<APPROVAL_ID>",
    "type": "milestone_fund",
    "status": "pending",
    "contractId": "...",
    "milestoneId": "...",
    "jobId": "...",
    "proposalId": null,
    "approvalUrl": "https://app.opentrain.ai/approvals/<APPROVAL_ID>",
    "expiresAt": "...",
    "resolvedAt": null,
    "result": null,
    "createdAt": "..."
  },
  "message": "A human must confirm this request before any money moves. Share the approvalUrl."
}
```

Your job after a `202`: **surface the `approvalUrl` to your human** — in chat, a notification, wherever they'll see it. They open it, review the amount and context, and confirm or decline.

Opening `/approvals/{approvalId}` for a pending `milestone_fund` drops the human into the normal contract view with the funding modal already open and pre-filled — the same interface they use for any other milestone, plus a banner noting the agent requested it:

<Frame caption="The approval link opens the regular contract view with the funding modal pre-opened — milestone name, amount, and AI trainer exactly as the API client requested, with a banner to confirm or decline the agent's request.">
  <img src="https://mintcdn.com/opentrainai/top-Uy3T2gAVcCjx/images/screenshots/developers/approval-confirm-milestone-fund-desktop.png?fit=max&auto=format&n=top-Uy3T2gAVcCjx&q=85&s=be4c5321727a82a5cc3469e6331f195d" alt="OpenTrain employer dashboard with the Fund and Activate Milestone modal open over the contract view. A banner reads Your agent requested this action, confirm below or decline, with a Decline button. The modal shows the AI trainer Marcus O., the milestone name Production batch 2, a due date, a description, and a button reading Deposit 450 dollars and Activate Milestone." width="1280" height="800" data-path="images/screenshots/developers/approval-confirm-milestone-fund-desktop.png" />
</Frame>

## The Flow

```mermaid theme={null}
sequenceDiagram
    participant A as Agent
    participant API as OpenTrain API
    participant H as Human owner

    A->>API: POST .../milestones/{id}/fund
    API-->>A: 202 {approval: {approvalUrl, expiresAt}}
    A->>H: Share approvalUrl
    H->>API: Opens /approvals/{id}, signs in, confirms
    API->>API: Executes the action (funds escrow)
    API-->>A: approval.confirmed event (via /updates or webhook)
    A->>API: GET /approvals/{id}
    API-->>A: status: confirmed, result: {invoiceId, paymentIntentId}
```

## Tracking an Approval

Two complementary mechanisms:

**Poll the approval directly:**

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

**Watch for the `approval.confirmed` event** in [`/updates` or a webhook](/docs/developers/guides/stay-in-sync) — it fires when the approval resolves (whether confirmed, declined, or expired):

```json theme={null}
{
  "approvalId": "...",
  "approvalType": "MILESTONE_FUND",
  "status": "confirmed",
  "contractId": "...",
  "milestoneId": "...",
  "jobId": "..."
}
```

### Status Lifecycle

| `status`    | Meaning                                                     |
| ----------- | ----------------------------------------------------------- |
| `pending`   | Waiting for the human                                       |
| `confirmed` | Human approved — the action **has executed**; see `result`  |
| `declined`  | Human rejected it — the action did **not** execute          |
| `expired`   | Nobody acted within the window — the action did not execute |

On confirmation, `result` carries the execution evidence:

| Type                | `result`                                                                            |
| ------------------- | ----------------------------------------------------------------------------------- |
| `proposal_hire`     | `{ "hired": true, "contractId": "...", "jobId": "...", "freelancerUserId": "..." }` |
| `milestone_fund`    | `{ "invoiceId": "...", "paymentIntentId": "..." }`                                  |
| `milestone_approve` | `{ "invoiceId": "...", "payoutTransactionId": "..." }`                              |
| `contract_end`      | `{ "contractEnded": true }`                                                         |

## Rules That Matter in Practice

* **Approvals expire after \~72 hours.** An expired approval never executes; create a new request if the action is still wanted.
* **A decline is final for that approval.** It resolves to `declined`, emits `approval.confirmed` with `status: "declined"`, and nothing executes. Talk to your human before re-requesting.
* **Re-requesting is safe and idempotent.** Posting the same fund/approve/end/hire request while an approval is pending returns the **same** approval — no duplicates pile up for the human, and no money moves until a confirm. A hire re-requested with *different* milestone terms supersedes the old approval so only one live hire request exists per proposal.
* **`approval.confirmed` fires for every terminal state** — check the `status` field in the payload; the event name does not mean "approved".
* **Co-sign applies even with credits on balance.** A funded [credit balance](/docs/developers/concepts/credits-and-billing) changes *how* a hire or milestone is funded, not *whether* a human confirms it. The human picks the payment source — card or credit balance — on the confirmation screen.

## Why It Works This Way

Unattended agents shouldn't be able to spend their owner's money on their own — a bug, a prompt injection, or a misread requirement could be expensive. The co-sign pattern keeps the agent in the driver's seat for everything operational (drafting, publishing, evaluating candidates, preparing the hire) while reserving the irreversible steps — hiring a person and money leaving the account — for an authenticated human with full context on one screen.

## Related

<CardGroup cols={2}>
  <Card title="Hire and Pay" href="/docs/developers/guides/hire-and-pay" icon="briefcase">
    The full hire → milestone → fund → approve walkthrough.
  </Card>

  <Card title="Credits and Billing" href="/docs/developers/concepts/credits-and-billing" icon="credit-card">
    How hires are funded: balances, holds, and top-ups.
  </Card>
</CardGroup>
