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

# Bring your own key

> Run requests on your own OpenAI, Anthropic, Google AI Studio or OpenRouter account, and pay only our fees.

Source: https://telluvian.ai/docs/byok

Add your own provider API keys and requests they can serve run on **your**
provider account. You pay the provider for the tokens, at your own rates and
under your own limits. We bill only our fees:

| On a request served by your key | You pay us |
| --- | --- |
| Model tokens | Nothing. The provider bills you directly. |
| [Verification](https://telluvian.ai/docs/include-scores.md) | $1.00 per 1M completion tokens, when enabled. |
| [Model Select](https://telluvian.ai/docs/model-select.md) | $0.05 per 1M input tokens, when we pick the model. |

A plain request on your own key, with no verification and a named model, costs
nothing on Telluvian. Your balance still needs to be above zero for requests to
be admitted.

Nothing changes in your application code: you keep calling Telluvian with your
Telluvian API key, and requests your provider keys can serve use them
automatically. Every response then says which of your keys served it.

Add keys in the dashboard under [Provider keys](https://telluvian.ai/dashboard/provider-keys), or
through [the API](#managing-keys-with-the-api). Each key is checked with the
provider when you add it, then stored encrypted. It is never shown again, only
a masked label such as `sk-pro…1a2b`.

| `provider` | Key from | Serves |
| --- | --- | --- |
| `openai` | OpenAI | `openai/` models |
| `anthropic` | Anthropic | `anthropic/` (Claude) models |
| `google` | Google AI Studio | `google/` Gemini models |
| `openrouter` | OpenRouter | Any catalogue model except Claude |

## Which key serves a request

1. A key for the **model's own provider** first: an `openai/` model uses your
   OpenAI key, a `google/` Gemini model your Google AI Studio key.
2. Then your **OpenRouter** key, which can serve any model in the catalogue.
   Claude models are the exception: they only ever use an Anthropic key.
3. Several keys for one provider are tried in their `sort_order`, lowest first.

A key is skipped for a request when:

- it is turned off (`disabled`);
- its `allowed_models` excludes the model. `allowed_models` takes catalogue ids
  or patterns such as `anthropic/*` or `openai/gpt-5*`, and `null` allows any;
- its `allowed_api_key_ids` excludes the Telluvian API key making the request
  (see [Keys for each API key](#keys-for-each-api-key)).

If none of your keys can serve the model, the request runs on our key at the
normal price, exactly as it would without BYOK.

### Choosing the key on a request

To choose the key yourself, give it a `slug` and name it on the request:

```json
{"model": "openai/gpt-4.1", "messages": [...], "byok_credential": "team-a"}
```

With the OpenAI Python SDK, pass it through `extra_body`:

```python
client.chat.completions.create(
    model="openai/gpt-4.1",
    messages=[{"role": "user", "content": "Hello"}],
    extra_body={"byok_credential": "team-a"},
)
```

That key alone serves the request (a key's `id` works too). It works on
`/v1/chat/completions` and `/v1/responses`, and is never sent to the provider.
If the key is turned off, excluded by its `allowed_models` or
`allowed_api_key_ids`, or its provider does not serve the model, the request
fails with `400` rather than quietly using another key. If the key fails at the
provider, our key takes over only if that key has `allow_platform_fallback`
turned on.

### Keys for each API key

By default every Telluvian API key on your account can use every provider key.
To keep, say, production traffic on one OpenAI account and staging on another,
set `allowed_api_key_ids` on each provider key to the ids of the Telluvian API
keys that may use it:

```json
{"allowed_api_key_ids": ["2b0c6a51-8f3e-4b0e-9d1a-7c5e4f3a2b10"]}
```

Copy an API key's id from its **⋯** menu on the [API keys](https://telluvian.ai/dashboard/keys)
page. Only active, ordinary API keys on your own account are accepted. A
management key or a revoked key is rejected with `invalid_allowed_api_key_ids`.
`null`, the default, means every API key. A request whose API key may use none
of your provider keys runs on our key at the normal price, as if you had none.

## Which key served a request

When your account has a provider key that could serve the request, its `usage`
gains `byok_credential`, naming the key that served it:

```json
"usage": {
  "prompt_tokens": 12,
  "completion_tokens": 9,
  "total_tokens": 21,
  "byok_credential": {
    "id": "6f1c0d0e-6c1d-4a59-9a4c-1f3e2a7b9c10",
    "slug": "production",
    "provider": "openai"
  }
}
```

- `id` is the key's id from `/v1/byok`. `slug` is `null` if you have not set one.
- `"byok_credential": null` means our key served it, because your keys failed
  and had `allow_platform_fallback` on. That request was billed at the normal
  price.
- No `byok_credential` field at all means none of your keys applied to the
  request, and it ran on our key at the normal price.

It is on Chat Completions and Responses, streamed or not: in a chat stream, on
the chunk that carries `usage`, and in a Responses stream, on
`response.completed`. With the Python SDK, read it as
`getattr(response.usage, "byok_credential", None)`.

Use it to attribute provider spend when you have several keys. Every usage
record also names the key that served it, and records what your provider account
was charged for it at our list price (`byok_cost_micros`). That figure is for
your reference only and is never billed. In the dashboard, those requests are
marked **Own key**.

## When your key fails

If a key fails before it has produced any output, the next key is tried. That
happens when:

- the key itself fails: rejected (`401`), out of credit (`402`), forbidden from
  the model (`403`) or rate-limited (`429`);
- the provider fails: a `5xx`, an overload or a timeout.

A `400` is about the request rather than the key, so it is returned at once
without trying other keys. A failure after output has started ends the request.
It is never resent to another key halfway through.

When every applicable key has failed, the request **fails**, unless every one of
them has `allow_platform_fallback` turned on, in which case it runs on our key
and is billed at the normal price. Fallback is off by default, so bringing a key
never costs you platform tokens you did not opt into.

The error never contains the provider's own message, which can quote part of
your key:

```json
{
  "error": {
    "message": "Your OpenAI API key was rejected as invalid (upstream 401). Check it in the dashboard, or enable platform fallback on the key.",
    "type": "upstream_error",
    "code": "byok_credential_rejected"
  }
}
```

A key the provider rejects three times in a row is **turned off** and shows as
*Rejected* in the dashboard. Replace the key there to turn it back on. Provider
outages and rate limits never turn a key off. A key's most recent failure is in
its `last_error` (such as `"upstream 429"`), and its next success clears it.

> **Streamed verification requests**
>
> A streamed request with verification enabled starts its response before the
> provider is called, so a key that fails there ends the stream early rather
> than returning an error status. See [streaming failures](https://telluvian.ai/docs/errors.md#streaming-failures).

## Managing keys with the API

Provider keys are managed with a **management key**, a separate kind of API key
you create on the [API keys](https://telluvian.ai/dashboard/keys) page by choosing *Management*. A
management key can only call `/v1/byok`. It cannot run requests, and an
ordinary key cannot call `/v1/byok`. Keep management keys out of application
code.

The API is modelled on OpenRouter's BYOK API, so code written for one ports to
the other with few changes.

| Request | Does |
| --- | --- |
| `GET /v1/byok` | List your provider keys. |
| `POST /v1/byok` | Add one. |
| `GET /v1/byok/{id}` | Get one. |
| `PATCH /v1/byok/{id}` | Change its settings, or replace the key itself. |
| `DELETE /v1/byok/{id}` | Remove it. |

### The provider key object

Every endpoint returns provider keys in this shape. The key itself is never
returned.

```json
{
  "id": "6f1c0d0e-6c1d-4a59-9a4c-1f3e2a7b9c10",
  "provider": "openai",
  "slug": "production",
  "label": "sk-pro…1a2b",
  "name": "Production",
  "disabled": false,
  "allow_platform_fallback": false,
  "sort_order": 0,
  "allowed_models": null,
  "allowed_api_key_ids": null,
  "last_used_at": "2026-11-02T09:14:03Z",
  "last_error": null,
  "created_at": "2026-11-01T17:40:11Z",
  "updated_at": "2026-11-02T09:14:03Z"
}
```

| Field | Meaning |
| --- | --- |
| `id` | The key's id. Usage records and `usage.byok_credential` refer to it. |
| `provider` | `openai`, `anthropic`, `google` or `openrouter`. Fixed once added. |
| `slug` | Optional stable name for `byok_credential` on a request: 1 to 63 lowercase letters, digits, `-` and `_`, unique on your account. |
| `label` | A masked snippet of the key, to tell keys apart. |
| `name` | Optional free-text name. |
| `disabled` | `true` when turned off, by you or after repeated rejections. |
| `allow_platform_fallback` | Whether our key may serve a request once yours have failed. Default `false`. |
| `sort_order` | Order among keys for the same provider, lowest first. Default `0`. |
| `allowed_models` | Catalogue ids or patterns this key may serve (up to 100), or `null` for any. |
| `allowed_api_key_ids` | Ids of the Telluvian API keys that may use it (up to 100), or `null` for all. |
| `last_used_at` | When it last served a request, updated at most about once a minute. |
| `last_error` | Its most recent failure, such as `"upstream 401"`, or `null`. |

### Add a key

```bash
curl https://api.telluvian.ai/v1/byok \
  -H "Authorization: Bearer $TELLUVIAN_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "openai",
    "key": "'"$OPENAI_API_KEY"'",
    "name": "Production",
    "slug": "production",
    "allowed_models": ["openai/gpt-5*", "openai/gpt-4.1"]
  }'
```

Returns `201` with `{"data": <provider key>}`. `provider` and `key` are
required. `name`, `slug`, `disabled`, `allow_platform_fallback`, `sort_order`,
`allowed_models` and `allowed_api_key_ids` are optional. The key is checked
with the provider first. If the provider rejects it, nothing is saved and you
get `400 invalid_provider_key`.

### List and inspect

```bash
curl "https://api.telluvian.ai/v1/byok?provider=openai" \
  -H "Authorization: Bearer $TELLUVIAN_MANAGEMENT_KEY"
```

```json
{"data": [<provider key>, ...], "total_count": 1}
```

`offset` and `limit` (1 to 100, default 50) page the list, and `provider`
filters it to one provider. `GET /v1/byok/{id}` returns
`{"data": <provider key>}` for one key. Check `disabled` and `last_error` to
see whether a key is healthy.

### Change a key

Send only the fields to change. An explicit `null` clears `slug`,
`allowed_models` or `allowed_api_key_ids`.

```bash
curl -X PATCH https://api.telluvian.ai/v1/byok/6f1c0d0e-6c1d-4a59-9a4c-1f3e2a7b9c10 \
  -H "Authorization: Bearer $TELLUVIAN_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"allow_platform_fallback": true, "sort_order": 1}'
```

Returns `{"data": <provider key>}`. Send `key` to replace the key itself, for
example after rotating it with the provider. The new key is checked with the
provider again. To turn a key back on after it was turned off, send
`"disabled": false`, usually together with the replacement `key`. `provider`
cannot be changed: add a new key instead.

### Delete a key

```bash
curl -X DELETE https://api.telluvian.ai/v1/byok/6f1c0d0e-6c1d-4a59-9a4c-1f3e2a7b9c10 \
  -H "Authorization: Bearer $TELLUVIAN_MANAGEMENT_KEY"
```

Returns `{"id": "...", "deleted": true}`. Requests it would have served use your
other keys if any can serve them, and otherwise our key at the normal price.
Past usage records keep its id.

### In Python

```python
import os
import httpx

byok = httpx.Client(
    base_url="https://api.telluvian.ai/v1/byok",
    headers={"Authorization": f"Bearer {os.environ['TELLUVIAN_MANAGEMENT_KEY']}"},
)

created = byok.post("", json={
    "provider": "anthropic",
    "key": os.environ["ANTHROPIC_API_KEY"],
    "slug": "claude-prod",
}).raise_for_status().json()["data"]

for key in byok.get("").raise_for_status().json()["data"]:
    status = "off" if key["disabled"] else key["last_error"] or "ok"
    print(key["provider"], key["slug"], key["label"], status)
```

### Differences from OpenRouter

- `allow_platform_fallback` replaces OpenRouter's `is_fallback`. Here, order is
  set with `sort_order`, and the flag means "our key may serve the request once
  mine have failed".
- There are no workspaces, so no `workspace_id`. `allowed_api_key_ids` scopes a
  key to particular API keys instead.
- A key can have a `slug`, and a request can name it with `byok_credential` to
  be served by that key alone.
- Responses name the key that served them in `usage.byok_credential`.

## Errors

On a model request:

| Status | `code` | Cause |
| --- | --- | --- |
| `400` | `byok_credential_rejected` | Your key was rejected or is not permitted to use the model, and no other key could serve it. |
| `402` | `byok_credential_rejected` | Your provider account is out of credit. |
| `429` | `byok_credential_rejected` | The provider rate-limited your key. |
| `502` | `byok_credential_rejected` | The provider failed on your key (a `5xx` or overload). Retry shortly. |
| `504` | `byok_credential_rejected` | The provider did not respond in time on your key. Retry shortly. |
| `400` | `credential_not_found` | A request's `byok_credential` names no key on your account. |
| `400` | `credential_not_usable` | The key a request named is off, excluded by its `allowed_models` or `allowed_api_key_ids`, or its provider does not serve the model. |
| `403` | `management_key_not_allowed` | A model endpoint was called with a management key. |

On `/v1/byok`:

| Status | `code` | Cause |
| --- | --- | --- |
| `400` | `invalid_provider_key` | The provider rejected the key when you added or replaced it. Nothing was saved. |
| `400` | `too_many_credentials` | An account can store up to 50 provider keys. Delete one first. |
| `400` | `invalid_slug` | A `slug` that is not 1 to 63 lowercase letters, digits, `-` and `_`. |
| `400` | `invalid_allowed_models` | An `allowed_models` entry that is neither a catalogue id nor a pattern. |
| `400` | `invalid_allowed_api_key_ids` | An `allowed_api_key_ids` entry that is not an active, ordinary API key on your account. |
| `403` | `management_key_required` | `/v1/byok` was called with an ordinary key. |
| `404` | `credential_not_found` | No such key on your account. |
| `409` | `slug_taken` | Another of your keys already has that slug. |
| `502` | `byok_validation_unavailable` | The provider could not be reached to check the key. Try again. |
| `503` | `byok_unavailable` | Bringing your own key is not available on this account yet. |
