Skip to main content
Each project stores its own set of model-provider API keys (OpenAI, Anthropic, Google, OpenRouter). Keys are encrypted at rest and decrypted only when an agent, workflow, or indexing job needs them. Stored keys are returned masked; the full secret is never echoed back. When you set a key, the platform calls the provider to validate it. Soft failures (provider down, transient network issue) still store the key with is_valid: false; hard failures (provider rejects the credential) reject the request without storing anything.

Model-string format

A stored key is selected by the provider prefix of the model string you pass to an agent, indexing job, or other consumer. Powabase routes models through LiteLLM, so these are LiteLLM model IDs.
ProviderFormatExample
openaibare IDgpt-4o
anthropicbare IDclaude-sonnet-4-6
googlegemini/<model>gemini/gemini-2.5-pro
openrouteropenrouter/<org>/<model>openrouter/qwen/qwen3-235b-a22b-2507
OpenRouter slugs must match LiteLLM’s openrouter/... cost-map keys, which can differ from OpenRouter’s own slugs. For a full walkthrough, including DeepSeek and the function-calling requirement for agents, see Bring your own LLM.

Common Patterns

Configure keys once after creating a project, then let agents and indexing jobs pick them up automatically. Use the batch PUT endpoint when wiring keys from a setup script. Use the /validate endpoint to test a key before storing it (e.g. in an admin UI).

GET /api/ai-provider-keys

List all configured provider keys. Each entry includes id, provider, masked_key (the only representation of the secret the API ever returns), is_valid, last_validated_at, created_at, and updated_at.
response = requests.get(f"{BASE_URL}/api/ai-provider-keys", headers=headers)
const res = await fetch(`${BASE_URL}/api/ai-provider-keys`, { headers });
curl '{BASE_URL}/api/ai-provider-keys' -H "apikey: {API_KEY}" -H "Authorization: Bearer {API_KEY}"

POST /api/ai-provider-keys

Upsert a single provider key. Returns 201 on insert, 200 on update.
provider
string
required
One of: openai, anthropic, google, openrouter.
api_key
string
required
The provider’s raw API key. Validated against the provider before storage.
{ "provider": "openai", "api_key": "sk-..." }
requests.post(f"{BASE_URL}/api/ai-provider-keys", headers=headers, json={"provider": "openai", "api_key": "sk-..."})
await fetch(`${BASE_URL}/api/ai-provider-keys`, { method: "POST", headers, body: JSON.stringify({ provider: "openai", api_key: "sk-..." }) });
curl -X POST '{BASE_URL}/api/ai-provider-keys' -H "apikey: {API_KEY}" -H "Authorization: Bearer {API_KEY}" -H "Content-Type: application/json" -d '{"provider": "openai", "api_key": "sk-..."}'

PUT /api/ai-provider-keys

Batch upsert. Pass any subset of providers. Null or empty values are no-ops (won’t clear existing keys; use DELETE for that). On any hard-fail validation, the entire batch is rolled back.
{
  "openai": "sk-...",
  "anthropic": "sk-ant-...",
  "google": null,
  "openrouter": ""
}
requests.put(f"{BASE_URL}/api/ai-provider-keys", headers=headers, json={"openai": "sk-...", "anthropic": "sk-ant-..."})
await fetch(`${BASE_URL}/api/ai-provider-keys`, { method: "PUT", headers, body: JSON.stringify({ openai: "sk-...", anthropic: "sk-ant-..." }) });
curl -X PUT '{BASE_URL}/api/ai-provider-keys' -H "apikey: {API_KEY}" -H "Authorization: Bearer {API_KEY}" -H "Content-Type: application/json" -d '{"openai": "sk-...", "anthropic": "sk-ant-..."}'

DELETE /api/ai-provider-keys/

Remove a stored key. Returns 204 with no body.
provider
string
required
One of: openai, anthropic, google, openrouter.
requests.delete(f"{BASE_URL}/api/ai-provider-keys/openai", headers=headers)
await fetch(`${BASE_URL}/api/ai-provider-keys/openai`, { method: "DELETE", headers });
curl -X DELETE '{BASE_URL}/api/ai-provider-keys/openai' -H "apikey: {API_KEY}" -H "Authorization: Bearer {API_KEY}"

POST /api/ai-provider-keys/validate

Validate a key against the provider without storing it. Returns { "is_valid": true } on success, or { "is_valid": false, "error": "..." } with the provider’s rejection reason.
provider
string
required
One of: openai, anthropic, google, openrouter.
api_key
string
required
The key to test.
{ "provider": "openai", "api_key": "sk-..." }
result = requests.post(f"{BASE_URL}/api/ai-provider-keys/validate", headers=headers, json={"provider": "openai", "api_key": "sk-..."}).json()
if not result["is_valid"]:
    print("Rejected:", result.get("error"))
const res = await fetch(`${BASE_URL}/api/ai-provider-keys/validate`, { method: "POST", headers, body: JSON.stringify({ provider: "openai", api_key: "sk-..." }) });
curl -X POST '{BASE_URL}/api/ai-provider-keys/validate' -H "apikey: {API_KEY}" -H "Authorization: Bearer {API_KEY}" -H "Content-Type: application/json" -d '{"provider": "openai", "api_key": "sk-..."}'

GET /api/ai-provider-keys/platform_supported

Returns which providers the platform itself has keys for at this pod: the “AI-on-us” providers, where the platform pays for inference and bills you in credits instead of requiring your own key. The Studio’s LLM Provider Keys settings page uses this to render “AI-on-us active” vs “BYOK required” badges per provider. It’s also a pre-flight check before pointing an agent at a model whose provider you haven’t BYOK’d yet: if the provider is platform_supported, the model works; otherwise the agent run returns 402 provider_key_decrypt_failed until you upsert your own key.
The path uses an underscore (platform_supported), not a hyphen. Hyphenating it 404s.
response = requests.get(f"{BASE_URL}/api/ai-provider-keys/platform_supported", headers=headers)
print(response.json())  # {"providers": ["openai", "anthropic"]}
const res = await fetch(`${BASE_URL}/api/ai-provider-keys/platform_supported`, { headers });
const { providers } = await res.json();
curl '{BASE_URL}/api/ai-provider-keys/platform_supported' -H "apikey: {API_KEY}" -H "Authorization: Bearer {API_KEY}"
Response:
{ "providers": ["openai", "anthropic"] }
The list is a subset of ["openai", "anthropic", "google", "openrouter"]: whichever ones the pod has a platform-side env-var key configured for.

Error Responses

Errors return {"error": "<message>"}; validation failures additionally include a fields map with the per-provider rejection reason.
StatusDescription
400provider is not one of the four supported values
400Provider rejected the key (hard fail). Response body: {"error": "Validation failed", "fields": {"<provider>": "<reason>"}}
401Missing or invalid auth headers