Key policy
A key carries its own routing policy, so callers send no routing headers.
A router key is more than credentials. It carries language, use case, objective, price ceiling and optional pins, so your application sends nothing but a standard OpenAI request body, and your client code has no Speko-specific surface in it at all.
Mint and edit keys at platform.speko.ai/router/keys.
Creating a key
The create page is a sentence, not a form: a language, an optional accent, a use case, and an objective, over the stack the router resolves live for that policy. Every part has a working default - English, phone agent, balanced - so the key can be created immediately; each pick refines the resolved stack in place.
- Use case is one of Phone agent, In-app voice assistant, Transcription, or Narration. Transcription runs STT alone and narration TTS alone, so the console shows only the stages the use case runs; hidden stages keep their saved policy untouched. Picking a use case never changes the objective - it stays what you set,
balancedby default. - Each stage sits on Auto or Pin. Auto follows the live ranking for the key's language and objective; Pin sets an ordered failover chain, up to four entries per stage, picked from the same board the ranking reads. Chains below.
- A default TTS voice can be set as a voice id - the
tts.voicefield below.
Keys holding the older use-case values keep working: the console renders them read-only, as Translation (legacy) for example, and never rewrites them on a save.
The name is derived
There is no name field. The key names itself from the policy:
{language}-{use case}-{objective}
en-phone-balanced, es-mx-phone-latency, en-transcribe-quality - with a -2/-3 suffix when the name is taken. The name appears once, at reveal, and an edit never recomputes it: renaming on a policy change would orphan every log line and code sample carrying the old label. Renaming stays possible through the key management API - PATCH the key with a new name.
The value is shown once
At creation the console reveals the key value with the two lines a client environment needs:
SPEKO_API_KEY=sk_live_...
SPEKO_BASE_URL=https://api.speko.ai/v1Copy it now. Speko will not show it again. The /v1 suffix is part of the base URL - the quickstart shows the failure mode when it is dropped.
After creation the same page is the key's detail. Routing is live-editable there; the value never changes, and a policy edit never renames the key.
Policy fields
| Field | Value | Effect |
|---|---|---|
language | BCP 47 tag, e.g. es, es-PR | Which board ranks candidates, and TTS accent narrowing. See languages and accents. |
objective | latency, cost, quality, balanced | Which axis the ranking optimizes. |
useCase | phone_agent, realtime_agent, transcription, voice_content, translation, other, null | Names the workload and feeds the derived key name. The console's four choices map onto the first four values: Phone agent (phone_agent), In-app voice assistant (realtime_agent), Transcription (transcription), Narration (voice_content). Never changes objective. translation and other are legacy - the console shows them read-only and never rewrites them. |
maxPricePerMinUsd | Number or null | Drops candidates above this. Compared against each stage's own unit — USD/min for STT, /1M output tokens for LLM, /1M characters for TTS. One ceiling across three unlike units rarely means what it looks like; leave it null unless you have measured what it excludes. |
deny | Up to 16 providers or model ids | Never route to these. X-Speko-Deny on a request replaces this list rather than adding to it. |
stt / llm / tts | { "chain": [...] }, max 4 | Element 0 is the pin; the rest are the failover order. |
tts.voice | Voice id | Used when a request omits voice. A voice id belongs to one provider, so setting it restricts TTS to that provider. |
tts.instructions | Plain-language direction | Used when a request omits instructions. Not SSML — see API reference. |
{
"language": "es-PR",
"objective": "latency",
"maxPricePerMinUsd": null,
"deny": [],
"stt": { "chain": ["deepgram:nova-3", "cartesia:ink-2"] },
"llm": { "chain": [] },
"tts": { "chain": [], "voice": null, "instructions": null }
}Chains
Element 0 is the pin — called first. The rest are the failover order, tried in sequence. Four entries maximum per stage.
An empty chain routes by objective on the live board. In the console this is the Auto | Pin control on each stage: Auto keeps the chain empty, Pin fills it from the board.
A pinned chain never picks up a newly added model. The board changes as models
are measured; a key pinned to openai:gpt-4o-mini keeps calling it in 2027
whether or not something better arrived. Pin for a reason — reproducibility, a
compliance constraint, a bug you are chasing — and leave it empty otherwise.
Pin one stage and leave the others empty when only part of the stack is constrained.
Policy is a default, not a cap
Every field is a starting point a request header can override. A key set to objective: "cost" still honours X-Speko-Objective: latency on one call.
That makes it a good default and not a security boundary — maxPricePerMinUsd is not a hard limit, because a request can raise the ceiling with X-Speko-Max-Price. Full precedence is in routing.
Lifecycle
- The value is shown once, at creation. Lose it and you revoke and mint another.
- Ten active keys per account.
- Policy is editable after creation - the key's page doubles as its detail. The value is not editable, and a policy edit never renames the key.
One key per workload rather than one per account: the derived name is the description of that workload, and x-route in your logs tells you what each one actually chose.