> ## Documentation Index
> Fetch the complete documentation index at: https://docs.open.cx/llms.txt
> Use this file to discover all available pages before exploring further.

# API Reference

> All Partner API requests require a Bearer token in the Authorization header. Partner API keys are provisioned by the OpenCX team — they are separate from org-level API keys.

## Authentication

All Partner API requests require a **Bearer token** in the `Authorization` header. Partner API keys are provisioned by the OpenCX team — they are separate from org-level API keys.

```bash theme={"dark"}
Authorization: Bearer YOUR_PARTNER_API_KEY
```

**Base URL:** `https://api.open.cx`

### IP allow list

Partner API endpoints support **IP-level access control**. When an allow list is configured on your partner account, requests from IPs not in the list are rejected with `403 Forbidden` — even with a valid API key.

All traffic passes through Cloudflare, which forwards the real client IP in the `X-Forwarded-For` header. The backend extracts and validates this IP on every request.

<Note>
  IP allow lists are configured by the OpenCX team during onboarding. Contact us to add, update, or remove IPs. When no allow list is set, requests are accepted from any IP (token-only authentication).
</Note>

### Security layers

| Layer                    | What it does                                                                              |
| ------------------------ | ----------------------------------------------------------------------------------------- |
| **Bearer token**         | Every request must include a valid partner API JWT. Revoked or missing keys return `401`. |
| **IP allow list**        | When configured, only requests from whitelisted IPs are accepted. Others get `403`.       |
| **Partner active check** | Inactive partner accounts are blocked at the auth layer (`401`).                          |
| **Rate limiting**        | 100 req/min per partner globally, plus per-endpoint limits.                               |
| **Org ownership**        | All org-scoped endpoints verify the org belongs to the calling partner (`403` otherwise). |

***

## Create Org

Create a new organization for one of your customers.

```
POST /partner/v1/orgs
```

### Request body

| Field             | Type   | Required | Description                                                                                                                                      |
| ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name`            | string | Yes      | Display name for the org.                                                                                                                        |
| `external_id`     | string | No       | Your internal ID for this customer. Must be unique per partner — duplicates return `409`.                                                        |
| `website`         | string | No       | Customer's website URL.                                                                                                                          |
| `language`        | string | No       | Default language code (e.g. `"en"`, `"es"`, `"de"`). Defaults to `"en"`.                                                                         |
| `ai_instructions` | string | No       | The AI profile — system prompt that defines the agent's personality, knowledge scope, and behavior. Overrides your partner-level default if set. |
| `integrations`    | object | No       | Auto-connect integrations during creation. See below.                                                                                            |

### Integrations

Pass credentials in the `integrations` object to auto-connect supported third-party integrations during org creation. Credentials are validated against the live third-party API — invalid credentials cause the request to fail with a `400`.

Each integration has its own shape under its own key. Some integrations accept either a **single object** (for one connection) or an **array** (for multiple connections — different locations, brands, or currencies). When passing an array, each entry should include a `name` that the AI uses in conversations to disambiguate between connections.

#### FareHarbor

Key: `fareharbor`

| Field                       | Type   | Required | Description                                                                                                      |
| --------------------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------- |
| `app_key`                   | string | Yes      | FareHarbor API **App Key**.                                                                                      |
| `user_key`                  | string | Yes      | FareHarbor API **User Key**.                                                                                     |
| `name`                      | string | No       | Label for this connection, used by the AI to disambiguate between multiple connections. Defaults to `"Default"`. |
| `default_company_shortname` | string | No       | The FareHarbor company shortname to use by default when the org has access to multiple companies.                |

Single connection (object form):

```json theme={"dark"}
{
  "integrations": {
    "fareharbor": {
      "name": "Primary",
      "app_key": "...",
      "user_key": "...",
      "default_company_shortname": "acme-tours"
    }
  }
}
```

Multiple connections (array form):

```json theme={"dark"}
{
  "integrations": {
    "fareharbor": [
      { "name": "Account A", "app_key": "...", "user_key": "...", "default_company_shortname": "acme-tours" },
      { "name": "Account B", "app_key": "...", "user_key": "...", "default_company_shortname": "acme-adventures" }
    ]
  }
}
```

<Note>
  Supported integration keys and their per-integration field shapes are documented above. This section will expand as more integrations become available via the provisioning API. For integrations not yet supported here, use the org-level dashboard or API after creation.
</Note>

### Response

```json 201 Created theme={"dark"}
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "name": "Acme Tours",
  "widget_token": "f8e7d6c5b4a39281...",
  "external_id": "customer-12345"
}
```

| Field          | Type           | Description                                                     |
| -------------- | -------------- | --------------------------------------------------------------- |
| `id`           | string         | The org UUID. Use this to manage the org via other OpenCX APIs. |
| `name`         | string         | The org name as provided.                                       |
| `widget_token` | string         | Widget embed token. Pass this to the chat widget.               |
| `external_id`  | string \| null | Your external ID, echoed back.                                  |

### Error responses

| Status | Condition                                                           | Body                                                                              |
| ------ | ------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `400`  | Invalid request body, or integration credentials failed validation. | `{ "statusCode": 400, "message": "..." }`                                         |
| `401`  | Missing, invalid, or revoked API key.                               | `{ "statusCode": 401, "message": "..." }`                                         |
| `409`  | An org with this `external_id` already exists for your partner.     | `{ "statusCode": 409, "message": "Org with external_id \"...\" already exists" }` |

### Examples

<CodeGroup>
  ```bash cURL theme={"dark"}
  curl -X POST https://api.open.cx/partner/v1/orgs \
    -H "Authorization: Bearer YOUR_PARTNER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "Acme Tours",
      "external_id": "customer-12345",
      "language": "en",
      "ai_instructions": "You are a support agent for Acme Tours. Help customers with bookings and questions."
    }'
  ```

  ```javascript Node.js theme={"dark"}
  const response = await fetch('https://api.open.cx/partner/v1/orgs', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer YOUR_PARTNER_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      name: 'Acme Tours',
      external_id: 'customer-12345',
      language: 'en',
      ai_instructions: 'You are a support agent for Acme Tours. Help customers with bookings and questions.',
    }),
  });

  const org = await response.json();
  ```

  ```python Python theme={"dark"}
  import requests

  response = requests.post(
      'https://api.open.cx/partner/v1/orgs',
      headers={'Authorization': 'Bearer YOUR_PARTNER_API_KEY'},
      json={
          'name': 'Acme Tours',
          'external_id': 'customer-12345',
          'language': 'en',
          'ai_instructions': 'You are a support agent for Acme Tours. Help customers with bookings and questions.',
      },
  )

  org = response.json()
  ```
</CodeGroup>

***

## List Orgs

List all organizations created by your partner account.

```
GET /partner/v1/orgs
```

### Query parameters

| Parameter | Type   | Default | Description                              |
| --------- | ------ | ------- | ---------------------------------------- |
| `limit`   | number | 50      | Number of orgs to return (1–100).        |
| `offset`  | number | 0       | Number of orgs to skip (for pagination). |

### Response

```json 200 OK theme={"dark"}
{
  "data": [
    {
      "id": "a1b2c3d4-...",
      "name": "Acme Tours",
      "widget_token": "f8e7d6c5b4a3...",
      "external_id": "customer-12345",
      "created_at": "2026-03-15T10:30:00.000Z"
    }
  ],
  "total": 142
}
```

### Examples

<CodeGroup>
  ```bash cURL theme={"dark"}
  curl "https://api.open.cx/partner/v1/orgs?limit=10&offset=0" \
    -H "Authorization: Bearer YOUR_PARTNER_API_KEY"
  ```

  ```javascript Node.js theme={"dark"}
  const response = await fetch('https://api.open.cx/partner/v1/orgs?limit=10&offset=0', {
    headers: { 'Authorization': 'Bearer YOUR_PARTNER_API_KEY' },
  });

  const { data, total } = await response.json();
  ```

  ```python Python theme={"dark"}
  import requests

  response = requests.get(
      'https://api.open.cx/partner/v1/orgs',
      headers={'Authorization': 'Bearer YOUR_PARTNER_API_KEY'},
      params={'limit': 10, 'offset': 0},
  )

  result = response.json()
  ```
</CodeGroup>

***

## Get Org by external\_id

Look up a single org by the `external_id` you supplied at creation. Use this when you need to reverse-resolve from your internal identifier to the OpenCX org — for example, before creating an API key or generating a login link — without paginating through `/orgs`.

```
GET /partner/v1/orgs/by-external-id/:externalId
```

### Path parameters

| Parameter    | Type   | Description                                                                                                       |
| ------------ | ------ | ----------------------------------------------------------------------------------------------------------------- |
| `externalId` | string | The `external_id` you set when calling `POST /partner/v1/orgs`. URL-encode it if it contains reserved characters. |

### Response

```json 200 OK theme={"dark"}
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "name": "Acme Tours",
  "widget_token": "f8e7d6c5b4a39281...",
  "external_id": "customer-12345",
  "created_at": "2026-03-15T10:30:00.000Z"
}
```

### Error responses

| Status | Condition                                                       |
| ------ | --------------------------------------------------------------- |
| `401`  | Invalid or revoked partner API key.                             |
| `404`  | No org with this `external_id` exists for your partner account. |

<Note>
  Lookups are scoped to your partner account. If a different partner has an org with the same `external_id`, you will not see it — you only ever resolve orgs you created.
</Note>

### Examples

<CodeGroup>
  ```bash cURL theme={"dark"}
  curl "https://api.open.cx/partner/v1/orgs/by-external-id/customer-12345" \
    -H "Authorization: Bearer YOUR_PARTNER_API_KEY"
  ```

  ```javascript Node.js theme={"dark"}
  const externalId = encodeURIComponent('customer-12345');
  const response = await fetch(`https://api.open.cx/partner/v1/orgs/by-external-id/${externalId}`, {
    headers: { 'Authorization': 'Bearer YOUR_PARTNER_API_KEY' },
  });

  if (response.status === 404) {
    // org with this external_id doesn't exist yet — create it
  }

  const org = await response.json();
  ```

  ```python Python theme={"dark"}
  import requests
  from urllib.parse import quote

  external_id = quote('customer-12345', safe='')
  response = requests.get(
      f'https://api.open.cx/partner/v1/orgs/by-external-id/{external_id}',
      headers={'Authorization': 'Bearer YOUR_PARTNER_API_KEY'},
  )

  if response.status_code == 404:
      # org with this external_id doesn't exist yet — create it
      pass

  org = response.json()
  ```
</CodeGroup>

***

## Create Org API Key

Create an API key for a specific org. The returned key authenticates requests to the [OpenCX Public API](/api-reference/authentication) (crawling, training, contacts, etc).

```
POST /partner/v1/orgs/:orgId/api-keys
```

### Request body

| Field  | Type   | Required | Description                                                         |
| ------ | ------ | -------- | ------------------------------------------------------------------- |
| `name` | string | No       | A label for the key (e.g. `"Production"`). Defaults to `"Default"`. |

### Response

```json 201 Created theme={"dark"}
{
  "api_key_id": "key-uuid-here",
  "api_key": "eyJhbGciOi..."
}
```

| Field        | Type   | Description                                                          |
| ------------ | ------ | -------------------------------------------------------------------- |
| `api_key_id` | string | The key's UUID.                                                      |
| `api_key`    | string | The JWT API key. Use this as `Bearer` token for org-level API calls. |

<Warning>
  The full API key is only returned once — store it securely. If lost, create a new one.
</Warning>

### Examples

<CodeGroup>
  ```bash cURL theme={"dark"}
  curl -X POST https://api.open.cx/partner/v1/orgs/ORG_ID/api-keys \
    -H "Authorization: Bearer YOUR_PARTNER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "name": "Production" }'
  ```

  ```javascript Node.js theme={"dark"}
  const response = await fetch(`https://api.open.cx/partner/v1/orgs/${orgId}/api-keys`, {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer YOUR_PARTNER_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ name: 'Production' }),
  });

  const { api_key_id, api_key } = await response.json();
  ```

  ```python Python theme={"dark"}
  import requests

  response = requests.post(
      f'https://api.open.cx/partner/v1/orgs/{org_id}/api-keys',
      headers={'Authorization': 'Bearer YOUR_PARTNER_API_KEY'},
      json={'name': 'Production'},
  )

  result = response.json()
  api_key = result['api_key']
  ```
</CodeGroup>

### Error responses

| Status | Condition                            |
| ------ | ------------------------------------ |
| `401`  | Invalid or revoked partner API key.  |
| `403`  | Org does not belong to this partner. |

***

## Create Login Link

Generate a short-lived, single-use URL that logs a user directly into the dashboard with the correct org context. No email, no forms, no org picker — the user lands in the dashboard immediately.

Use this instead of invitations when you want **frictionless access** — for example, embedding a "Manage support" button in your own platform that takes the user straight into their dashboard.

```
POST /partner/v1/orgs/:orgId/login-links
```

### Request body

| Field      | Type      | Required | Description                                                                                      |
| ---------- | --------- | -------- | ------------------------------------------------------------------------------------------------ |
| `email`    | string    | Yes      | Email address of the user. If no account exists, one is created automatically.                   |
| `name`     | string    | No       | Display name for the user (used only when creating a new account). Defaults to the email prefix. |
| `role_ids` | string\[] | No       | Array of role UUIDs to assign when creating org membership. Ignored if user is already a member. |

### Response

```json 201 Created theme={"dark"}
{
  "url": "https://platform.open.cx/partner-login?token=a1b2c3...",
  "expires_at": "2026-04-12T12:15:00.000Z"
}
```

| Field        | Type   | Description                                                         |
| ------------ | ------ | ------------------------------------------------------------------- |
| `url`        | string | The login URL. Redirect the user's browser here.                    |
| `expires_at` | string | ISO 8601 timestamp. The link expires **15 minutes** after creation. |

<Warning>
  Login links are **single-use** — once a user visits the URL, it is consumed and cannot be reused. Generate a new link for each login session.
</Warning>

### Security properties

| Property      | Value                               |
| ------------- | ----------------------------------- |
| Token entropy | 256 bits (cryptographically random) |
| TTL           | 15 minutes                          |
| Single-use    | Token deleted on first use          |
| Rate limit    | 30 links/min per partner            |

### Error responses

| Status | Condition                            |
| ------ | ------------------------------------ |
| `401`  | Invalid or revoked partner API key.  |
| `403`  | Org does not belong to this partner. |

### Examples

<CodeGroup>
  ```bash cURL theme={"dark"}
  curl -X POST https://api.open.cx/partner/v1/orgs/ORG_ID/login-links \
    -H "Authorization: Bearer YOUR_PARTNER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "email": "owner@acmetours.com", "name": "Jane Smith" }'
  ```

  ```javascript Node.js theme={"dark"}
  const response = await fetch(`https://api.open.cx/partner/v1/orgs/${orgId}/login-links`, {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer YOUR_PARTNER_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      email: 'owner@acmetours.com',
      name: 'Jane Smith',
    }),
  });

  const { url, expires_at } = await response.json();
  // Redirect the user's browser to `url`
  ```

  ```python Python theme={"dark"}
  import requests

  response = requests.post(
      f'https://api.open.cx/partner/v1/orgs/{org_id}/login-links',
      headers={'Authorization': 'Bearer YOUR_PARTNER_API_KEY'},
      json={
          'email': 'owner@acmetours.com',
          'name': 'Jane Smith',
      },
  )

  result = response.json()
  login_url = result['url']
  # Redirect the user's browser to login_url
  ```
</CodeGroup>

***

## Idempotency

Use `external_id` to prevent duplicate orgs. If you call `POST /partner/v1/orgs` twice with the same `external_id`, the second call returns `409 Conflict`. This makes it safe to retry org creation without risk of duplicates. Use [`GET /partner/v1/orgs/by-external-id/:externalId`](#get-org-by-external_id) to resolve the existing org directly — no pagination required.

<Note>
  The `external_id` uniqueness constraint is scoped to your partner account. Different partners can use the same `external_id` values.
</Note>
