Reference
Authentication headers, error codes, environment variables, deployment, and configuration reference for the FreeRouter API. Uses only free providers - $0 cost.
Authentication
The API has no built-in API key. It is a stateless proxy - downstream provider authentication happens via per-provider HTTP headers.
Header mapping
Each provider uses its own header for the API key:
| Provider | Header |
|---|---|
| Groq | X-Groq-Key |
X-Google-Key | |
| OpenRouter | X-Openrouter-Key |
| Cloudflare | X-Cloudflare-Key |
| NVIDIA NIM | X-Nvidia-Key |
| Cerebras | X-Cerebras-Key |
| Together AI | X-Together-Key |
| Fireworks AI | X-Fireworks-Key |
| Mistral AI | X-Mistral-Key |
| SambaNova | X-Sambanova-Key |
| DeepSeek | X-Deepseek-Key |
| DeepInfra | X-Deepinfra-Key |
| Cohere | X-Cohere-Key |
How it works
- Server reads provider headers from the incoming request
- Builds a
FreeRouterKeysobject with only the providers present in headers - Passes keys to
freerouter.languageModel(alias, keys) - Resolution automatically excludes providers with no key
Security
Key protection
- Keys are never logged - the request logger sanitizes all headers and paths
- Keys are never stored server-side
- Error messages redact key patterns matching known formats
- CORS allows all 13 provider key headers through
Error Responses
All errors are returned in OpenAI-compatible format.
Error format
{
"error": {
"message": "All providers failed: groq: rate limit exceeded; google: Internal server error",
"type": "freerouter_all_providers_failed",
"code": 502
}
}Status codes
| HTTP Status | Error Type | Condition |
|---|---|---|
400 | invalid_request_error | Invalid request body (Zod validation failure) |
429 | rate_limit_error | IP-based rate limit exceeded |
502 | freerouter_all_providers_failed | All candidate providers exhausted |
502 | freerouter_provider_error | Single provider error |
500 | server_error | Unexpected internal error |
400 - Invalid request
{
"error": {
"message": "Validation failed: messages: Expected array, received string",
"type": "invalid_request_error",
"code": 400
}
}429 - Rate limited
Default: 100 requests per 60 seconds per IP. Response includes Retry-After header.
{
"error": {
"message": "Rate limit exceeded. Retry after 42 seconds",
"type": "rate_limit_error",
"code": 429
}
}Retry-After: 42502 - Provider errors
{
"error": {
"message": "All providers failed: groq: rate limit exceeded; google: Internal server error",
"type": "freerouter_all_providers_failed",
"code": 502
}
}Key redaction
Known key patterns are replaced with [REDACTED] in error messages:
| Pattern | Provider |
|---|---|
gsk_... | Groq |
sk-or-... | OpenRouter |
AIza... | |
nvapi-... | NVIDIA NIM |
CF... | Cloudflare |
ghp_... / github_pat_... | GitHub (general precaution) |
Client-side error handling
import OpenAI from "openai"
const client = new OpenAI({
baseURL: "http://localhost:3000/v1",
apiKey: "unused",
})
async function retryOnRateLimit(model: string, messages: any[], maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await client.chat.completions.create(
{ model, messages },
{ headers: { "X-Groq-Key": process.env.GROQ_API_KEY } }
)
} catch (err: any) {
if (err.status === 429) {
const retryAfter = Number(err.headers?.get("Retry-After") ?? 5)
console.log(`Rate limited. Retrying after ${retryAfter}s...`)
await new Promise((r) => setTimeout(r, retryAfter * 1000))
continue
}
if (err.status === 502) {
throw new Error("All providers are down. Try again later.")
}
throw err
}
}
throw new Error("Max retries exceeded")
}Deployment
Environment variables
Prop
Type
Running the server
bun run apps/api/src/index.tsGraceful shutdown
The server handles SIGTERM and SIGINT for clean teardown:
^C received. Shutting down gracefully...Rate limiting
IP-based, in-memory sliding window.
| Setting | Default | Notes |
|---|---|---|
RATE_LIMIT_MAX | 100 | Max requests per window per IP |
RATE_LIMIT_WINDOW_MS | 60000 | Window in ms |
- IP determined by
x-forwarded-forheader →x-real-ip→ remote address - 429 responses include
Retry-Afterheader - Stale buckets cleaned every 5 minutes
CORS
Enabled on all /v1/* routes. Allow headers include all 13 provider key headers plus Retry-After.
CORS_ORIGINS env var controls allowed origins (default: *).
Logging
Per-request logging with automatic key sanitization:
GET /v1/models 200 42ms
POST /v1/chat/completions 200 1234msSanitization
Any 8+ character alphanumeric token in log lines is automatically replaced with [REDACTED]. This catches API keys, tokens, and other sensitive strings.
Testing
bun test --filter @freerouter/apiTest coverage includes:
| Area | What's tested |
|---|---|
| Health | GET /health returns status and providers |
| Models | GET /v1/models returns aliases + concrete models |
| Chat completions | Non-streaming, streaming, alias, pinned model |
| Authentication | Header extraction, partial keys |
| Rate limiting | 429 response, retry-after header |
| Error mapping | All error codes, key sanitization |
| OpenAI SDK compatibility | End-to-end with official SDK |