AGENTYK
Developers / API

Build on Agentyk

One OpenAI-compatible API for the whole AgentykLM lineup (chat, transcription, embeddings, and reranking), reachable with a single agk_ key. Hosted in the EU, prompts not stored, live docs that can’t drift.

Quickstart

If you already use the OpenAI SDK, you are three lines away: change the base URL, drop in your agk_ key, pick a model.

curl https://api.agentyk.xyz/v1/chat/completions \
  -H "Authorization: Bearer $AGENTYK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "jaguar",
    "messages": [
      { "role": "user", "content": "Summarise the EU AI Act in three points." }
    ]
  }'

Model ids are illustrative; call GET /v1/models or browse the live reference for the current lineup.

The surface

A focused, OpenAI-compatible /v1 surface plus self-serve account and key management.

POST

/v1/chat/completions

Chat & reasoning across the AgentykLM tiers. OpenAI-compatible request and response, including streaming and tool calls, plus a GBNF grammar field and strict JSON schema output when a reply has to be guaranteed, not requested.

POST

/v1/audio/transcriptions

Speech-to-text with Agentyk Scribe (agentyk-scribe-1.0, or agentyk-scribe-urdu-1.0 for Urdu; its language detection favours Urdu, so use agentyk-scribe-1.0 to identify languages). Drop in any audio format; hosted in the EU, and the audio is not kept after transcription. Optional language controls: allowed_languages, on_language_mismatch and include=language_candidates. €0.04 per hour of audio, pro-rated to the second, with a 10-second minimum billed length.

WSS

/v1/realtime?model=agentyk-scribe-live-1.0

Live transcription with Agentyk Scribe Live. Stream audio over a WebSocket and read the text while it is being spoken; the transcript at the end matches batch accuracy. Add translation to the start message for live captions in up to five languages (see Live translation below).

POST

/v1/decisions

Agentyk Decision: yes/no, multiple-choice and score questions about a text, answered in one fast pass with a probability per option and no text generated. An account add-on, enabled on request.

POST

/v1/embeddings

1024-dimension text embeddings (agentyk-embedding-1.0) for search and retrieval.

POST

/v1/rerank

Cross-encoder reranking (agentyk-reranker-1.0) to sharpen retrieval results.

GET

/v1/models

List the available models and their capabilities: the source of truth for model ids.

POST

/api/keys

Programmatically manage your agk_ API keys, plus credits, usage and subscriptions.

Why build on Agentyk

Drop-in

OpenAI-compatible

Point any OpenAI SDK at https://api.agentyk.xyz/v1 and change the model name. Existing chat, embeddings, and transcription code just works.

One key

A single agk_ key

One key reaches every surface: every chat tier, transcription, embeddings, and reranking. No per-model credentials to juggle.

Private

Hosted in the EU, prompts not stored

Hosted in the EU, and our language, speech and voice models run on our own servers in the Netherlands. Chat prompts, answers and transcription audio are not stored: we keep usage records (model, token counts, timing). Knowledge keeps the documents you add until you delete them, and batch speaker results are kept for 7 days unless you delete them sooner. We don't use your data for training.

Always-correct

Generated, live docs

The Scalar reference at agentyk.xyz/docs renders the API's own generated OpenAPI spec, so the documentation can never drift from the implementation.

Making the output a guarantee

Most of the time you do not want the model's opinion about the format; you want a value from a fixed set, and you want it every time. Three mechanisms can narrow what comes back, and they are not interchangeable. Only one of them is a guarantee.

Prompt instructions

Guarantees: Nothing.

The model usually complies. “Usually” is not a guarantee, and the failures are the interesting ones.

response_format: json_object

Guarantees: The reply parses as JSON.

Not which keys. The same prompt can return cleaned_text one day and {language, text} the next.

response_format: json_schema + strict

Guarantees: Keys, nesting, types, array length, and any enum you declare.

Not the characters inside a plain "type": "string". That field accepts every string there is.

grammar (GBNF)

Guarantees: Exactly the language you wrote, character for character.

Nothing outside the grammar can be emitted: no near-miss, no code fence, no apology.

Grammar-constrained decoding

/v1/chat/completions accepts a grammarfield holding a GBNF grammar, and passes it to the model untouched. The grammar is applied while the answer is being generated: at every step, tokens that would take the output outside your grammar are removed from consideration. So the reply cannot be outside it. This is the only mechanism here that turns “please answer with one of these three labels” into something you never have to validate again.

It is worth being blunt about the trade: this is a llama.cpp extension, not an OpenAI-compatible parameter. Your SDK will not have a field for it, and that is expected: send it in the raw request body (extra_body in the OpenAI Python SDK, an extra key in the JSON body anywhere else). Everything else about the request and the response stays OpenAI-shaped.

tickets.gbnf
# Bind each record's id to its OWN rule, so the label can only land
# on the record it belongs to.
root     ::= "[" rec-1041 "," rec-1042 "]"
rec-1041 ::= "{\"id\":\"T-1041\",\"label\":" label "}"
rec-1042 ::= "{\"id\":\"T-1042\",\"label\":" label "}"
label    ::= "\"billing\"" | "\"bug\"" | "\"feature-request\""
sending it
GRAMMAR = open("tickets.gbnf").read()

resp = client.chat.completions.create(
    model="jaguar-express",
    max_tokens=80,
    messages=[{"role": "user", "content":
        "T-1041: 'I was charged twice this month.' "
        "T-1042: 'The export button does nothing.' Label each ticket."}],
    extra_body={"grammar": GRAMMAR},   # not an OpenAI field: send it raw
)

# [{"id":"T-1041","label":"billing"},{"id":"T-1042","label":"bug"}]
#
# There is no other possible response. The set of strings the model is
# allowed to emit is the set you wrote, and it is checked token by token.

Two traps, each of which costs a whole run

trap 1: constrain the pair, not the vocabulary

The instinct is to list every permitted string once, as one global alternation, and let the model fill in the rest. That grammar is satisfied by a permitted string in the wrong record: it guarantees that each value exists in your vocabulary, not that it was attached to the right thing. If two fields have to agree with each other, the constraint has to bind the pair.

# TRAP. Every string here is permitted, so every PAIRING is permitted too.
root   ::= "[" record ("," record)* "]"
record ::= "{\"id\":" id ",\"label\":" label "}"
id     ::= "\"T-1041\"" | "\"T-1042\""
label  ::= "\"billing\"" | "\"bug\"" | "\"feature-request\""

# This grammar happily accepts all of the following:
#   [{"id":"T-1041","label":"bug"},{"id":"T-1041","label":"bug"}]   same id twice
#   [{"id":"T-1042","label":"billing"},{"id":"T-1041","label":"bug"}]   swapped
#
# A global list of permitted strings guarantees only that each string is
# permitted somewhere. If two fields have to agree, the rule that emits one
# must be the rule that emits the other.
trap 2: rule names take no underscores

GBNF rule names accept letters, digits and hyphens only. A single underscore fails the entire grammar, and the request comes back as a bare 400 whose body reports an upstream error without mentioning the grammar at all. There is nothing in the response to point you at the real cause, so it is worth knowing before you meet it: if a grammar that looks correct returns 400, check the rule names first.

root ::= yes_or_no          # ✗ underscore
yes_or_no ::= "yes" | "no"  #   -> HTTP 400, and the body does not mention the grammar

root ::= yes-or-no          # ✓ letters, digits and hyphens only
yes-or-no ::= "yes" | "no"

Structured output, and the thing it does not do

response_format accepts json_schema with strict: true, not only json_object. It is the right tool when you need a predictable object to deserialise, and the wrong tool, on its own, when you need exact string content.

"response_format": {
  "type": "json_schema",
  "json_schema": {
    "name": "ticket",
    "strict": true,
    "schema": {
      "type": "object",
      "properties": {
        "id":    { "type": "string" },
        "label": { "type": "string" }
      },
      "required": ["id", "label"],
      "additionalProperties": false
    }
  }
}

// Returns, reliably:   { "id": "T-1041", "label": "Billing Issue" }
//
// Both keys present, both strings, nothing extra: the schema did its job.
// And "Billing Issue" is not one of your three labels, because you never
// told the schema what the labels were. Declaring
//     "label": { "type": "string", "enum": ["billing","bug","feature-request"] }
// changes that answer to "billing", because an enum becomes a real
// constraint. A bare "type": "string" never is.
the caveat that matters

A strict schema constrains the shape of a field and says nothing about the characters inside it. Every key you declared will be there, correctly typed, with nothing extra, and a field declared "type": "string" will accept any string in the world, including a plausible-looking wrong one. On a task that needs exact string content, a strict schema scores the same as plain JSON mode. If the value must come from a fixed set, say so with enum, which is enforced, or drop to a grammar.

counts and lengths

Array bounds do hold: under "maxItems": 3 you get three elements. But a bound on the count is not a bound on the content: asked for forty items under a limit of three, the reply came back with three elements and the other thirty comma-separated inside the third string. If the number of items is part of what you are relying on, write the repetition into a grammar, where the count and the shape of each item are constrained together.

Defaults worth knowing

We set no sampling parameters on your behalf. A request that sends none is served with the tier's own built-in defaults, and those are tuned for open-ended generation rather than for repeatability.

sampling

Send no sampling parameters and the express tier runs at temperature 1.0, top-k 64, top-p 0.95, min-p 0.05. That is a creative default, not a sober one: the same prompt sent twice returns two different answers, which surprises people who expected near-determinism out of the box. temperature, top_p, top_k and seed all pass through and are honoured, so ask for what you need: temperature: 0 with a fixed seed returns the same answer run after run.

reasoning

Reasoning is off by default, so you are not paying for hidden thinking tokens you did not ask for. Turn it on per request with "chat_template_kwargs": { "enable_thinking": true } and the reply carries a reasoning_content field alongside the usual content; set it explicitly and your setting is respected rather than overridden. The OpenAI reasoning_effort field is accepted but currently has no effect on this tier; a request carrying only reasoning_effort is still served with reasoning off, so use enable_thinking as the switch.

max_tokens

Set one. There is no small implicit cap standing in for you, and a prompt that invites a long answer will be given one: we have watched a single request run to twelve thousand output tokens for a caller who set no limit. An explicit max_tokens bounds both your bill and your latency, and it is a hard requirement rather than a nicety if you use constrained tool calls, below.

errors

Errors come back as JSON { "error": "<message>" } with the HTTP status: 401 for a missing or invalid key, 403 when the product is not in your plan, 429 for a rate, quota or capacity limit, and 502/503/504 for a temporary problem that is safe to retry. Honour Retry-After when it is present. One known gap: a transcription refused because the service is at capacity currently answers 429 with an empty message and no Retry-After, so retry it after a short back-off of one to five seconds. Failed requests are not billed. The API reference lists the responses of every endpoint.

Constrained tool calls, and the two headers that tell you the truth

Set agentyk_strict_tools: true on a non-streaming call that forces exactly one function, and the function's parameter schema (enum and pattern included) is enforced on the arguments while they are generated, instead of being a suggestion the model usually follows.

resp = client.chat.completions.with_raw_response.create(
    model="jaguar-express",
    max_tokens=2500,         # ~1.5x the longest output your prompt can produce
    messages=messages,
    tools=tools,             # exactly one function
    tool_choice={"type": "function", "function": {"name": "classify"}},
    extra_body={"agentyk_strict_tools": True},
)

mode = resp.headers.get("X-Agentyk-Strict-Tools")         # enforced | fallback
why  = resp.headers.get("X-Agentyk-Strict-Tools-Reason")  # only when fallback
if mode != "enforced":
    logger.warning("strict tools fell back: %s", why)

completion = resp.parse()
why the headers exist

Enforcement works by constraining the output to your schema. If that generation hits the token cap, the output is a valid JSON prefix and nothing more, so we fall back to an unconstrained attempt rather than fail your request.

The fallback usually still returns well-formed output. From your side nothing looks wrong: you get a tool call with sensible arguments, and the token count you receive belongs to the second attempt, so it always appears comfortably under your cap. A client-side histogram of completion tokens cannot find this. These two response headers are the only way to detect it.

Log X-Agentyk-Strict-Tools on every call and alert when fallback rises above a small threshold of your traffic; a rising rate almost always means max_tokens is now straddling your output length distribution, and raising it restores the guarantee. Read them server-side: they are not exposed to browser JavaScript.

enforced

(none)

The constraint held. The arguments match your schema.

fallback

max_tokens_too_small

The constrained attempt was cut off mid-JSON. Raise max_tokens. This is the actionable one, and the common one.

fallback

empty_content

The constrained attempt produced no JSON.

fallback

invalid_json

The constrained attempt produced something that was not JSON.

fallback

upstream_error / unavailable

A backend error, or no capacity for the constrained attempt.

fallback

no_forced_tool_call

The request does not qualify: it needs exactly one function and a tool_choice that forces that function.

fallback

unsupported_stream

stream: true. The constrained path is non-streaming only, so opting in on a stream does nothing.

What usage costs

Plans are flat monthly; metered usage on the API is billed per unit on top. These are the published rates; GET /api/credits/rates returns the same card, complete and machine-readable, for every tier.

Transcription

€0.04 per hour of audio

Pro-rated to the second and rounded once over the whole request. Minimum billed length 10 seconds: a shorter clip is charged as 10 seconds. A request whose duration cannot be measured is not charged at all, and is never lifted to the minimum.

Express language tier

€0.15 / €0.60 per million tokens

€0.15 per million input tokens, €0.60 per million output. Cached input is billed at half the input rate: €0.075 per million. Tokens are counted from the usage block of your own response.

Volume discounts apply automatically on month-to-date spend, and live transcription sessions are not metered per clip; see plans and pricing.

Live translation on Scribe Live: €0.002 per audio minute per target language, on top of the session; see Live translation.

Live transcription

Agentyk Scribe Live streams text back while the audio is still being spoken. Open one WebSocket per speaker, send raw PCM at real time, and read partials within about half a second; opt in to refine and the transcript you hold at the end is the same one a batch request would have returned.

import asyncio, json, wave, websockets

URL = "wss://api.agentyk.xyz/v1/realtime?model=agentyk-scribe-live-1.0"

async def main():
    async with websockets.connect(URL, additional_headers={"Authorization": "Bearer agk_..."}) as ws:
        await ws.send(json.dumps({"type": "start", "language": "de", "refine": True}))

        async def read():
            async for m in ws:
                f = json.loads(m)
                if f["type"] == "partial":        print("...", f["text"])   # provisional; replaces the previous partial
                elif f["type"] == "final":        print("   ", f["text"])   # committed
                elif f["type"] == "transcript":   print("== ", f["text"])   # the whole session, batch accuracy
                elif f["type"] in ("done", "error", "busy"):
                    print(f); return

        reader = asyncio.create_task(read())
        w = wave.open("meeting.wav", "rb")           # 16 kHz, mono, 16-bit PCM
        while frames := w.readframes(1600):          # 100 ms per frame, sent at real time
            await ws.send(frames)
            await asyncio.sleep(0.1)
        await ws.send(json.dumps({"type": "stop"}))
        await reader

asyncio.run(main())
you send
  • start: language (ISO 639-1 or auto), refine, sample_rate, format, channels, diarize (speaker labels), final_labels (with diarize; default false: an end-of-session re-check of every speaker turn, see speakers below)
  • binary: raw PCM frames, 20–100 ms each, at real time
  • stop: flush everything and end the session
you receive
  • ready: session open; echoes the accepted settings
  • partial: provisional text, half a second behind the speaker; replaces the previous partial
  • final: committed text, appended to the utterance
  • utterance: with refine: the finished utterance re-decoded with full context, replacing its finals
  • transcript: with refine, at stop: the whole session re-decoded for batch accuracy
  • done: audio_ms consumed, utterance count, why the session ended, speakers when labels were on, and final_pass (whether the re-check was delivered)
  • speaker: with diarize: a speaker number on partials and finals (finals split where the speaker changes), turns on utterance and transcript; up to eight speakers
  • speakers: with diarize and final_labels: true, at stop: every speaker turn of the session re-checked with full context (pass: final, supersedes: all), up to eight speakers. Expect stop to take longer (typically well under a minute). Apply it over the labels you hold; the text is unchanged
  • busy / error: capacity reached (retry after retry_after_ms) / an actionable error code

Audio is 16-bit or float PCM, mono or stereo, 8–48 kHz; declare it once in start. Sessions run up to eight hours and end on their own, transcript included, after thirty minutes without speech; keep sending silence during breaks, it costs nothing. Twenty-five European languages are covered by default; other languages, Persian among them, work with an explicit language code. Same agk_ key and the same Scribe plan as batch transcription, but metered per session rather than per clip: usage is counted per audio second and the 10-second minimum billed length that applies to a batch request does not apply here.

Live translation

Live translated captions from the same Scribe Live session that gives you the live transcript. Open one WebSocket, stream audio, and receive the transcript and, for every finished sentence, its translation into up to 5 of 24 languages. There is no second call to make. Available to every account with Scribe Live, at €0.002 per audio minute per target language.

€0.002 per audio minute per languageup to 5 of 24 languagesevery Scribe Live account

Quick start

Connect as for live transcription and add translation to the start message. Then send audio and stop as usual; every transcript frame is unchanged, and the translations arrive beside them.

{"type":"start","language":"nl","refine":true,"translation":{"target_languages":["en","de"]}}
{"type":"translation","segment_id":3,"source_language":"nl","target_language":"en","text":"Currently, only dragonflies cannot fold their wings.","start":12.4,"end":17.85,"speaker":2}
// Live translated captions from Agentyk Scribe Live.
//
//   npm install ws
//   AGENTYK_API_KEY=agk_... node live-translation.mjs talk.wav nl en,de
//
// Streams a 16 kHz mono 16-bit WAV file at real time and prints the transcript and the
// translations as they arrive. In a browser, use the built-in WebSocket with the subprotocols
// ["agentyk.v1", "agentyk-key." + key] instead of the Authorization header.
import { readFileSync } from "node:fs";
import WebSocket from "ws";

const [file, language = "nl", targets = "en"] = process.argv.slice(2);
const key = process.env.AGENTYK_API_KEY;
if (!file || !key) {
  console.error("usage: AGENTYK_API_KEY=agk_... node live-translation.mjs <file.wav> <language> <target,target>");
  process.exit(2);
}

// Minimal WAV reader: finds the PCM data chunk.
const wav = readFileSync(file);
let pcm = null;
for (let off = 12; off < wav.length;) {
  const id = wav.toString("ascii", off, off + 4);
  const size = wav.readUInt32LE(off + 4);
  if (id === "data") { pcm = wav.subarray(off + 8, off + 8 + size); break; }
  off += 8 + size + (size % 2);
}

const ws = new WebSocket("wss://api.agentyk.xyz/v1/realtime?model=agentyk-scribe-live-1.0", {
  headers: { Authorization: `Bearer ${key}` },
});

ws.on("open", () => {
  ws.send(JSON.stringify({
    type: "start",
    language,
    refine: true,
    sample_rate: 16000,
    translation: { target_languages: targets.split(",") },
  }));
});

function streamAudio() {
  const frameBytes = 3200; // 100 ms at 16 kHz, 16-bit mono
  const t0 = Date.now();
  let sent = 0;
  const tick = () => {
    if (ws.readyState !== WebSocket.OPEN) return;
    if (sent >= pcm.length) { ws.send(JSON.stringify({ type: "stop" })); return; }
    ws.send(pcm.subarray(sent, sent + frameBytes));
    sent += frameBytes;
    setTimeout(tick, Math.max(0, t0 + (sent / 32000) * 1000 - Date.now())); // real time
  };
  tick();
}

ws.on("message", (data, isBinary) => {
  if (isBinary) return;
  const msg = JSON.parse(data.toString());
  switch (msg.type) {
    case "busy":
      console.error(`busy, retry in ${msg.retry_after_ms} ms`);
      break;
    case "ready":
      if (msg.translation === false) console.error("translation is not available for this session");
      streamAudio();
      break;
    case "utterance":
      console.log(`[${msg.id}] ${msg.text}`);
      break;
    case "translation":
      console.log(`[${msg.segment_id}] ${msg.target_language}: ${msg.text}`);
      break;
    case "translation_error":
      console.error(`[${msg.segment_id}] ${msg.target_language}: (${msg.code})`);
      break;
    case "error":
      console.error(`session error: ${msg.code}: ${msg.message}`);
      break;
    case "done":
      console.error(`done: ${(msg.audio_ms / 1000).toFixed(1)} s of audio`);
      break;
  }
});

ws.on("close", () => process.exit(0));

Both examples stream a 16 kHz mono 16-bit WAV file at real time and print the captions. In a browser, pass the key as the subprotocols agentyk.v1 and agentyk-key.agk_... instead of the Authorization header.

The start message

fieldtypenotes
translation.target_languagesarray of strings1 to 5 language codes from the table below, each once. A target may not be the session's own language. Codes are case-insensitive.
languagestringThe language being spoken (ISO 639-1), as for live transcription. With auto translation still works, and source_language in each frame is then auto.
refinebooleanRecommended: each sentence is then translated from its corrected text. Without it, a sentence is translated from its committed final text once the speaker moves on or pauses for 2.5 seconds.

Languages

24 target languages. A session takes up to 5 of them.

enEnglish
nlDutch
deGerman
frFrench
esSpanish
itItalian
plPolish
ptPortuguese
svSwedish
daDanish
fiFinnish
csCzech
roRomanian
hrCroatian
bgBulgarian
trTurkish
ukUkrainian
ruRussian
idIndonesian
zhChinese
jaJapanese
koKorean
arArabic (right to left)
faPersian (right to left)

What you receive

ready carries a translation field when you asked for it:

{"type":"ready","language":"nl","refine":true,"translation":{"target_languages":["en","de"]}}
  • {"target_languages": [...]}: translation is on for this session, into these languages (lower-case, in your order).
  • false: translation is not available for this session. The session runs as plain live transcription; nothing else changes.

translation: one frame per finished segment and target language.

fieldmeaning
segment_idThe utterance it translates: the id of the utterance frame, or the utterance number on the final frames.
source_languageThe session's language (auto when it is detected automatically).
target_languageOne of your targets.
textThe translation. Arabic and Persian are written right to left; render them with the right text direction.
start, endSeconds from the start of the session's audio, as on the transcript frames.
speakerWith speaker labels (diarize), when the whole segment is one speaker.

translation_error: only that segment and that language are affected; the session and the transcript carry on. It is deliberately not an error frame, because an error ends a session.

{"type":"translation_error","segment_id":4,"target_language":"de","code":"timeout","message":"translation took too long for this segment"}
codemeaning
backlogTranslation fell behind, for example many short segments at once while the service is busy. The oldest waiting segment was skipped so the captions stay current.
timeoutThe translation did not finish within 8 seconds.
unavailableTranslation is unavailable for a moment. Transcription carries on.
failedThis segment could not be translated.
emptyThe translation of this segment came back empty.
cancelledThe session ended before this segment was translated (stop waits up to 15 seconds for the last translations).

How it behaves

  • Transcript frames are never held back by translation: partial, final, utterance, transcript and done arrive exactly as without translation, and translations arrive beside them.
  • For each target language, translation and translation_error frames arrive in segment order. Frames of different target languages may interleave.
  • Each translated segment gets exactly one frame per target language: a translation or a translation_error. Filler and repeated segments (below) get none.
  • Translation uses spare capacity and gives way to other work. When the service is busy it can fall behind; once more than 8 segments per target language are waiting, the oldest one is skipped with translation_error code backlog so the captions stay current. Skipped segments are not billed.
  • Each segment is translated with the 3 segments before it as context. Translations are shown once and not revised: a later correction of the transcript (for example the transcript frame at stop) does not re-send them.
  • Segments made only of filler sounds (such as "uh" or "hmm"), or of a caption line that speech recognition sometimes writes for silence, are not translated and not billed. A segment of four or more words that repeats one of the last three is not translated again.
  • At stop the last sentences are translated before the session ends: their frames arrive before done. stop waits up to 15 seconds for them; anything still open then gets translation_error code cancelled.
  • Targets are set once, in start. To change them, start a new session.
  • Translation works on the recognised text, so a recognition error carries into the translation. Very quiet or near-silent audio may produce no text, and therefore no translation.

Measured on the live service on 2026-11-03 (Dutch speech into English, German and French, 309 captions):

  • A translation arrives about 0.4 seconds after its segment is finished (95 percent within 0.7 seconds).
  • From the moment the speaker finishes a sentence to its translated caption: about 2.3 seconds (95 percent within 3 seconds).

Errors

A translation option that cannot be accepted ends the session at once, with an error frame of code bad_request:

whenmessage
translation is not an objecttranslation must be an object: {"target_languages": ["en"]}.
target_languages is missing or emptytranslation.target_languages must be a non-empty list of language codes.
more than 5 target languagestranslation.target_languages takes at most 5 languages.
a code that is not in the language tableunsupported translation target: xx. Available: en, nl, de, fr, es, it, pl, pt, sv, da, fi, cs, ro, hr, bg, tr, uk, ru, id, zh, ja, ko, ar, fa.
a target equal to the session's languagetranslation target nl is the session's own language.
the same target twicetranslation target en is listed twice.

Billing

Live translation costs €0.002 per audio minute per target language (€0.12 per hour per target language), on top of the live transcription of the session itself. It is metered per second.

You pay for the share that was delivered: the session's audio length times the number of target languages, times the share of segment translations that arrived as a translation frame. Translations that failed, timed out or were skipped, and filler segments that were not translated, are not billed. A session that produced no segment costs nothing for translation.

It appears on your usage page and statement as its own line, "Scribe Live translation", in translated audio hours (audio x target languages). Volume discounts on month-to-date spend apply as for other usage.

Worked example

A 10-minute session translated into 2 languages counts 20 translated minutes. If every segment is delivered, that is 20 x €0.002 = €0.04.

Say the session had 100 segments, 2 of them filler (not translated), and 2 translations were skipped while the service was busy. Then 194 of 200 segment translations were delivered, and 1200 x 194 / 200 = 1164 seconds are billed: €0.0388.

Questions

Is the audio kept?

No. Audio, transcripts and translations are processed in memory while your session runs and are not stored. For billing we record usage metadata, such as the time, the model, the audio length and the number of target languages, never the audio or the text.

Are my transcripts or translations used for training?

No. We do not use your data to train models.

Where is it processed?

Hosted in the EU, the AI runs on our own servers in the Netherlands. No outside AI provider sees your audio or text.

Do I need an add-on or a separate key?

No. Every account with Scribe Live can use live translation with its usual agk_ key: add translation to the start message.

Does translation slow down the transcript?

No. Transcript frames are sent the moment they are ready; translations run beside them.

Which languages can be spoken?

Any language Scribe Live transcribes. The target languages are the ones in the table.

Can I translate into more than five languages?

A session takes at most 5 targets. For more, run a second session on the same audio.

Decision: answers about a text, with probabilities

Agentyk Decision reads a text and answers your questions about it: yes or no, a choice from your own options, or a score on your own scale. All questions are answered in one fast pass and no text is generated, so a short message typically comes back in under a second, and every answer carries a probability per option so you set your own thresholds.

curl https://api.agentyk.xyz/v1/decisions \
  -H "Authorization: Bearer $AGENTYK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "agentyk-decision-1.0",
    "input": "The parcel arrived broken and nobody answers my emails.",
    "questions": {
      "complaint": {"type": "yes_no", "question": "Is this a complaint?"},
      "topic": {"type": "choice", "question": "What is it mainly about?",
                "options": [{"key": "delivery"}, {"key": "billing"}, {"key": "product_quality"},
                            {"key": "customer_service", "description": "how staff respond"}]},
      "anger": {"type": "score", "question": "How angry is the writer?",
                "levels": ["calm", "annoyed", "angry", "furious"]}
    }
  }'

yes_no

You send: optional yes and no describe what each answer means

You get: answer (true or false) and probability, the probability of yes

choice

You send: options: 1 to 255 {key, description?} entries with distinct keys, in your order

You get: answer (the most probable key), probabilities per key, confidence

score

You send: levels: 2 to 10 distinct labels, lowest first

You get: answer (the most probable level), probabilities per level, score (the expected position, 0 for the first level), confidence

Send 1 to 16 questions per request, each under an id you choose; every question reads the whole input, and the answers come back under the same ids. The input is text (a string, or a JSON record read as its JSON text), at most 1 MB; images and video are not supported yet. Two models take the same request: agentyk-decision-1.0 (the default) and the faster agentyk-decision-flash-1.0. If one question cannot be answered, its slot carries an error code (input_too_long, invalid_question or model_error) and the others are still answered. Up to four requests per account can be in flight at once; beyond that the API answers 429 with Retry-After.

Pricing: EUR 0.03 per million billed input tokens, output free. usage.billed_tokens is the tokens read over all answered questions, with a minimum of 120 per request, and each billed request costs at least EUR 0.00001. Failed requests cost nothing. Access: Decision is an account add-on, enabled per account on request; without it the endpoint returns 403. Ask us to enable it, and see the API reference for every field and error.

SDKs

On-device inference SDKs

Ship sovereign AI inside your own apps. The Agentyk SDKs run the smaller AgentykLM tiers on-device and fall back to the cloud API when a task needs a bigger model, with the same agk_ key throughout. SDK access is included from the Pro plan up.

Knowledge API

Ground answers you can verify

The Agentyk Knowledge API turns your documents into a verifiable atomic claim graph and answers against it: every response cites the exact source and ships a verification code.

Start building in minutes

Create a key, point your SDK at api.agentyk.xyz, and ship on infrastructure hosted in the EU.