Skip to content
OpenTrain AIOpenTrain AIOpenTrain AIDocs

Ask OpenTrain

Answers from the documentation, with sources.

What would you like to do with OpenTrain?

AI answers can be mistaken. Check the linked sources. Don’t include private account information.

Open app

DEVELOPER DOCUMENTATION

Hire From Proposal

Request a hire — records a pending approval a human must co-sign in the OpenTrain app before the contract is created and any money moves.

POST/api/public/v1/proposals/{proposalId}/hire

Requests a hire of the candidate behind one of your proposals. This call never hires anyone or moves money. It records a pending approval (type: "proposal_hire") and returns 202; a signed-in human must open approval.approvalUrl and confirm in the OpenTrain app. Approvals expire after ~72 hours.

When the human confirms, OpenTrain accepts the proposal, creates the contract, creates the first escrow milestone, and funds it — the milestone amount plus a 10% marketplace fee plus a $9.95 contract initiation fee (see credits & billing). The human chooses the payment source (card or credit balance) at confirm time. Funds are held, not paid out; payout happens when you approve the milestone later.

Re-requesting with the same milestone terms while a pending approval exists returns the same approval (idempotent). Re-requesting with different terms supersedes the old approval, so the human only ever sees one live hire request per proposal. Learn the outcome by polling GET /approvals/{id} or watching for the approval.confirmed event on GET /updates; once confirmed, the contract appears in GET /contracts with the post-hire job conversation. The AI trainer’s identity stays masked — first name + last initial, never an email (see privacy).

Requirements: proposals:write scope + the public_api_hiring feature + a claimed account (unclaimed accounts get 403 with details.reason: "account_claim_required" and a claimUrl) + a payment method on file or a credit balance covering the full charge (409 with details.reason: "payment_method_required" otherwise). The proposal must be on a job you own.

proposalIdstringpathrequired

The proposal to hire from.

milestoneobjectbodyrequired

The first escrow milestone for the new contract. At least one of name or description must be non-empty.

milestone
namestringbody

Short milestone name (max 500 chars).

descriptionstringbody

Description of the work to deliver (max 2,000 chars).

amountnumberbodyrequired

Milestone amount in USD. Must be positive. The escrow hold at confirm time covers this amount plus fees.

dueDatestringbody

Optional due date (ISO 8601).

confirmNotFitOverridebooleanbody

Set true to confirm hiring a proposal you previously marked Not a fit, after receiving a 409 with details.reason: "not_fit_confirmation_required".

Returns 202 — the request is recorded; nothing has been hired or charged yet.

approvalobject

The pending approval, in the same shape as GET /approvals/{id}: {id, type: "proposal_hire", status: "pending", contractId: null, milestoneId: null, jobId, proposalId, approvalUrl, expiresAt, resolvedAt, result, createdAt}. contractId stays null until the human confirms and the contract is created.

messagestring

Explains that a signed-in human must confirm the approval before the freelancer is hired and any money moves.

StatuscodeMeaning
400BAD_REQUESTEmpty body, invalid JSON, or invalid milestone (details.issues carries zod details — e.g. missing amount, or neither name nor description provided)
401UNAUTHORIZEDMissing or invalid token
403FORBIDDENMissing proposals:write scope, public_api_hiring disabled, account not claimed (details.reason: "account_claim_required", details.claimUrl), or the proposal is on another account’s job
404NOT_FOUNDNo such proposal
409CONFLICTHire request blocked — see the details.reason catalog below
details.reasonMeaningExtra details fields
payment_method_requiredNo payment method on file and the credit balance doesn’t cover the full charge; a human must add a card or credits in the OpenTrain appbillingUrl
already_acceptedThe proposal was already hired
not_fit_confirmation_requiredProposal is marked Not a fit; retry with confirmNotFitOverride: true