Skip to main content
POST
/
api
/
public
/
v1
/
milestones
/
{milestoneId}
/
approve
curl -sS -X POST https://app.opentrain.ai/api/public/v1/milestones/<MILESTONE_ID>/approve \
  -H "Authorization: Bearer $OT_API_TOKEN"
{
  "approval": {
    "id": "<APPROVAL_ID>",
    "type": "milestone_approve",
    "status": "pending",
    "contractId": "<CONTRACT_ID>",
    "milestoneId": "<MILESTONE_ID>",
    "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": "Release request recorded. A signed-in human must confirm this approval in the OpenTrain app before any money moves."
}
Requests payment release for an ACTIVE_FUNDED milestone — typically after the AI trainer has delivered the work. This call never releases money. It records a pending approval (type: "milestone_approve") and returns 202; a signed-in human must open approval.approvalUrl and confirm in the OpenTrain app before the escrowed funds pay out. Approvals expire after ~72 hours. Re-requesting while a pending approval exists returns the same approval (idempotent). Learn the outcome by polling GET /approvals/{id} or watching for the approval.confirmed event on GET /updates. The request has no body. 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 milestone must be ACTIVE_FUNDEDfund it first.

Request

milestoneId
string
required
The milestone to release payment for. Must be ACTIVE_FUNDED, not cancelled, on an active contract you own.

Response

Returns 202 — the request is recorded, nothing has been released yet.
approval
object
The pending approval, in the same shape as GET /approvals/{id}: {id, type: "milestone_approve", status: "pending", contractId, milestoneId, jobId, proposalId: null, approvalUrl, expiresAt, resolvedAt, result, createdAt}.
message
string
Explains that a signed-in human must confirm the approval before any money moves.

Errors

StatuscodeMeaning
401UNAUTHORIZEDMissing or invalid token
403FORBIDDENMissing payments:write scope, public_api_payments_write disabled, or account not claimed (details.reason: "account_claim_required", details.claimUrl)
404NOT_FOUNDNo such milestone, or its contract is on another account
409CONFLICTSee the reason catalog below

409 reason catalog

details.reasonMeaningExtra details fields
milestone_not_fundedMilestone is NOT_FUNDED — fund it before requesting releasestatus
milestone_not_releasableMilestone is in some other non-releasable state (e.g. already completed)status
milestone_cancelledMilestone has been cancelled
contract_endedThe contract has endedcontractId
curl -sS -X POST https://app.opentrain.ai/api/public/v1/milestones/<MILESTONE_ID>/approve \
  -H "Authorization: Bearer $OT_API_TOKEN"
{
  "approval": {
    "id": "<APPROVAL_ID>",
    "type": "milestone_approve",
    "status": "pending",
    "contractId": "<CONTRACT_ID>",
    "milestoneId": "<MILESTONE_ID>",
    "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": "Release request recorded. A signed-in human must confirm this approval in the OpenTrain app before any money moves."
}