Bring your own key
Run requests on your own OpenAI, Anthropic, Google AI Studio or OpenRouter account, and pay only our fees.
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 | $1.00 per 1M completion tokens, when enabled. |
| Model Select | $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, or
through 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
- A key for the model's own provider first: an
openai/model uses your OpenAI key, agoogle/Gemini model your Google AI Studio key. - Then your OpenRouter key, which can serve any model in the catalogue. Claude models are the exception: they only ever use an Anthropic key.
- 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_modelsexcludes the model.allowed_modelstakes catalogue ids or patterns such asanthropic/*oropenai/gpt-5*, andnullallows any; - its
allowed_api_key_idsexcludes the Telluvian API key making the request (see 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:
{"model": "openai/gpt-4.1", "messages": [...], "byok_credential": "team-a"}With the OpenAI Python SDK, pass it through extra_body:
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:
{"allowed_api_key_ids": ["2b0c6a51-8f3e-4b0e-9d1a-7c5e4f3a2b10"]}Copy an API key's id from its ⋯ menu on the API 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:
"usage": {
"prompt_tokens": 12,
"completion_tokens": 9,
"total_tokens": 21,
"byok_credential": {
"id": "6f1c0d0e-6c1d-4a59-9a4c-1f3e2a7b9c10",
"slug": "production",
"provider": "openai"
}
}idis the key's id from/v1/byok.slugisnullif you have not set one."byok_credential": nullmeans our key served it, because your keys failed and hadallow_platform_fallbackon. That request was billed at the normal price.- No
byok_credentialfield 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:
{
"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.
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 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.
{
"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
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
curl "https://api.telluvian.ai/v1/byok?provider=openai" \
-H "Authorization: Bearer $TELLUVIAN_MANAGEMENT_KEY"{"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.
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
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
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_fallbackreplaces OpenRouter'sis_fallback. Here, order is set withsort_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_idsscopes a key to particular API keys instead. - A key can have a
slug, and a request can name it withbyok_credentialto 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. |