Quickstart
From clone to your first response, end to end.
This walks through the full path once: start the gateway, register a real provider behind a public model, issue a client key, and make a call. It assumes one OpenAI key, but every step works the same for Anthropic, Google, Azure, or an OpenAI-compatible endpoint — see Creating deployments for the per-provider bodies.
You need two kinds of credentials and they are not the same thing: the master key (
MASTER_KEY, operator-only) administers the gateway, and virtual keys (unified-…) are what your clients send. Never hand the master key to a client.
1. Run the gateway
Requirements: Bun 1.3+, Postgres 18+, Redis 8+ (full configuration in Setup).
git clone https://github.com/boelabs/unified-gateway.git && cd unified-gateway
bun install
docker compose -f docker-compose.yml -f compose.local.yaml up -d postgres redisCreate apps/gateway/.env (see apps/gateway/.env.example) with at least:
PORT=4000
NODE_ENV=development
MASTER_KEY=replace-with-a-long-secret
CREDENTIALS_ENCRYPTION_KEY=64_hex_chars_32_bytes # openssl rand -hex 32
DATABASE_URL=postgres://gateway:gateway@localhost:5432/unifiedgateway
REDIS_URL=redis://localhost:6379Apply migrations and start it:
bun run --filter @boelabs/unified-gateway db:migrate
bun run --filter @boelabs/unified-gateway devConfirm it is up:
curl http://localhost:4000/health/ready # 200 once Postgres + Redis + extensions are healthyFor the rest of this page:
export BASE=http://localhost:4000
export MASTER_KEY=replace-with-a-long-secret2. Register a deployment
A deployment binds a public model name to a provider, an upstream model, and credentials. Create one with the master key — the provider API key goes inline and is encrypted at rest:
curl -X POST $BASE/admin/deployments \
-H "Authorization: Bearer $MASTER_KEY" -H "content-type: application/json" -d '{
"publicModel": "general",
"provider": "openai",
"upstreamModel": "gpt-5.4",
"credentials": { "apiKey": "sk-..." }
}'general is now a public model. Want to preview what the gateway resolved (capabilities, operations,
transports) without saving? Send the same body to POST /admin/deployments/resolve.
3. Issue a virtual key
Clients never use the master key. Mint a scoped virtual key instead — allowedModels limits which
public models it may call ([] or omitted = all):
curl -X POST $BASE/admin/keys \
-H "Authorization: Bearer $MASTER_KEY" -H "content-type: application/json" -d '{
"name": "my-app",
"allowedModels": ["general"]
}'The response contains the plaintext key once — store it now, it is never shown again:
{ "data": { "id": "…", "keyPrefix": "unified-abcd…", "key": "unified-…", "allowedModels": ["general"] } }See Virtual keys for budgets, RPM/TPM limits, and rotation.
4. Make a call
Use the virtual key exactly as you would an OpenAI key, with model set to your public name:
curl -X POST $BASE/v1/chat/completions \
-H "Authorization: Bearer unified-..." -H "content-type: application/json" -d '{
"model": "general",
"messages": [{ "role": "user", "content": "Say hello in one word." }]
}'Or with the official OpenAI SDK — this is the whole integration:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:4000/v1", api_key="unified-...")
resp = client.chat.completions.create(
model="general",
messages=[{"role": "user", "content": "Say hello in one word."}],
)
print(resp.choices[0].message.content)The same key and base URL also serve /v1/responses, /v1/messages (Anthropic shape),
/v1/embeddings, /v1/images/*, and /v1/audio/transcriptions. Every response carries an
x-request-id; virtual-key requests with limits also return x-ratelimit-* headers.
What to read next
- Concepts — the mental model behind public models, pools, adapters, and transports.
- Creating deployments — every provider and custom-model body.
- Fallbacks — make a public model survive a provider outage.
- Errors — what a
400/401/403/429/503means and how to fix it.