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

# Publish Job

> Publish a draft job to the marketplace.

Takes a `publishReady` draft live on the marketplace. The final step of the [posting loop](/docs/developers/guides/post-a-job): [create](/docs/developers/api-reference/job-drafts/create) → [gap-fill](/docs/developers/api-reference/job-drafts/update) → publish. Idempotent — publishing an already-open job returns `200` with `alreadyPublished: true`.

Every publish passes content moderation, and accounts have a daily publish limit (**20/day claimed, 3/day unclaimed**). A draft that is not publish-ready is rejected with the same validation detail the draft endpoints return — go back and patch the missing fields.

**Requirements:** `jobs:write` scope + the `public_api_job_publishing` feature (check [capabilities](/docs/developers/api-reference/job-drafts/capabilities) → `capabilities.publish`). Works pre-claim within the lower daily limit.

## Request

<ParamField path="id" type="string" required>
  The draft job ID.
</ParamField>

No body.

## Response

<ResponseField name="ok" type="boolean">
  `true` on success.
</ResponseField>

<ResponseField name="jobId" type="string">
  The job ID.
</ResponseField>

<ResponseField name="status" type="string">
  `OPEN` — the job is live.
</ResponseField>

<ResponseField name="alreadyPublished" type="boolean">
  `true` when the job was already open (no-op).
</ResponseField>

<ResponseField name="jobUrl" type="string">
  Public listing URL.
</ResponseField>

<ResponseField name="publishAttemptId" type="string | null">
  Internal attempt identifier for support/audit, when available.
</ResponseField>

<ResponseField name="warnings" type="string[]">
  Non-blocking warnings (e.g. fields worth improving).
</ResponseField>

<ResponseField name="sideEffects" type="object">
  `{dispatched, status}` — whether post-publish side effects (distribution, notifications) were kicked off.
</ResponseField>

## Errors

| Status | `code`         | Meaning                                                                                                                  |
| ------ | -------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `400`  | `BAD_REQUEST`  | Draft is not publish-ready — `details.validation` carries the same `missingFields`/`issues` shape as the draft endpoints |
| `401`  | `UNAUTHORIZED` | Missing or invalid token                                                                                                 |
| `403`  | `FORBIDDEN`    | Missing scope, feature disabled, or moderation block (`details.reason: "moderation_blocked"` with `details.reasons[]`)   |
| `404`  | `NOT_FOUND`    | No such job, or not yours                                                                                                |
| `429`  | `RATE_LIMITED` | Daily publish limit reached — `details` carries `{limit, windowHours: 24}`                                               |

<RequestExample>
  ```bash curl theme={null}
  curl -sS -X POST https://app.opentrain.ai/api/public/v1/jobs/<JOB_ID>/publish \
    -H "Authorization: Bearer $OT_API_TOKEN"
  ```

  ```bash CLI theme={null}
  opentrain jobs publish --job-id <JOB_ID> --json
  ```

  ```json MCP: opentrain_publish_job theme={null}
  {
    "jobId": "<JOB_ID>"
  }
  ```
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {
    "ok": true,
    "jobId": "<JOB_ID>",
    "status": "OPEN",
    "alreadyPublished": false,
    "jobUrl": "https://app.opentrain.ai/jobs/<JOB_ID>",
    "publishAttemptId": "<ATTEMPT_ID>",
    "warnings": [],
    "sideEffects": {
      "dispatched": true,
      "status": "queued"
    }
  }
  ```

  ```json 429 (daily limit) theme={null}
  {
    "error": "Daily publish limit reached",
    "code": "RATE_LIMITED",
    "requestId": "<REQUEST_ID>",
    "details": {
      "limit": 3,
      "windowHours": 24
    }
  }
  ```
</ResponseExample>
