On this page
Overview
DevAI speaks the OpenAI Chat Completions format, so any OpenAI client works by changing the base URL and the API key. It is bring-your-own-key: requests run only on provider keys you add yourself. Behind that one endpoint the gateway picks one of your providers and keys, retries on the next key or provider when one fails, and records tokens and latency for the dashboard.
| Endpoint | Auth | Purpose |
|---|---|---|
| POST /v1/chat/completions | dvai_ key | Chat completion on your own provider keys, JSON or streaming (SSE) |
| GET /v1/models | dvai_ key | Models of the providers your key routes over |
| GET /v1/usage | dvai_ key | Usage summary for the calling key’s project |
| GET /v1/health | none | Gateway, database and Redis health (no provider needed) |
| /user/* | Clerk session | Your provider keys, key health, dvai_ keys and hackathon kits |
| /admin/* | admin | Providers, keys, projects, analytics (used by the dashboard) |
X-Request-ID header (e.g. req_3f9c2a1b7d4e8f60); include it when reporting a problem. The gateway runs on a free hosting tier, so the first request after a quiet period can take longer while it wakes up.Bring your own key
DevAI has no provider keys of its own to lend. Every signed-in user adds their own provider keys under Dashboard → Your keys, creates a DevAI API key there, and calls the gateway with it. The gateway routes that key's requests only over the keys its owner added. Provider usage is billed by each provider to you, and free tiers (Gemini, Groq, OpenRouter :free models) work too.
# 1. Sign in → Dashboard → Your keys → "Add your first provider key"
# (OpenAI, Anthropic, Gemini, Groq, OpenRouter or Mistral; stored encrypted)
# 2. Same page → "New key" → copy your dvai_ key (shown once)
# 3. Call the gateway; it routes only over the keys you added:
curl https://devai-gateway.onrender.com/v1/chat/completions \
-H "Authorization: Bearer $DEVAI_KEY" \
-H "Content-Type: application/json" \
-d '{ "model": "auto", "messages": [{ "role": "user", "content": "hi" }] }'
# Check your keys without spending tokens (Clerk session token, as the dashboard does):
# POST /user/pool/health → every key
# POST /user/pool/keys/:id/test → one keyWith no provider key (or every key disabled), chat requests fail fast with 400 no_provider_keys: the gateway never falls back to anyone else's keys. If you ask for a model none of your keys serves, you get 400 no_provider_for_model; use "model": "auto", pick a provider with devai.providers, or add a key for that provider.
{
"error": {
"type": "no_provider_keys",
"message": "no_provider_keys: add your own provider key in the dashboard (Your keys → Add a key), then retry. DevAI never uses shared keys for your requests.",
"request_id": "req_3f9c2a1b7d4e8f60"
}
}| Provider | id when adding a key | Default model for "auto" |
|---|---|---|
| OpenAI | openai | gpt-4o-mini |
| Anthropic | anthropic | claude-3-5-haiku-20241022 |
| Google Gemini | gemini | gemini-2.5-flash |
| Groq | groq | llama-3.1-8b-instant |
| OpenRouter | openrouter | openrouter/auto (any model id) |
| Mistral | mistral | mistral-small-latest (any model id) |
Authentication
Every /v1 endpoint except /v1/health needs a DevAI API key in the Authorization header. Create one under Dashboard → Your keys; keys start with dvai_ and are shown in full only once.
Authorization: Bearer dvai_your_keyA missing header, an unknown or revoked key, or an inactive project returns 401 with error.type = authentication_error. Keep the key on your server: browser requests are only accepted from origins on the gateway's CORS allowlist.
Chat completions
/v1/chat/completionsBearer dvai_…Creates a model response for a conversation. With "stream": true the answer is sent as Server-Sent Events (see Streaming).
curl https://devai-gateway.onrender.com/v1/chat/completions \
-H "Authorization: Bearer $DEVAI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "auto",
"messages": [
{ "role": "system", "content": "You are a concise assistant." },
{ "role": "user", "content": "Explain binary search in one line." }
],
"temperature": 0.3,
"max_tokens": 200
}'Request body
| Field | Type | Description |
|---|---|---|
| messages | array, required | List of { role, content }. role is system, user or assistant; content is a string. |
| model | string | Model id, e.g. gpt-4o-mini or llama-3.1-8b-instant. Defaults to auto: any of your providers, each using its default model. |
| temperature | number | Sampling temperature, passed to the provider. |
| max_tokens | number | Maximum tokens to generate. |
| top_p | number | Nucleus sampling, passed to the provider. |
| stop | string | string[] | Stop sequence(s). |
| stream | boolean | Stream the answer as SSE. |
| stream_options.include_usage | boolean | With streaming: send a final chunk with token usage. |
| user | string | Your end-user id (optional). |
| devai | object | DevAI routing overrides for this request; never forwarded to providers. See Routing. |
The devai object
| Field | Type | Description |
|---|---|---|
| mode | single | fastest | best | Routing mode (default single). |
| providers | array (max 10) | Providers to use, in priority order. Each entry is a provider id, name or type (google, openai, anthropic, groq, ollama), optionally with a model: "openai:gpt-4o-mini" or { "provider": "groq", "model": "llama-3.1-8b-instant" }. |
| judge | string | object | Model that picks the winner in best mode, same format as a provider entry. |
| timeoutMs | integer 1000–120000 | Per-provider call timeout. Default 30000. |
| keyStrategy | failover | round_robin | random | least_used | How a provider's keys are used. See Multi-key strategies. |
An invalid body returns 400 invalid_request with per-field details.
Response
The standard OpenAI chat.completion object, plus a devai object describing how the request was routed: the mode, the provider, model and masked key that answered, how many providers (fallbacks) and keys (key_fallbacks) were tried first, and one entry per provider call in candidates. In best mode it also has a judge object (method, reason, provider, model, latency_ms). Keys are always masked to a prefix and the last four characters.
{
"id": "chatcmpl-...",
"object": "chat.completion",
"created": 1792310400,
"model": "llama-3.1-8b-instant",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "Binary search halves a sorted range each step..." },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 14, "completion_tokens": 22, "total_tokens": 36 },
"devai": {
"mode": "single",
"provider": "Groq",
"model": "llama-3.1-8b-instant",
"fallbacks": 0,
"key": "gsk_…DyMg",
"key_fallbacks": 1,
"latency_ms": 912,
"candidates": [
{ "provider": "Groq", "provider_type": "groq", "model": "llama-3.1-8b-instant",
"status": "error", "latency_ms": 120, "key": "gsk_…k3Qa", "error": "429 rate limited" },
{ "provider": "Groq", "provider_type": "groq", "model": "llama-3.1-8b-instant",
"status": "success", "latency_ms": 780, "key": "gsk_…DyMg", "selected": true,
"usage": { "prompt_tokens": 14, "completion_tokens": 22, "total_tokens": 36 } }
]
}
}Streaming (SSE)
Set "stream": true to receive OpenAI-compatible Server-Sent Events: a series of chat.completion.chunk objects, a final chunk with finish_reason, and data: [DONE]. With "stream_options": { "include_usage": true } a usage chunk (empty choices) is sent before [DONE].
curl -N https://devai-gateway.onrender.com/v1/chat/completions \
-H "Authorization: Bearer $DEVAI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "auto",
"stream": true,
"stream_options": { "include_usage": true },
"messages": [{ "role": "user", "content": "Write a haiku about failover" }]
}'data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":1792310400,"model":"llama-3.1-8b-instant","choices":[{"index":0,"delta":{"role":"assistant","content":"Keys"},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":1792310400,"model":"llama-3.1-8b-instant","choices":[{"index":0,"delta":{"content":" rotate quietly"},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":1792310400,"model":"llama-3.1-8b-instant","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":1792310400,"model":"llama-3.1-8b-instant","choices":[],"usage":{"prompt_tokens":9,"completion_tokens":17,"total_tokens":26}}
data: [DONE]{"error":{"type":"stream_error",…}} followed by a final chunk with finish_reason: "error" and [DONE]. In fastest mode the first provider to produce text streams and the others are cancelled.Routing modes
| Mode | Behaviour |
|---|---|
| single | Default. One provider at a time in priority order. On failure the next key of the same provider is tried, then the next provider. |
| fastest | Sends the request to several providers in parallel (up to 5). The first valid answer wins; slower calls are aborted. |
| best | Collects every answer, then a judge model picks the most accurate and complete one. The decision is returned in devai.judge. |
Precedence: the request body devai field, then the x-devai-mode header, then the project's routing defaults (set in the dashboard), then single. With model: "auto" and no provider list, every provider you have a key for takes part, ordered by its configured priority and then recent latency.
# Race every provider; first valid answer wins
curl https://devai-gateway.onrender.com/v1/chat/completions \
-H "Authorization: Bearer $DEVAI_KEY" \
-H "Content-Type: application/json" \
-H "x-devai-mode: fastest" \
-d '{ "model": "auto",
"messages": [{ "role": "user", "content": "Summarise RAFT in 2 lines" }] }'
# Ask two providers, let a judge pick the best answer
curl https://devai-gateway.onrender.com/v1/chat/completions \
-H "Authorization: Bearer $DEVAI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "auto",
"messages": [{ "role": "user", "content": "Is 2^61-1 prime? Explain briefly." }],
"devai": {
"mode": "best",
"providers": ["openai:gpt-4o-mini", "groq:llama-3.1-8b-instant"],
"judge": "openai:gpt-4o-mini",
"timeoutMs": 20000
}
}'Multi-key strategies
A provider can hold several API keys. The key strategy only changes the order keys are tried in: a failing key always falls through to the next key of the same provider before DevAI moves to another provider, and keys that are cooling down are skipped.
| Strategy | Order |
|---|---|
| failover | Keys in priority order (oldest first); the first healthy key is used. |
| round_robin | Default for your keys. Rotates the starting key on every request to spread load evenly. |
| random | Shuffles the healthy keys for each request. |
| least_used | Key with the fewest recent requests first. |
Set it in the dashboard (Your keys → Rotation), or override per request with devai.keyStrategy in the body or the x-devai-key-strategy header. The body takes precedence over the header, the header over the project default, and that over the provider setting.
{
"model": "auto",
"messages": [{ "role": "user", "content": "hi" }],
"devai": { "keyStrategy": "round_robin" }
}
# or as a header (body wins if both are set):
x-devai-key-strategy: least_usedHeaders
Request headers
| Header | Description |
|---|---|
| Authorization | Required: Bearer dvai_… (or the admin credential on /admin). |
| Content-Type | application/json for POST requests. |
| x-devai-mode | single, fastest or best. Unknown values are ignored. |
| x-devai-key-strategy | failover, round_robin, random or least_used. Unknown values are ignored. |
Response headers (chat completions)
| Header | Description |
|---|---|
| X-Request-ID | Unique id of this request (on every response). |
| x-devai-mode | Routing mode that was used. |
| x-devai-provider | Provider and model that answered, e.g. OpenAI/gpt-4o-mini. |
| x-devai-fallbacks | How many providers failed before this one answered. |
| x-devai-key | Name and masked hint of the key that answered, e.g. Key 2 (sk-p...DyMg). |
| x-devai-key-fallbacks | How many keys of the provider failed before this one. |
| X-Cache | HIT or MISS for cacheable requests (see Caching). |
Browsers on allowlisted origins can read all of these headers (they are exposed via CORS).
Caching
Plain requests on the admin's own projects (mode single and no devai field) are cached in Redis for one hour. Requests on your own keys are never served from, or written to, the shared cache; an identical request is answered from the cache with X-Cache: HIT. Identical requests that arrive at the same time share a single upstream call. To skip the cache, send any devai field or use a parallel mode. Streaming requests are cached when they finish normally.
Models
/v1/modelsBearer dvai_…Lists the models your key can use, in the OpenAI list format: each of your providers' default model plus any models registered for them. The list is empty until you add a provider key.
curl https://devai-gateway.onrender.com/v1/models -H "Authorization: Bearer $DEVAI_KEY"
# {
# "object": "list",
# "data": [
# { "id": "gpt-4o-mini", "object": "model", "created": 1792310400, "owned_by": "OpenAI" },
# { "id": "llama-3.1-8b-instant", "object": "model", "created": 1792310400, "owned_by": "Groq" }
# ]
# }
# Only models of the providers YOUR key routes over; empty until you add a provider key.Usage
/v1/usage?period=today|week|monthBearer dvai_…Token and request totals for the project that owns the key. period defaults to today; cacheHitRate is a percentage.
curl "https://devai-gateway.onrender.com/v1/usage?period=week" -H "Authorization: Bearer $DEVAI_KEY"
# {
# "requestCount": 42, "inputTokens": 5120, "outputTokens": 8730, "totalTokens": 13850,
# "avgLatencyMs": 840, "cacheHitRate": 12, "period": "week"
# }Health
/v1/healthpublicNo authentication, and no provider is needed: it reports the gateway, database and Redis. status is unhealthy when the database is down and degraded when Redis is down. Provider keys belong to users, so they never change this status; providers lists only the admin's shared providers from the last background check (key counts only) and is usually empty. Check your own keys in the dashboard or with POST /user/pool/health.
curl https://devai-gateway.onrender.com/v1/health
# {
# "status": "healthy", // healthy | degraded | unhealthy (gateway, database, Redis)
# "gateway": "healthy",
# "database": "healthy",
# "redis": "healthy",
# "byok": true,
# "providers": [], // shared providers only, if any; never affects status
# "providers_checked_at": null,
# "uptime": 5321.4,
# "timestamp": "2026-10-18T08:00:00.000Z"
# }Your provider keys
Signed-in users add their own provider keys (OpenAI, Anthropic, Gemini, Groq, OpenRouter, Mistral) under Dashboard → Your keys. A DevAI API key (dvai_…, created on that page) is routed only over that user's keys, with their rotation strategy. A key that is rate-limited cools down and the next key answers. This traffic is never cached and never uses shared keys.
| Plan | Provider keys |
|---|---|
| Free | 2 |
| Student | 10 |
| Pro | Unlimited |
| Team | Unlimited |
The pool is managed with the user's Clerk session token (Authorization: Bearer <session JWT>); admin keys and project keys are not accepted. Adding a key beyond the plan limit returns 403 plan_limit_reached. With no enabled key, chat requests return 400 no_provider_keys.
| Endpoint | Description |
|---|---|
| GET /user/pool | Plan, limit, key strategy, keys (masked, with health: healthy, unused, cooling_down, quota_exhausted, error, disabled, and lastCheck), onboarding (nextStep: add_provider_key, create_api_key or done) and 30-day stats: served, tokens, rateLimitsAbsorbed, stretchFactor. |
| POST /user/pool/keys | Add a key: provider (openai, anthropic, gemini, groq, openrouter, mistral), apiKey, optional name, defaultModel, quotaLimit. Stored encrypted. |
| POST /user/pool/health | Test every enabled key with a cheap authenticated call (no tokens). Returns status (healthy, degraded, unhealthy, no_keys), healthy, total and one entry per key. A key that fails is skipped by routing for 5 minutes. |
| POST /user/pool/keys/:id/test | Test one of your keys the same way. |
| PATCH /user/pool/keys/:id | Update enabled or name. |
| DELETE /user/pool/keys/:id | Remove a key from the pool. |
| PUT /user/pool/strategy | { "keyStrategy": "round_robin" | "least_used" | "failover" | "random" } |
| POST /user/pool/api-keys | Create your dvai_ key (shown once). DELETE /user/pool/api-keys/:id revokes it. |
stretchFactor = answers served by all your keys ÷ answers served by your busiest single key. 3× means your keys served three times what one free key managed on its own.Hackathon kits
Team plan users can create kits under Dashboard → Hackathon kits. A kit is a set of temporary dvai_ keys for a team, with a request cap and/or token cap, an end time (up to 30 days), an optional max team size and an optional allowed-model list. Kit traffic runs on the owner's own provider keys, and the keys are used like any other API key. With an allowed-model list, "model": "auto" uses the first allowed model.
Every kit has a public page at /kit/<shareToken> that shows the quota left and the end time. Its data comes from GET /public/kits/:shareToken (no auth, never includes keys). Caps are counted in Redis across all of the kit's keys. Tokens are added when a response finishes, so the last request before a token cap can go slightly over it.
| Status | error.type | When |
|---|---|---|
| 400 | kit_model_not_allowed | The requested model is not on the kit’s allowed list (the message lists the allowed models). |
| 403 | kit_revoked | The owner revoked the kit. |
| 403 | kit_expired | The kit’s end time has passed. |
| 429 | kit_request_cap_reached | The kit has used all of its requests. |
| 429 | kit_token_cap_reached | The kit has used its token cap. |
| Endpoint (Clerk session) | Description |
|---|---|
| GET /user/kits | Your kits with status (active, expired, revoked) and usage (requests, tokens, requestsRemaining, tokensRemaining). |
| POST /user/kits | name, expiresAt (ISO 8601), requestCap and/or tokenCap, optional maxTeamSize, keyCount (1–50, ≤ team size), allowedModels. Returns the keys once. Requires Team (403 plan_required otherwise). |
| POST /user/kits/:id/revoke | Revoke a kit; its keys stop working immediately. |
Errors
Errors use one shape: an error object with type, message and, on /v1 routes, request_id.
| Status | error.type | When |
|---|---|---|
| 400 | invalid_request | Body is not JSON or fails validation (details lists the fields). |
| 400 | no_provider_keys | You have not added a provider key yet (or all are disabled). Add one under Your keys. Shared keys are never used instead. |
| 400 | no_provider_for_model | None of your provider keys serves the requested model. Use auto, devai.providers, or add a key for that provider. |
| 400 | no_eligible_providers | None of the providers in devai.providers are among yours. |
| 400 | validation_error | Admin API: a required field is missing or invalid. |
| 401 | authentication_error | Missing/invalid Authorization header, unknown or revoked key, inactive project, invalid admin key or expired session. |
| 403 | permission_error | Admin API: valid Clerk session, but the user is not on the admin allowlist. |
| 404 | not_found | Unknown route, or an admin resource that does not exist. |
| 409 | conflict | Admin API: the model already exists for this provider. |
| 500 | server_error | Unexpected gateway error. |
| 502 | all_providers_failed | Every provider and key failed or was skipped. Includes retry_after (seconds) and one attempts entry per call. |
| 400 | kit_model_not_allowed | Hackathon kit: model not allowed (see kits). |
| 403 | kit_revoked / kit_expired | Hackathon kit was revoked or has ended. |
| 403 | plan_limit_reached / plan_required | Key pool: plan key limit reached. Kits: Team plan required. |
| 429 | kit_request_cap_reached / kit_token_cap_reached | Hackathon kit used its cap. |
{
"error": {
"type": "all_providers_failed",
"message": "All 1 provider attempt(s) failed",
"request_id": "req_3f9c2a1b7d4e8f60",
"retry_after": 30,
"attempts": [
{ "provider": "Groq", "provider_type": "groq", "model": "llama-3.1-8b-instant",
"status": "skipped", "latency_ms": 0, "key": "gsk_…DyMg", "error": "rate limited (cooling down)" }
]
}
}Rate limits & quotas
The gateway does not add its own per-project request limit; the limits that apply are your providers' limits. DevAI tracks them per key and routes around keys that hit them:
| Provider response | What DevAI does |
|---|---|
| 429 rate limit | Key cools down for 30 s (or the provider's Retry-After, 5 s – 1 h); next key is tried. |
| 429 / 402 quota or billing | Key cools down for 10 minutes (or the provider hint); next key is tried. |
| 401 / 403 invalid key | Key cools down for 5 minutes; next key is tried. |
| 5xx, timeout, network | That key + model cools down for 60 s; next key is tried. |
| Other 4xx | Returned as a failure without trying other keys (they would fail the same way). |
Each provider key can also have a token quota (set when the key is added). A key over its quota is skipped until the quota resets (every 30 days). Google per-model quotas only cool down that key + model, so other models on the same key keep working. When nothing is left to try you get 502 all_providers_failed with retry_after.
Admin API
The admin part of the dashboard (shared providers, projects, analytics) uses /admin/*. Regular users never need it: their keys live under /user/*. These endpoints accept either the gateway's ADMIN_API_KEY or a Clerk session token of a user on the gateway's admin allowlist (ADMIN_EMAILS / ADMIN_USER_IDS), as Authorization: Bearer …. A Clerk user who is not on the list gets 403. Project keys (dvai_…) do not work here. Responses wrap results in data.
# With the gateway's ADMIN_API_KEY (server-side only, never in a browser)
curl https://devai-gateway.onrender.com/admin/me -H "Authorization: Bearer $DEVAI_ADMIN_KEY"
# { "data": { "method": "admin_key" } }
# With a Clerk session token of an allowlisted user (what the dashboard sends)
# { "data": { "method": "clerk", "userId": "user_…", "email": "you@example.com" } }
# Create a project, then an app key for it
curl -X POST https://devai-gateway.onrender.com/admin/projects \
-H "Authorization: Bearer $DEVAI_ADMIN_KEY" -H "Content-Type: application/json" \
-d '{ "name": "My app", "description": "Chat feature" }'
curl -X POST https://devai-gateway.onrender.com/admin/api-keys/project/PROJECT_ID \
-H "Authorization: Bearer $DEVAI_ADMIN_KEY" -H "Content-Type: application/json" \
-d '{ "name": "production" }'
# → data.key is the full dvai_ key. It is returned only once.| Endpoint | Description |
|---|---|
| GET /admin/me | Who is calling: { method: "admin_key" } or { method: "clerk", userId, email }. |
| GET /admin/providers | List providers with key strategy, key count and model count. |
| POST /admin/providers | Add a provider: type (openai, anthropic, google, groq, ollama), name, optional baseUrl, config. |
| PATCH /admin/providers/:id | Update name, baseUrl, status or config (keyStrategy, keyOrder, priority; merged with the existing config). |
| DELETE /admin/providers/:id | Delete a provider. |
| POST /admin/providers/:id/test | Test the provider connection. |
| GET /admin/providers/:id/models | List the provider’s models. |
| POST /admin/providers/:id/models | Add a model (modelId required). |
| DELETE /admin/providers/:id/models/:modelId | Remove a model. |
| POST /admin/providers/:id/sync-models | Fetch the model list from the provider. |
| GET /admin/credentials/provider/:providerId | List a provider’s keys (masked). |
| POST /admin/credentials/provider/:providerId | Add a key: name, apiKey, optional quotaLimit. Stored encrypted (AES-256-GCM). |
| PATCH /admin/credentials/:id | Update name, enabled or status. |
| DELETE /admin/credentials/:id | Delete a key. |
| POST /admin/credentials/:id/test | Test one key and update its status (clears its rate-limit cooldown). |
| GET /admin/projects | List projects. |
| POST /admin/projects | Create a project: name, optional description. |
| PATCH /admin/projects/:id | Update name, description, status or routing (same fields as the devai object). |
| DELETE /admin/projects/:id | Delete a project. |
| GET /admin/api-keys/project/:projectId | List a project’s keys (prefix only). |
| POST /admin/api-keys/project/:projectId | Create a dvai_ key (name required). The full key is returned only in this response. |
| DELETE /admin/api-keys/:id | Revoke a key. |
| GET /admin/analytics/overview | Totals for ?period=today|week|month. |
| GET /admin/analytics/by-provider | Usage per provider (same period parameter). |
| GET /admin/analytics/by-model | Usage per model. |
| GET /admin/analytics/by-project | Usage per project. |
| GET /admin/analytics/recent-requests | Latest requests (?limit=, default 50). |
| GET /admin/audit-logs | Audit log of admin changes. |
Ready to make a request?
Sign in, add your first provider key, create your dvai_ key, and call the endpoint.