Add external keys (BYOK)
Bring your own provider keys — auto-detected from environment variables in local mode, or sealed-box encrypted in cloud mode.
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.
There are two deployment modes, each with a different key flow.
Local mode — env-var auto-detection
Run the binary, set provider keys in the environment, 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.
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: 'openai', kek_id: meta.kek_id, ciphertext: ciphertextB64 }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': 'openai',
'kek_id': meta['kek_id'],
'ciphertext': base64.b64encode(ciphertext).decode(),
}
# 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: "openai", kek_id: "$KEK_ID", ciphertext: "$CIPHERTEXT" }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
By default, keys are scoped to a workspace — every member can submit requests routed through those keys, but no one can read the ciphertext or the raw key.
For one-off overrides, attach a key to a single request via the X-BitRouter-Key header:
curl https://cloud.bitrouter.ai/v1/chat/completions \
-H "Authorization: Bearer $BITROUTER_TOKEN" \
-H "X-BitRouter-Key: openai=sk-..." \
-d '{...}'Per-request keys are transmitted as plaintext over TLS. The cloud node decrypts TLS, sees the key in memory for the request, then drops it — it is never persisted. This is convenient for ephemeral scripts and CI runs, but if you submit the same key on every request, prefer the sealed-box upload path: it's both more efficient (no per-request key bytes) and harder to leak via a debug log. In local mode, the loopback hop means the plaintext header never crosses the public network.
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 in the workspace's key history view — every submission is recorded with the submitting member, time, and
kek_idused. Plaintext is never visible to anyone, including BitRouter operators.
If a node's kek_id rotates (we re-key every 90 days), already-submitted ciphertexts are re-wrapped server-side using the previous key in memory; you don't need to re-encrypt unless you explicitly choose to.
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?
Last updated on