Speko Docs
RouterRouting

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, balanced by 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.voice field 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/v1

Copy 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

FieldValueEffect
languageBCP 47 tag, e.g. es, es-PRWhich board ranks candidates, and TTS accent narrowing. See languages and accents.
objectivelatency, cost, quality, balancedWhich axis the ranking optimizes.
useCasephone_agent, realtime_agent, transcription, voice_content, translation, other, nullNames 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.
maxPricePerMinUsdNumber or nullDrops 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.
denyUp to 16 providers or model idsNever route to these. X-Speko-Deny on a request replaces this list rather than adding to it.
stt / llm / tts{ "chain": [...] }, max 4Element 0 is the pin; the rest are the failover order.
tts.voiceVoice idUsed when a request omits voice. A voice id belongs to one provider, so setting it restricts TTS to that provider.
tts.instructionsPlain-language directionUsed 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 boundarymaxPricePerMinUsd 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.

On this page