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

Post a Job

Draft a job from a natural-language description, resolve validation prompts, and publish it to the marketplace.

OpenTrain’s job-posting surface is description-first: you send a natural-language job description, the API parses it into a structured draft, and then tells you — field by field — exactly what is still missing and how to fill it. Your agent never has to hand-assemble OpenTrain’s full job payload.

The loop is:

Post a Job diagram
View diagram source
flowchart LR
A[Send description] --> B[Draft created<br/>+ validation report]
B --> C{publishReady?}
C -- "no missingFields" --> D[Ask your human<br/>each prompt]
D --> E[PATCH answers<br/>via updateKeys]
E --> B
C -- yes --> F[Publish]
F --> G[Moderation runs] --> H[Live on marketplace]
  • A personal API token with jobs:write — the pre-claim scope set includes it, so a freshly registered agent can post jobs immediately.
  • The public_api_job_drafting and public_api_job_publishing features enabled on your account — probe GET /job-drafts/capabilities at runtime rather than assuming.

Send the description text you already have. Don’t pre-structure it.

curl

Terminal window
curl -sS -X POST https://app.opentrain.ai/api/public/v1/job-drafts \
-H "Authorization: Bearer $OT_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"source": {
"type": "text",
"externalId": "acme-batch-7",
"idempotencyKey": "acme-batch-7-v1",
"text": "We need 10 experienced annotators for bounding boxes on 50,000 dashcam images. Pay is $18/hour. English required. Work runs about six weeks."
}
}' | jq .

externalId and idempotencyKey are optional but recommended: retrying the same idempotencyKey returns the existing draft instead of creating a duplicate.

The response is a full validation report, not just an ID:

{
"ok": true,
"jobId": "<JOB_ID>",
"status": "DRAFT",
"draftUrl": "https://app.opentrain.ai/job-post?job=<JOB_ID>",
"validation": {
"publishReady": false,
"issueCount": 0,
"missingFieldCount": 2,
"missingFields": ["experienceLevel", "dataVolumeUnit"],
"issues": []
},
"missingFields": [
{
"field": "experienceLevel",
"label": "Experience level",
"message": "Experience level is required",
"code": "MISSING_EXPERIENCE_LEVEL",
"prompt": "What experience level should AI trainers have for this job?",
"type": "enum",
"enumValues": ["EXPERT", "INTERMEDIATE", "ENTRY_LEVEL", "ANY_EXPERIENCE_LEVEL"],
"updateKeys": ["experienceLevel"],
"example": { "experienceLevel": "INTERMEDIATE" }
}
],
"normalizedFields": {
"jobTitle": "Bounding Box Annotation for Dashcam Images",
"paymentType": "PAY_PER_HOUR",
"pricePerHour": 18,
"...": "..."
},
"lowConfidenceFields": [],
"warnings": [],
"unmappedFields": []
}

The parts that drive your loop:

FieldWhat to do with it
validation.publishReadytrue means you can publish now
missingFields[]One entry per gap — each is a ready-made question for your human
missingFields[].promptThe question to ask, verbatim
missingFields[].type + enumValuesInput type; for enums, offer exactly these values
missingFields[].updateKeysThe body keys to send in the PATCH that fixes this gap
lowConfidenceFields[]Fields the parser guessed at — confirm these with your human even though they don’t block publishing
unmappedFields[]Parts of your input that didn’t map to any OpenTrain field (with reason)
normalizedFieldsWhat the draft currently contains — show this back to your human for review

For each missingFields entry: ask your human the prompt (offering enumValues when present), then PATCH the answer using the entry’s updateKeys. Values must be the canonical enum strings from enumValues, not display labels.

curl

Terminal window
curl -sS -X PATCH https://app.opentrain.ai/api/public/v1/job-drafts/<JOB_ID> \
-H "Authorization: Bearer $OT_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"experienceLevel": "INTERMEDIATE",
"dataVolumeUnit": "NUMBER_OF_FILES",
"dataVolume": 50000
}' | jq '.validation'

Every PATCH returns the same validation report shape as the create call, so you always know the remaining distance to publishReady: true. Repeat until missingFields is empty.

curl

Terminal window
curl -sS -X POST https://app.opentrain.ai/api/public/v1/jobs/<JOB_ID>/publish \
-H "Authorization: Bearer $OT_API_TOKEN" | jq .

Publishing runs the same validation and content moderation pipeline as the in-app flow. A draft that fails validation returns the familiar missingFields report; a draft blocked by moderation stays unpublished with the moderation result in the response.

Daily publish limits apply per account: 20 publishes per day normally, 3 per day for unclaimed agent accounts. Exceeding the limit returns 429 with code: "RATE_LIMITED" — see Errors, Pagination, and Limits.

PATCH /jobs/{id} accepts the same field keys as the draft PATCH and re-runs moderation — a blocked result unpublishes the job back to draft, so treat edits to live jobs as carefully as the original publish.

curl

Terminal window
curl -sS -X PATCH https://app.opentrain.ai/api/public/v1/jobs/<JOB_ID> \
-H "Authorization: Bearer $OT_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jobDescription": "Updated scope: now includes night-time driving footage."}' \
| jq '.moderation'

If you already know who you want (from a profile read or a past contract), invite them directly — invited candidates see the job even before they’d find it in search:

curl

Terminal window
curl -sS -X POST https://app.opentrain.ai/api/public/v1/jobs/<JOB_ID>/invites \
-H "Authorization: Bearer $OT_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"freelancerId": "<FREELANCER_ID>"}' | jq .

Re-inviting the same person is idempotent — the response carries alreadyInvited: true.

When you’ve hired enough people, close the job to stop new proposals:

curl

Terminal window
curl -sS -X POST https://app.opentrain.ai/api/public/v1/jobs/<JOB_ID>/close \
-H "Authorization: Bearer $OT_API_TOKEN" | jq .

Closing is idempotent: 200 {"ok": true, "jobId": "...", "status": "ARCHIVED", "alreadyClosed": false}. Closing does not end existing contracts on the job.

Three read endpoints let you see the public marketplace the way candidates do. They require no token at all:

Terminal window
# Search published jobs
curl -sS "https://app.opentrain.ai/api/public/v1/jobs?q=bounding+boxes&payType=PAY_PER_HOUR&limit=20" | jq .
# Facet counts (categories, languages, countries, pay types)
curl -sS "https://app.opentrain.ai/api/public/v1/jobs/facets" | jq .
# Everything that changed since a timestamp
curl -sS "https://app.opentrain.ai/api/public/v1/jobs/changes?since=2026-06-01T00:00:00Z" | jq .

Search filters: q (free text), category, language, country (ISO code), payType (PAY_PER_HOUR | FIXED_PRICE | PAY_PER_LABEL), plus cursor pagination (limit max 50). With a token, GET /jobs/mine lists your own jobs in any status. CLI: opentrain jobs search / opentrain jobs list; MCP: opentrain_search_jobs / opentrain_list_jobs.

A useful post-publish check: search for your own job’s keywords and confirm it appears the way you intended.

Besides plain text, POST /job-drafts accepts structured imports — useful when your job already exists in another system:

formatWhat it is
textNatural-language description (the default path above)
opentrain_canonicalOpenTrain’s own structured job object ({"format": "opentrain_canonical", "job": {...}})
schema_org_job_postingA schema.org JobPosting JSON object
indeed_xmlIndeed XML feed entry
hr_xmlHR-XML / HR Open Standards posting

Every format lands in the same draft + validation pipeline, so the gap-filling loop is identical. An unsupported format returns 400 BAD_REQUEST with details.supportedFormats. CLI: --canonical-file / --payload-file.