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

# Create Project Link

> Link one of your platform's projects to an OpenTrain job. externalProjectId must be unique per install; emits project_link.created.

Creates a [project link](/docs/developers/annotation-platforms/project-links) mapping one of your projects to an OpenTrain job. Contract [lifecycle events](/docs/developers/annotation-platforms/lifecycle-events) are emitted only for linked jobs, so this is the routing step of the integration. Emits a `project_link.created` event.

**Requirements:** `project-links:write` scope.

## Request

<ParamField body="externalProjectId" type="string" required>
  Your project's identifier (max 200 chars). **Unique per install** — reusing one returns `409`.
</ParamField>

<ParamField body="externalProjectName" type="string">
  Human-readable project name (max 300 chars).
</ParamField>

<ParamField body="externalProjectUrl" type="string">
  Deep link to the project in your platform (max 2048 chars).
</ParamField>

<ParamField body="jobId" type="string">
  The OpenTrain job to link. Must be a job owned by the installing employer — anything else returns `404`. A link without a job receives no contract events until it points at one.
</ParamField>

<ParamField body="provisioningMode" type="string" default="PARTNER_WEBHOOK">
  `PARTNER_WEBHOOK` (you provision workspace members from webhook events — recommended) or `MANAGED_ADAPTER` (the OpenTrain managed-credential path). See [provisioning modes](/docs/developers/annotation-platforms/overview#provisioning-modes).
</ParamField>

## Response

<ResponseField name="projectLink" type="object">
  The created link: `{id, jobId, externalProjectId, externalProjectName, externalProjectUrl, provisioningMode, createdAt}`.
</ResponseField>

## Errors

| Status | `code`         | Meaning                                                                                |
| ------ | -------------- | -------------------------------------------------------------------------------------- |
| `400`  | `BAD_REQUEST`  | Body not valid JSON, `externalProjectId` missing, or a field exceeds its length limit  |
| `401`  | `UNAUTHORIZED` | Missing, invalid, or revoked token                                                     |
| `403`  | `FORBIDDEN`    | Token lacks `project-links:write`                                                      |
| `404`  | `NOT_FOUND`    | `jobId` does not reference a job owned by the installing employer                      |
| `409`  | `CONFLICT`     | Duplicate `externalProjectId` for this install, or the install's link limit is reached |

<RequestExample>
  ```bash curl theme={null}
  curl -sS -X POST https://app.opentrain.ai/api/partner/v1/project-links \
    -H "Authorization: Bearer $OT_PARTNER_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "externalProjectId": "42",
      "externalProjectName": "Traffic signs batch 3",
      "externalProjectUrl": "https://your-platform.example.com/projects/42",
      "jobId": "<OPENTRAIN_JOB_ID>",
      "provisioningMode": "PARTNER_WEBHOOK"
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json 201 theme={null}
  {
    "projectLink": {
      "id": "<LINK_ID>",
      "jobId": "<OPENTRAIN_JOB_ID>",
      "externalProjectId": "42",
      "externalProjectName": "Traffic signs batch 3",
      "externalProjectUrl": "https://your-platform.example.com/projects/42",
      "provisioningMode": "PARTNER_WEBHOOK",
      "createdAt": "2026-06-12T10:00:00.000Z"
    }
  }
  ```

  ```json 409 (duplicate) theme={null}
  {
    "error": "A project link with this externalProjectId already exists for this install.",
    "code": "CONFLICT",
    "requestId": "<REQUEST_ID>"
  }
  ```
</ResponseExample>
