External providers (BYOK)
Bring your own provider keys on BitRouter Cloud — sealed-box encrypted client-side, never stored or logged in plaintext. Local mode auto-detects keys from the environment.
BYOK (bring your own key) routes your requests using your own provider account, not BitRouter's. You pay the provider directly at their list price — BitRouter takes no rev share, adds no per-token fee, and never sees plaintext keys on the cloud write path.
On BitRouter Cloud your keys are encrypted client-side with a sealed box and stored only as ciphertext. When you self-host or run the binary locally, the same providers are configured by local-mode env-var detection instead — no upload, no encryption step.
Cloud mode — sealed-box encryption
On cloud.bitrouter.ai, your provider key is encrypted client-side against the node's X25519 sealed-box public key before submission. The node never sees plaintext on the write path; ciphertext is decrypted in-memory at request time and never logged.
The flow:
- Fetch the node's public key.
GET /v1/byok/encryption-pubkeyreturns the current X25519 public key and akek_idfingerprint. Cache by fingerprint and pass it back asIf-None-Matchto short-circuit on304 Not Modified. - Encrypt the plaintext key. Use libsodium
crypto_box_seal(or any sealed-box implementation) against the public key. - Submit the ciphertext. The console does steps 1–2 in-browser when you paste a key — you never need to leave the dashboard. The same submission API is also available for scripted onboarding; see the API reference.
Encryption recipe
If you're scripting key submission, here's the minimum sealed-box step. The output ciphertext is what the submission endpoint expects.
import sodium from 'libsodium-wrappers';
await sodium.ready;
const meta = await fetch(
'https://cloud.bitrouter.ai/v1/byok/encryption-pubkey'
).then(r => r.json());
const ciphertext = sodium.crypto_box_seal(
sodium.from_string(process.env.OPENAI_API_KEY),
sodium.from_base64(meta.public_key, sodium.base64_variants.ORIGINAL)
);
const ciphertextB64 = sodium.to_base64(ciphertext, sodium.base64_variants.ORIGINAL);
// Submit { provider_name: 'openai', kek_id: meta.kek_id, ciphertext_b64: ciphertextB64, key_prefix: 'sk-...' }import os, base64, requests
from nacl.public import PublicKey, SealedBox
meta = requests.get('https://cloud.bitrouter.ai/v1/byok/encryption-pubkey').json()
pubkey = PublicKey(base64.b64decode(meta['public_key']))
ciphertext = SealedBox(pubkey).encrypt(os.environ['OPENAI_API_KEY'].encode())
payload = {
'provider_name': 'openai',
'kek_id': meta['kek_id'],
'ciphertext_b64': base64.b64encode(ciphertext).decode(),
'key_prefix': 'sk-...',
}
# POST `payload` to the BYOK submission endpoint.META=$(curl -s https://cloud.bitrouter.ai/v1/byok/encryption-pubkey)
PUBKEY=$(echo "$META" | jq -r .public_key)
KEK_ID=$(echo "$META" | jq -r .kek_id)
CIPHERTEXT=$(printf '%s' "$OPENAI_API_KEY" \
| sodium-seal --pubkey "$PUBKEY" \
| base64)
# Submit { provider_name: "openai", kek_id: "$KEK_ID", ciphertext_b64: "$CIPHERTEXT", key_prefix: "sk-..." }The pubkey endpoint honors If-None-Match for cheap caching — pin the kek_id from a prior response and you'll get 304 Not Modified while the key is unchanged.
Key scope
Keys are scoped to your user account — every API key and OAuth token issued under your account can route requests through the keys you have stored. No one can read the ciphertext or the raw key.
Rotation, revocation, and audit
- Rotate by submitting a new ciphertext for the same provider — the previous record is overwritten atomically.
- Revoke from the dashboard. In-flight requests using the prior key complete; new requests get
402 Payment Required. - Audit — every submission is recorded with its time and the
kek_idused. Plaintext is never visible to anyone, including BitRouter operators.
If a node's kek_id rotates (we re-key every 90 days), the previous key is retained in memory so already-submitted ciphertexts remain decryptable at request time. New submissions must use the current kek_id; re-encrypt only if you explicitly want to migrate to the new key.
Local mode — env-var auto-detection
When you self-host or run the binary locally there's no upload step: set provider keys in the environment and you're done. No config file required.
export OPENAI_API_KEY=sk-...
export ANTHROPIC_API_KEY=sk-ant-...
export GOOGLE_API_KEY=AIza...
bitrouterBitRouter detects keys at startup and exposes only the providers whose keys are present. If a provider's key is missing, attempts to route to that provider return 402 Payment Required with a structured error pointing at the missing variable.
Recognized variables
| Provider | Preferred name | Passthrough fallback |
|---|---|---|
| OpenAI | BITROUTER_OPENAI_API_KEY | OPENAI_API_KEY |
| Anthropic | BITROUTER_ANTHROPIC_API_KEY | ANTHROPIC_API_KEY |
BITROUTER_GOOGLE_API_KEY | GOOGLE_API_KEY, GEMINI_API_KEY | |
| Custom (registry-listed) | BITROUTER_<PROVIDER_ID>_API_KEY | — |
The BITROUTER_* variants take precedence over the passthrough names — useful when your shell already has OPENAI_API_KEY set for a different tool and you want BitRouter to use a different account.
For a custom provider registered as id: my-provider in the registry, set BITROUTER_MY_PROVIDER_API_KEY (uppercased, hyphens become underscores).
Key rotation is live. BitRouter watches the environment of its parent process; updating a key (re-export and kill -HUP $(pgrep bitrouter)) takes effect on the next request without restart.
Custom providers
BYOK works for any provider listed in the registry that declares byok in its manifest's payment.modes. The provider field in the encryption submission must match the registry id exactly. Local-mode env-var detection follows the BITROUTER_<PROVIDER_ID>_API_KEY convention.
How is this guide?
Managed Models
The hosted BitRouter Cloud provider — one account, no upstream keys — the full model catalog with pricing, and automatic discounts on open models.
Workspaces & Teams
Isolate keys, routing, policies, and observability per project — with team seats and agents that can self-manage inside the boundary.