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

# Invite Freelancer to Job

> Invite a specific AI trainer to submit a proposal to your job.

Invites a specific AI trainer to a published job of yours, creating an invited proposal they can respond to. Find candidates via [`GET /freelancers/{idOrSlug}`](/docs/developers/api-reference/freelancers/get) or from proposals on other jobs. Idempotent per freelancer: re-inviting returns `200` with `alreadyInvited: true` instead of creating a duplicate.

**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`). The job must be yours and published.

## Request

<ParamField path="id" type="string" required>
  Your published job's ID.
</ParamField>

<ParamField body="freelancerId" type="string" required>
  The AI trainer's user ID.
</ParamField>

## Response

`201` when a new invite was created, `200` when the freelancer was already invited.

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

<ResponseField name="proposalId" type="string">
  The invited proposal's ID — track it via [`GET /proposals/{id}`](/docs/developers/api-reference/proposals/get).
</ResponseField>

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

<ResponseField name="freelancerId" type="string">
  The invited AI trainer's user ID.
</ResponseField>

<ResponseField name="alreadyInvited" type="boolean">
  `true` when this invite already existed (idempotent repeat).
</ResponseField>

## Errors

| Status | `code`         | Meaning                                                                                                                                                |
| ------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400`  | `BAD_REQUEST`  | Missing or invalid `freelancerId` (zod details)                                                                                                        |
| `401`  | `UNAUTHORIZED` | Missing or invalid token                                                                                                                               |
| `403`  | `FORBIDDEN`    | Missing `proposals:write` scope, `public_api_hiring` disabled, or account not claimed (`details.reason: "account_claim_required"`, `details.claimUrl`) |
| `404`  | `NOT_FOUND`    | No such job (or not yours), or no such user                                                                                                            |
| `409`  | `CONFLICT`     | Invite not possible — `details.reason` is one of `job_not_published`, `not_a_freelancer`, `freelancer_unavailable`                                     |

<RequestExample>
  ```bash curl theme={null}
  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>"}'
  ```

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

  ```json MCP: opentrain_invite_freelancer theme={null}
  {
    "jobId": "<JOB_ID>",
    "freelancerId": "<FREELANCER_ID>"
  }
  ```
</RequestExample>

<ResponseExample>
  ```json 201 theme={null}
  {
    "ok": true,
    "proposalId": "<PROPOSAL_ID>",
    "jobId": "<JOB_ID>",
    "freelancerId": "<FREELANCER_ID>",
    "alreadyInvited": false
  }
  ```

  ```json 409 (job not published) theme={null}
  {
    "error": "Job is not published",
    "code": "CONFLICT",
    "requestId": "<REQUEST_ID>",
    "details": {
      "reason": "job_not_published"
    }
  }
  ```
</ResponseExample>
