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 |
200. Contract end is the dual-mode case:
Anatomy of the 202 Response
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:

The Flow
Tracking an Approval
Two complementary mechanisms: Poll the approval directly:approval.confirmed event in /updates or a webhook — it fires when the approval resolves (whether confirmed, declined, or expired):
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 |
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, emitsapproval.confirmedwithstatus: "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.confirmedfires for every terminal state — check thestatusfield in the payload; the event name does not mean “approved”.- Co-sign applies even with credits on balance. A funded credit balance 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
Hire and Pay
The full hire → milestone → fund → approve walkthrough.
Credits and Billing
How hires are funded: balances, holds, and top-ups.