
## Connect a provider key

`POST /v1/ai/credentials`

Connects a new provider key to your user (USER scope). The key is automatically verified with the provider before saving: a key that fails the check is not saved. One user can connect only one key per provider.

## Request fields (body)

| Field | Type | Required | Default | Description |
|------|-----|:-----:|-----------|----------|
| `providerId` | string | yes | — | Provider ID. List: [`GET /v1/ai/providers`](/docs/ai/credentials/providers) |
| `name` | string | yes | — | Arbitrary key name shown in the list |
| `credentials.apiKey` | string | yes | — | Provider key (e.g. `sk-...` for OpenAI) |
| `credentials.baseUrl` | string | conditional | — | Required for `custom-openai-compat`. Format: `https://api.example.com/v1`. Only `http`/`https`, private networks forbidden |
| `isDefault` | boolean | no | `false` | Use this key as the default for the provider |

## Examples

### curl — personal key

```bash
curl -X POST https://vibecode.bitrix24.com/v1/ai/credentials \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "providerId": "openai",
    "name": "My personal OpenAI",
    "credentials": {
      "apiKey": "sk-proj-..."
    },
    "isDefault": true
  }'
```

### curl — OAuth application

```bash
curl -X POST https://vibecode.bitrix24.com/v1/ai/credentials \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "providerId": "openai",
    "name": "My personal OpenAI",
    "credentials": {
      "apiKey": "sk-proj-..."
    },
    "isDefault": true
  }'
```

### JavaScript — personal key

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/ai/credentials', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    providerId: 'openai',
    name: 'My personal OpenAI',
    credentials: { apiKey: 'sk-proj-...' },
    isDefault: true,
  }),
})

const { success, data } = await res.json()
if (success) console.log('Key connected, ID:', data.id)
```

### JavaScript — OAuth application

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/ai/credentials', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    providerId: 'openai',
    name: 'My personal OpenAI',
    credentials: { apiKey: 'sk-proj-...' },
  }),
})

const { data } = await res.json()
console.log('ID:', data.id)
```

## Connecting a Custom OpenAI-Compatible service

For an arbitrary OpenAI-compatible service (Minimax, Cerebras, Cohere, a corporate `vLLM`/`Ollama`, etc.) use `providerId: cprv_custom_seed` and be sure to specify `baseUrl`:

```bash
curl -X POST https://vibecode.bitrix24.com/v1/ai/credentials \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "providerId": "cprv_custom_seed",
    "name": "Corporate service",
    "credentials": {
      "apiKey": "sk-corp-...",
      "baseUrl": "https://llm.example.com/v1"
    }
  }'
```

## Response fields

| Field | Type | Description |
|------|-----|----------|
| `success` | boolean | Always `true` on success |
| `data.id` | string | Unique ID of the created key |
| `data.providerId` | string | Provider ID |
| `data.provider.slug` | string | Provider system name |
| `data.provider.name` | string | Provider name |
| `data.name` | string | Key name from the request |
| `data.isDefault` | boolean | Default-key flag |
| `data.createdAt` | string | Creation time in `ISO 8601` |
| `data.updatedAt` | string | Last update time in `ISO 8601` |

## Response example

HTTP status `201 Created`:

```json
{
  "success": true,
  "data": {
    "id": "cred_abc123def456",
    "providerId": "openai",
    "provider": {
      "slug": "openai",
      "name": "OpenAI"
    },
    "name": "My personal OpenAI",
    "isDefault": true,
    "createdAt": "2026-04-27T11:30:00.000Z",
    "updatedAt": "2026-04-27T11:30:00.000Z"
  }
}
```

## Error response example

`422 credential_invalid` — the key failed verification with the provider:

```json
{
  "success": false,
  "error": {
    "code": "credential_invalid",
    "message": "OpenAI API 401: Incorrect API key provided"
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `invalid_request` | The `body` schema is violated (missing required fields) |
| 400 | `no_key` | `credentials` has no `apiKey` field |
| 400 | `base_url_invalid` | `baseUrl` uses a scheme other than `http`/`https`, or is malformed |
| 400 | `base_url_private` | `baseUrl` points at a private network or does not resolve to an IP address via `DNS` |
| 404 | `provider_not_found` | A provider with this `providerId` does not exist or is disabled |
| 409 | `already_exists` | A key for this provider is already connected — use [`PATCH /v1/ai/credentials/:id`](./update.md) |
| 422 | `credential_invalid` | The key failed verification with the provider. The message contains details from the provider |
| 422 | `provider_geoblocked` | The provider rejected the key check based on our server's region. Connect your own proxy or switch to OpenRouter. The response carries a `suggestions` field |
| 403 | `scope_missing` | The API key lacks the `vibe:ai` scope |

Full list of common API errors — [Errors](/docs/errors).

Key creation limit: 10 requests per minute (one bucket per portal — all the portal's keys share it).

## Known specifics

**Verification before saving.** The key is verified with the provider via `verify()` — an HTTP request to `GET /v1/models` or (if the provider does not support `models`) to `POST /v1/chat/completions` with a minimal body. The verification timeout is 10 seconds. For an invalid key, nothing is saved to the database.

**SSRF protection for the Custom provider.** The `baseUrl` for `custom-openai-compat` must pass a check: private networks are forbidden (`127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`), `IPv6` link-local and ULA, `IPv4`-mapped `IPv6`. Only `http://`/`https://` schemes are accepted.

**Error message sanitized.** The error text from the provider is truncated to 200 characters, and URLs are replaced with the placeholder `[URL]` — this protects against accidental leakage of internal addresses into the client's logs.

## See also

- [Provider list](/docs/ai/credentials/providers)
- [Update a key](./update.md)
- [Test a key](./test.md)
- [Your own keys (BYOK)](/docs/ai/credentials)
