API reference
VIABLE API
Every public endpoint with its request fields, response shape, and copy-ready examples, generated from OpenAPI 2.0.0 so it always matches the contract.
- Base URL
- Authentication
Authorization: Bearer $VIABLE_API_KEY- Compatibility
- OpenAI request and response shapes
Introduction
The VIABLE API is a model-first, OpenAI-compatible API operated by VIABLE Lab. One key and one base URL reach chat, multimodal input, published Agents, embeddings, image generation, speech, and transcription across frontier and open-weight models from several providers.
- OpenAI SDKs work unchanged once
base_urlishttps://api.viablelab.org/v1. Gemini models use the same OpenAI-compatible shape; Gemini-native routes are not exposed. - Send a catalog ID or a VIABLE alias in
model. Routing, provider translation, cost estimation, and logging happen behind it. - Every authenticated request lands in your Request Logs with status, tokens, estimated cost, and the logged payloads.
- VETTING (Verification and Evaluation Tool for Targeting Invalid Narrative Generation) has a second model check each chat reply against your guidance or an answer key before it is returned. It was evaluated in a peer-reviewed classroom study.
Your own AI
Let your AI set it up
Start a new task, paste this, and approve the sign-in link it shows you in your own browser. It installs the VIABLE CLI and its Agent Skill, and never sees your password.
- If you don’t code
- Claude Cowork in the Claude desktop app, ChatGPT Work in the ChatGPT app, or Gemini with Task chosen under the prompt box.
- If you build software
- Claude Code, Codex, Google Antigravity, or any agent with a terminal.
Read https://www.viablelab.org/downloads/viable-cli.md and set up VIABLE CLI and its Agent Skill for your environment. Help me sign in securely.Your AI needs to run commands, with Node.js 22+.Setup detailsCan’t run commands? Share the API guide
Authentication and API keys
Every protected request carries a VIABLE API key as a bearer token. Keys belong to a project, are created in the dashboard (or by the CLI), and stay on your server.
RecommendedCreate one key per app or environment, give it only the scopes it needs, and call the API from your backend. Browser and mobile clients should call your server, never the API directly.
Authorization: Bearer $VIABLE_API_KEYon every protected request.X-API-Keyis also accepted.- Scopes:
model:requestfor model endpoints,agents:executefor published Agents,learning:writefor learning data from your app,utils:cryptofor encryption utilities. - Optional limits: requests per minute, daily token cap, monthly cost cap, and an expiration date.
allowedModelsrestricts a key to exact model IDs or aliases.
- Public GET endpoints such as
/v1/modelsand/v1/docs/metadataneed no key. - A missing or invalid key returns 401 with
error.codeinvalid_api_key. - Paused keys can be resumed without rotation. Revoked keys are permanent.
- Blank key limits inherit the account tier; they do not mean unlimited. Token and cost caps are admission checks on observed usage, not a guaranteed spending ceiling.
Errors
Errors use conventional HTTP status codes and an OpenAI-style error object with message, type, code and request_id. Branch on the status, then on code; messages are for people and may change. Each endpoint below lists the statuses it returns.
| Status | What it means, and what to do | Codes you may see |
|---|---|---|
| 400Bad request | A field is missing or invalid, the JSON doesn’t parse, or the model isn’t one this API lists. Fix the request; sending it again unchanged fails the same way. error.message names the field. | invalid_jsoninvalid_requestinvalid_request_structuremissing_messagesmodel_not_listedinvalid_includeinvalid_idempotency_keyprovider_key_not_found |
| 401Unauthorized | No API key, or one that is wrong, revoked or expired. Send Authorization: Bearer <key> with an active key from Operate → API keys. | invalid_api_key |
| 402Payment required | A model runs on your own provider key, and that provider account is out of credit. Add funds with the provider, or change the provider key. Retrying won’t help until then. | provider_insufficient_fundsprovider_keys_exhausted |
| 403Forbidden | The key is valid but may not do this: it lacks the scope, may not use this model or agent, or its project is paused. Give the key the scope or model it needs, or grant it the agent under the agent’s Connect → API & keys. | insufficient_api_key_scopeapi_key_model_not_allowedagent_access_deniedproject_inactive |
| 404Not found | Nothing with that ID or slug that this key can see, or an agent that isn’t published. Check the ID and the key’s project. An agent in another project must grant your key. | agent_not_foundagent_run_not_foundagent_job_not_foundagent_conversation_not_foundasset_not_found |
| 405Method not allowed | The path exists, but not with this HTTP method. Use the method the reference shows for the path. | |
| 409Conflict | An Idempotency-Key was reused for a different request, the same request is still running, or the provider key it needs is paused. For a pending request, retry with the same Idempotency-Key to get its result; for a new request, use a new key. Never resend a turn whose outcome is unknown under a new key. | idempotency_conflictagent_request_pendingagent_request_interruptedprovider_key_paused |
| 410Gone | The model was retired. Choose an available model from /v1/models and update the request; retired models aren’t redirected. | model_retired |
| 413Payload too large | The body, an attachment or an input is over its limit. Send less, or upload the file and send its URL. error.max_bytes gives the limit. | request_body_too_largeagent_input_too_largeattachment_too_large |
| 422Unprocessable | The request was valid, but the agent couldn’t finish it: a step’s setting is broken, its input was unusable, the run reached its limits, or a sub-agent couldn’t be called. Open the run in the agent’s Run history to see which step stopped, then fix the agent or the input before retrying. | invalid_graphagent_expression_erroragent_budget_exceedednothing_heardspeech_text_emptyllm_json_parse_failedsubagent_unavailable |
| 424Failed dependency | The model provider rejected your own provider key. Replace or re-enable the key under Billing → Provider keys. | provider_key_rejected |
| 429Too many requests | This key is over its requests per minute, the key or account is over its spending limit, or the model service is busy. Wait for Retry-After, then retry. A spending limit needs a higher limit or more credit; provider_keys_rotating can be retried at once. | api_key_rate_limit_exceededrate_limit_exceededdaily_token_limit_exceededmonthly_cost_limit_exceededaccount_credit_exceededprovider_rate_limitedprovider_keys_rotating |
| 499Client closed request | Your client disconnected before a non-streaming response finished. You won’t receive it; it shows in Request logs. Allow longer timeouts, or stream. A call that already reached the model is still billed. | client_disconnected |
| 500Server error | Something failed inside VIABLE. Retry once; if it persists, contact us with error.request_id. | internal_error |
| 501Not implemented | A reserved route that isn’t built. Use the supported route the message names, such as /v1/chat/completions. | not_implemented |
| 502Bad gateway | The model service failed or couldn’t be reached, or returned nothing usable. Retry shortly, or choose another model. | provider_errorprovider_endpoint_unreachableagent_empty_answer |
| 503Service unavailable | A run couldn’t be queued, or its saved result couldn’t be read just now. Retry with the same Idempotency-Key. | agent_queue_unavailableagent_replay_unavailable |
| 504Gateway timeout | A model took too long to answer. Retry with the same Idempotency-Key; for long work, use a background job. | provider_timeout |
Limits and retries
Limits protect shared capacity and your budget. Retry carefully: a slow or dropped response does not mean the request failed.
- Rate limits. Each key has a requests-per-minute limit, set per key or inherited from the account tier. A 429 includes
Retry-AfterandX-RateLimit-*headers; wait before retrying. - Body size. Model JSON requests accept up to 32 MiB including base64, other JSON 12 MiB, multipart uploads 26 MiB. Larger bodies return 413
request_body_too_largewitherror.max_bytes, before any provider call. - Streaming.
/v1/chat/completions, agentrunsand agentchat/completionsstream with"stream": true. VETTING returns complete responses, and a background job's result is fetched when it is done. - Retries. A timeout does not prove a request failed. After a lost response, retry an agent run or job with the same
Idempotency-Keyto get its result. A run that started and then failed keeps its error under that key, so send a new key to try again; errors raised before a run starts (validation, auth, limits, a full queue) can be retried with the same key.
Research logging
Every authenticated request is recorded the moment it arrives and updated when it finishes, so studies, pilots, and production traffic can be filtered, replayed, and exported. A few optional headers make that data easy to slice later.
Tagging headers
| Header | Use it for |
|---|---|
X-Platform-ID | The product, course, or study sending the request. Without it, the request's origin host is used, or direct. |
X-Platform-Name | Optional readable name shown next to the platform ID. |
X-Operation-Type | The feature or step, such as generate-hint or grade-answer. Filterable in Request Logs. |
X-Request-ID | Your own correlation ID, for example a session and turn. Searchable as the client request ID. |
What is recorded
- Lifecycle. Each request appears as processing on arrival, then completed, cancelled (client disconnected), or unknown when no completion was recorded.
- Measures. Endpoint, model, API key, project, status, latency, input and output tokens, and estimated cost.
- Content. Request and response text, including prompts and VETTING configuration, kept by default for replay and export. Media sent to Agents and transcription audio are archived privately and referenced, not inlined.
- Never stored. Authorization headers, API keys, tokens, and passwords are always redacted. See the privacy policy.
Analyze
- Filter Request Logs by key, project, platform, operation, model, status, and time, then export complete CSV or JSONL files.
- Agent runs record every internal model call by step, linked to the run trace.
- From a terminal or a coding agent,
viable developer logs statsandlogs exportanswer the same questions without opening the dashboard.
Bring your own provider key
/v1/chat/completionsUse your own OpenAI, Google Gemini, UF NaviGator, OpenRouter, or other OpenAI-compatible account. Requests on a provider key are billed by that provider instead of VIABLE credits; your VIABLE API key, base URL, logs, VETTING, and Agents stay the same.
When to useYou already have a provider account, grant, or negotiated contract, your credits ran out, or you need a model only your own endpoint serves.
- Add keys under Account → Provider keys. A personal provider key applies to the projects you own, like credits (all of them by default, including new ones). A shared one belongs to a single project: its owner, and editors when the owner allows it, manage it.
- One provider key can hold several keys, for example a grant’s ten one-time keys. Each request uses the next available key, so long runs continue as keys run out, hit rate limits, or are revoked.
- Catalog models such as
gpt-6-lunabill a provider key marked "use for catalog models" with no code change; a project’s own provider key wins over the owner’s. "model": "@openrouter/anthropic/claude-sonnet-4.5"calls any model on a provider key; OpenAI-compatible endpoints receive the body unchanged exceptmodel.- Responses carry
X-Viable-Billing: credits | byok | mixedandX-Viable-Provider-Key. API-keyallowedModelsaccepts@<key-name>/*.
Behavior
- Each key has a credit type: pay as you go, one-time credit, or monthly credit with a reset day, each with an optional limit. Used-up, rate-limited and rejected keys are skipped and tried again on their own; nothing is removed automatically.
- Provider-key calls never draw credits unless the provider key is set to use VIABLE credits when its keys run out. API-key monthly cost limits count their estimated spend.
- Request logs record who paid, the key that answered, keys skipped on the way, and a cost source: the provider’s reported amount (OpenRouter), the list price, prices set on the key, or unknown.
- Errors say what to do: 402
provider_keys_exhaustedorprovider_insufficient_funds, 429provider_rate_limited(with Retry-After) orprovider_keys_rotating(retry now), 424provider_key_rejected, 502provider_endpoint_unreachable; routing errors are 400provider_key_not_foundand 409provider_key_paused. - Keys are write-only and bound to their base URL; changing the URL requires the keys again. VIABLE still logs these requests the same way as every other request.
Create a chat completion
model:requestOpenAI-compatible chat across OpenAI, Google, and VIABLE Lab models with one request shape. Set model to a catalog ID or a VIABLE alias; routing, provider translation, and logging happen behind it.
When to useGeneration, analysis, tutoring, classification, and coding tasks that need one model response. Streaming is available here and nowhere else.
Request body application/json
messagesobject[]RequiredA nonempty list of conversation messages. Legacy system-only requests are supported: Gemini translation preserves system instructions and adds a minimal user turn only when no conversation content remains after conversion. Ordinary user/assistant turns are unchanged. Typed image inputs accept public HTTPS image URLs or base64 image data URLs. The gateway downloads URLs with public-network checks, at most three redirects and a shared 20-second download deadline, then sends image bytes using provider-native fields (OpenAI-compatible data URLs, Gemini inlineData or Anthropic base64). No credentials, custom ports, private/local names or IP literals. At most 64 images and 10 MiB decoded combined per chat provider request, including history. Supported MIME types depend on the provider. URLs in plain prompt text are only text; the selected model must support image input.
2 child attributes
rolestringRequiredThe role of the message author.
One of
userassistantsystemcontentstring | object[]Required
modelstringFull model ID or VIABLE alias to use. Defaults to gpt-6-luna. Examples: gpt-6-astra, gpt-6-luna, gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gemini-3.8-flash, gemini-3.1-pro-preview, gemini-2.5-flash, gpt-oss-120b, mistral-small-3.1, llama-3.3-70b-instruct, viable-1. Legacy short aliases remain accepted for existing integrations; new integrations should use full model IDs or VIABLE aliases. Prefix a model with
@<provider-key>/to call it with one of the paying account's provider keys (BYOK), for example@openrouter/anthropic/claude-sonnet-4.5or@lab-openai/gpt-6-luna. The rest of the ID is sent to that provider unchanged. Catalog models also use a provider key automatically when the project owner marked one "use for catalog models" for that provider.max_tokensintegerMaximum number of tokens to generate, passed to the provider unchanged; values above the model's max_output_tokens are rejected by the provider. For Gemini, this limit covers both thinking and final output; omitted reasoning_effort preserves provider default thinking.
max_completion_tokensintegerMaximum completion tokens for compatible reasoning models. The gateway maps max_tokens to this field for GPT-5 family models when needed. For Gemini, this limit includes thinking and final output and takes precedence over max_tokens; omitted reasoning_effort preserves provider default thinking.
reasoning_effortstringReasoning effort for compatible models. Check parameter_support.reasoning_effort_values in /v1/models or /v1/docs/metadata before sending a value.
One of
noneminimallowmediumhighxhighmaxtemperaturenumberDefault1top_pnumberDefault1nintegerDefault1How many chat completion choices to generate. (Passed through)
stopstring | string[]Up to 4 sequences where the API will stop generating. (Passed through)
presence_penaltynumberDefault0frequency_penaltynumberDefault0response_formatobjectAn object specifying the format that the model must output. Use { "type": "json_object" } to enable JSON mode, which ensures the message the model generates is valid JSON. Note: The model needs instructions in the prompt to produce JSON. Gemini maps json_object to native JSON output for ordinary and streaming chat; omitted or text format keeps the provider default.
streambooleanDefaultfalseIf true, returns OpenAI-style server-sent events from /v1/chat/completions. Streaming is supported on the main chat endpoint; vetting endpoints remain non-streaming.
providerstringOptional provider hint for public providers. Most clients should omit this and rely on model-first routing.
One of
openaigeminiuserstringYour identifier for the end user. VIABLE keeps it as a learner of the key's project (the project's logs can be filtered by learner_id). Providers that accept it receive it as sent, so use a pseudonymous id, not a name or email.
Returns 200 · application/json
Errors400401402403404405409410413424429499500501502What each means here
- 400Bad Request. Can occur due to invalid JSON, missing required fields, or attempting unsupported options like streaming. Provider key models can also be malformed (invalid_provider_key_model), unknown here (provider_key_not_found), not enabled for this project (provider_key_not_in_project), or unsupported for this operation (provider_key_unsupported_operation).
- 401Missing, invalid, inactive, or expired VIABLE API key. HTTP 401 with WWW-Authenticate: Bearer realm="VIABLE API".
- 402No key can pay: the key is out of credit or over its limit (provider_insufficient_funds), or every key in the provider key is (provider_keys_exhausted; details give counts and next_available_at). Add funds or keys, raise a limit, or let the provider key fall back to VIABLE credits. details.attempts lists what each key’s provider said.
- 403Forbidden, including provider_model_unavailable when every eligible key refuses the model with 400/401/403 (for example UF/LiteLLM Invalid model name). Ordinary 400 parameter errors do not rotate keys.
- 404No key in the provider key can use this model (provider_model_unavailable).
- 405Method Not Allowed. The HTTP method used is not supported for this endpoint.
- 409The provider key this request needs is paused (provider_key_paused) or has no keys (provider_key_empty).
- 410The requested model is no longer supported by VIABLE Lab. Choose an available ID from /v1/models and update the request explicitly. Requests are never redirected.
- 413Serialized model JSON bodies are limited to 32 MiB, other non-multipart bodies to 12 MiB, and multipart bodies to 26 MiB including framing. These gateway byte limits are independent of model token/context limits. See Request size limits for the endpoint categories. Model providers can also return 413 with the generic service_error/model_provider_error shape, without max_bytes. With a provider key, also provider_request_too_large: the request is larger than the per-minute token limit of every key tried. Shorten the input or lower max_tokens.
- 424The provider rejected the only key in the provider key (provider_key_rejected), or it can no longer be read. Replace it under Provider keys.
- 429Too Many Requests. Rate limit exceeded. Provider keys add provider_rate_limited (every available key is rate-limited by its provider; Retry-After says when one is free) and provider_keys_rotating (more keys are left than one request tries; retry now to reach them, Retry-After: 1). details.attempts lists what each key’s provider said.
- 499Client disconnected before the non-streaming response completed. Normally visible only in Request Logs. A call already sent to the provider still finishes and its actual usage and cost are recorded; a call that never started records zero. Streaming responses already started keep HTTP 200.
- 500Internal Server Error. An unexpected error occurred on the server.
- 501Not Implemented. The requested functionality is not available.
- 502Upstream model service error. Error occurred when communicating with a model provider. With a provider key: the provider failed, or its endpoint could not be reached (provider_endpoint_unreachable) or redirected (provider_endpoint_redirected).
Behavior
- Omit
providerfor ordinary calls; the API resolves it from the model registry. - Check
parameter_supportin/v1/modelsbefore sendingreasoning_effortor sampling parameters; support varies by model. - Streaming starts before logging finishes; logging completes asynchronously after the response.
- If the client disconnects before the call reaches the provider, nothing is sent and nothing is billed. A call already sent is not abandoned: it finishes, and Request Logs record its 499 with the provider’s actual usage and cost.
- Retired model IDs return 410
model_retired. Requests are never silently redirected to another model.
Send images, files, and PDFs
/v1/chat/completionsScope model:requestUse OpenAI-style content arrays to give a model images, files, or PDFs. Provider adapters convert supported parts, including to Gemini inline data.
When to useAsk a model to read a worksheet photo, summarize a PDF, or reason over a small file. Choose a model whose input modalities include image, file, or pdf.
type: "text"withtexttype: "image_url"withimage_url.url, an HTTPS URL or data URItype: "file"or"input_file"withfile.file_datafile.file_datatakes a base64 data URI such asdata:application/pdf;base64,...
Behavior
- Gemini file and PDF input requires base64 data, preferably as a data URI.
- Typed image URLs are downloaded safely by VIABLE and sent as provider-native image bytes. URLs in plain text remain text.
- Chat images share a 10 MiB decoded budget and 20-second download deadline, including history; the selected model must support images.
- Model JSON bodies may be up to 32 MiB including base64 expansion. Larger files belong in your own upload pipeline.
Create a vetted chat completion
model:requestThe chat request you already have, plus a verification pass. A model drafts the reply, a verifier checks it against your hidden guidance, and failed drafts are revised with the verifier feedback until one passes or maxAttempts (default 3) is reached.
When to useTutoring and feedback products where a reply must follow pedagogy, for example guiding a student without giving away the answer. No answer-key setup required.
Request body application/json
messagesobject[]RequiredConversation history. System message can be included here. A system message whose string content starts with enc:: (the
valuereturned by /v1/utils/encrypt) is decrypted by the Worker before use and before logging; prompts encrypted before the dedicated prompt key was introduced keep working.2 child attributes
rolestringRequiredThe role of the message author.
One of
userassistantsystemcontentstring | object[]Required
modelstringFull chat model ID or VIABLE alias. Defaults apply if omitted. See /v1/models for supported chat models.
providerstringOptional provider hint for public providers. Most clients should omit this and rely on model-first routing.
One of
openaigeminimax_tokensintegerDefault1024Drafting token budget. Defaults to 1024 when neither token budget is provided. max_completion_tokens takes precedence. The selected model determines whether reasoning and final output share this budget.
max_completion_tokensintegerMaximum completion tokens for compatible reasoning models. The gateway maps max_tokens to this field for GPT-5 family models when needed. For Gemini, this limit includes thinking and final output and takes precedence over max_tokens; omitted reasoning_effort preserves provider default thinking.
reasoning_effortstringReasoning effort for compatible models. Check parameter_support.reasoning_effort_values in /v1/models or /v1/docs/metadata before sending a value.
One of
noneminimallowmediumhighxhighmaxtemperaturenumberDefault0.7top_pnumberDefault1stopstring | string[]Up to 4 sequences where the API will stop generating. (Passed through)
streambooleanDefaultfalseOmit or set false. This VETTING endpoint rejects stream: true with HTTP 400 streaming_not_supported.
One of
falseuserstringYour pseudonymous end-user identifier for VIABLE learner/log attribution; do not send a name or email. This VETTING route does not forward it to the drafting provider.
vettingConfigobject4 child attributes
systemPromptstringA specific guideline for the internal verification model to check against. If omitted, a default safety/guidance prompt is used. Values prefixed with enc:: (from /v1/utils/encrypt
value) are decrypted by the Worker before use and before logging; prompts encrypted before the dedicated prompt key was introduced keep working.maxAttemptsintegerDefault3Optional integer from 1 to 10. Defaults to 3 when omitted; invalid values return HTTP 400.
verificationModelobject6 child attributes
modelIdstringRequiredFull chat model ID or VIABLE alias. Examples: viable-1, gpt-6-luna, gemini-3.8-flash, gemini-3.1-pro-preview, gemini-2.5-flash, gpt-oss-120b, mistral-small-3.1, llama-3.3-70b-instruct. See /v1/models for the current catalog. Non-chat models are rejected on VETTING endpoints. Prefix a model with
@<provider-key>/to call it with one of the paying account's provider keys (BYOK), for example@openrouter/anthropic/claude-sonnet-4.5or@lab-openai/gpt-6-luna. The rest of the ID is sent to that provider unchanged. Catalog models also use a provider key automatically when the project owner marked one "use for catalog models" for that provider.temperaturenumberSampling temperature. If omitted, server defaults apply (e.g., Chat: 0.7, Verify: 0.1).
maxTokensintegerMaximum token budget for the selected model; it may include reasoning and final output. Equivalent to Chat Completions max_tokens. Defaults to 1024 for chat, 256 for simple verification or 512 for full verification. maxCompletionTokens takes precedence. Model/provider output limits still apply.
topPnumberNucleus sampling parameter.
reasoningEffortstringFormal reasoning effort for this chat or verification model, equivalent to Chat Completions reasoning_effort. Supported values depend on the selected model; gpt-oss-120b accepts low, medium, high. Omission preserves the existing model or endpoint default. The legacy reasoning_effort spelling is also accepted.
One of
noneminimallowmediumhighxhighmaxmaxCompletionTokensintegerMaximum completion-token budget, including reasoning when the model counts it. Equivalent to Chat Completions max_completion_tokens. Takes precedence over maxTokens when both are supplied; forwarded to the selected provider without inflating an explicit value.
requireClosingQuestionbooleanDefaulttrueWhether to append the built-in instruction to conclude every tutor response with a question. Set false to let the supplied tutor prompt decide. Safety instructions remain enabled. Omission preserves the existing behavior.
Returns 200 · application/json
Errors400401402403405409410413424429500502What each means here
- 400Bad Request. Can occur due to invalid JSON, missing required fields, or attempting unsupported options like streaming.
- 401Missing, invalid, inactive, or expired VIABLE API key. HTTP 401 with WWW-Authenticate: Bearer realm="VIABLE API".
- 402No key can pay: the key is out of credit or over its limit (provider_insufficient_funds), or every key in the provider key is (provider_keys_exhausted; details give counts and next_available_at). Add funds or keys, raise a limit, or let the provider key fall back to VIABLE credits. details.attempts lists what each key’s provider said.
- 403Access denied. Request source (Origin/Referer via CORS) might be invalid (temporary check), or insufficient permissions (future).
- 405Method Not Allowed. The HTTP method used is not supported for this endpoint.
- 409The provider key this request needs is paused (provider_key_paused) or has no keys (provider_key_empty).
- 410The requested model is no longer supported by VIABLE Lab. Choose an available ID from /v1/models and update the request explicitly. Requests are never redirected.
- 413Serialized model JSON bodies are limited to 32 MiB, other non-multipart bodies to 12 MiB, and multipart bodies to 26 MiB including framing. These gateway byte limits are independent of model token/context limits. See Request size limits for the endpoint categories. Model providers can also return 413 with the generic service_error/model_provider_error shape, without max_bytes.
- 424The provider rejected the only key in the provider key (provider_key_rejected), or it can no longer be read. Replace it under Provider keys.
- 429Too Many Requests. Rate limit exceeded. Provider keys add provider_rate_limited (every available key is rate-limited by its provider; Retry-After says when one is free) and provider_keys_rotating (more keys are left than one request tries; retry now to reach them, Retry-After: 1). details.attempts lists what each key’s provider said.
- 500Internal Server Error. An unexpected error occurred on the server.
- 502Upstream model service error. Error occurred when communicating with a model provider. With a provider key: the provider failed, or its endpoint could not be reached (provider_endpoint_unreachable) or redirected (provider_endpoint_redirected).
Behavior
- For ordinary non-streaming text chat, keep the JSON body and choices[0].message.content parsing; change only the request URL from /v1/chat/completions to /v1/chat/vetting. Both use model:request, and vettingConfig is optional. SDKs that append /chat/completions need a path override or direct POST.
- This is not every Chat Completions feature: stream: true returns 400; tool calls, response_format, n, and presence/frequency penalties are not forwarded. Verification needs the complete draft.
- Configure the reviewer with
vettingConfig.verificationModel(modelId,maxTokensormaxCompletionTokens,reasoningEffort,temperature,topP). SetvettingConfig.requireClosingQuestion: falseto let your prompt decide when a question helps. These explicit controls match the Agent VETTING node; omitted controls retain the existing defaults. - The response keeps the chat.completion shape and adds
vetting_infowithattemptCount,stopReason, andlastFailureReason. CheckstopReason:MAX_ATTEMPTS_REACHEDreturns the last draft, which did not pass. - Works with any chat model in the registry: OpenAI, Google, or VIABLE Lab.
vettingConfig.systemPromptand system messages acceptenc::values from the Encryption Tool, so browser-facing products do not ship guidance as plain text.
Run a full VETTING verification
model:requestStructured verification against questions and answer keys, with separate chat and verifier models and an auditable record of every attempt.
When to useAssessment and feedback workflows that must not leak the answer key, need per-attempt evidence for review, or compare models for research.
Request body application/json
modestringRequiredOperation mode: 'vetting' enables the verification layer, 'chat' provides standard chat.
One of
vettingchatconfigobjectRequired4 child attributes
maxVerificationAttemptsintegerDefault3Optional integer from 1 to 10. Defaults to 3 when omitted; invalid values return HTTP 400.
chatModelobjectRequired6 child attributes
modelIdstringRequiredFull chat model ID or VIABLE alias. Examples: viable-1, gpt-6-luna, gemini-3.8-flash, gemini-3.1-pro-preview, gemini-2.5-flash, gpt-oss-120b, mistral-small-3.1, llama-3.3-70b-instruct. See /v1/models for the current catalog. Non-chat models are rejected on VETTING endpoints. Prefix a model with
@<provider-key>/to call it with one of the paying account's provider keys (BYOK), for example@openrouter/anthropic/claude-sonnet-4.5or@lab-openai/gpt-6-luna. The rest of the ID is sent to that provider unchanged. Catalog models also use a provider key automatically when the project owner marked one "use for catalog models" for that provider.temperaturenumberSampling temperature. If omitted, server defaults apply (e.g., Chat: 0.7, Verify: 0.1).
maxTokensintegerMaximum token budget for the selected model; it may include reasoning and final output. Equivalent to Chat Completions max_tokens. Defaults to 1024 for chat, 256 for simple verification or 512 for full verification. maxCompletionTokens takes precedence. Model/provider output limits still apply.
topPnumberNucleus sampling parameter.
reasoningEffortstringFormal reasoning effort for this chat or verification model, equivalent to Chat Completions reasoning_effort. Supported values depend on the selected model; gpt-oss-120b accepts low, medium, high. Omission preserves the existing model or endpoint default. The legacy reasoning_effort spelling is also accepted.
One of
noneminimallowmediumhighxhighmaxmaxCompletionTokensintegerMaximum completion-token budget, including reasoning when the model counts it. Equivalent to Chat Completions max_completion_tokens. Takes precedence over maxTokens when both are supplied; forwarded to the selected provider without inflating an explicit value.
verificationModelobject6 child attributes
modelIdstringRequiredFull chat model ID or VIABLE alias. Examples: viable-1, gpt-6-luna, gemini-3.8-flash, gemini-3.1-pro-preview, gemini-2.5-flash, gpt-oss-120b, mistral-small-3.1, llama-3.3-70b-instruct. See /v1/models for the current catalog. Non-chat models are rejected on VETTING endpoints. Prefix a model with
@<provider-key>/to call it with one of the paying account's provider keys (BYOK), for example@openrouter/anthropic/claude-sonnet-4.5or@lab-openai/gpt-6-luna. The rest of the ID is sent to that provider unchanged. Catalog models also use a provider key automatically when the project owner marked one "use for catalog models" for that provider.temperaturenumberSampling temperature. If omitted, server defaults apply (e.g., Chat: 0.7, Verify: 0.1).
maxTokensintegerMaximum token budget for the selected model; it may include reasoning and final output. Equivalent to Chat Completions max_tokens. Defaults to 1024 for chat, 256 for simple verification or 512 for full verification. maxCompletionTokens takes precedence. Model/provider output limits still apply.
topPnumberNucleus sampling parameter.
reasoningEffortstringFormal reasoning effort for this chat or verification model, equivalent to Chat Completions reasoning_effort. Supported values depend on the selected model; gpt-oss-120b accepts low, medium, high. Omission preserves the existing model or endpoint default. The legacy reasoning_effort spelling is also accepted.
One of
noneminimallowmediumhighxhighmaxmaxCompletionTokensintegerMaximum completion-token budget, including reasoning when the model counts it. Equivalent to Chat Completions max_completion_tokens. Takes precedence over maxTokens when both are supplied; forwarded to the selected provider without inflating an explicit value.
requireClosingQuestionbooleanDefaulttrueWhether to append the built-in instruction to conclude every tutor response with a question. Set false to let the supplied tutor prompt decide. Safety instructions remain enabled. Omission preserves the existing behavior.
promptsobjectRequiredContainer for system prompts. The object itself is required in the request, but the fields within are optional.
2 child attributes
chatSystemPromptstringBase system prompt for the chat model. If omitted, a default helpful tutor prompt is used. Values prefixed with enc:: (from /v1/utils/encrypt
value) are decrypted by the Worker before use and before logging; prompts encrypted before the dedicated prompt key was introduced keep working.verificationSystemPromptstringBase system prompt for the verification model. If omitted and mode='vetting', a default evaluator prompt is constructed using the chat prompt and context. Values prefixed with enc:: (from /v1/utils/encrypt
value) are decrypted by the Worker before use and before logging; prompts encrypted before the dedicated prompt key was introduced keep working.
contextobjectContextual information. The 'items' array is required if mode='vetting'.
3 child attributes
sessionIdstringSession identifier.
userIdstringUser identifier.
itemsobject[]Required(Required if mode='vetting') Array of questions and their corresponding answer keys.
2 child attributes
questionobjectRequiredanswerKeyobjectRequired
messagesobject[]RequiredConversation history, ending with the last user message. Client should provide messages with roles 'user' and optionally previous 'assistant' messages. Do not include 'system' role messages here; use the
promptsfield instead.2 child attributes
rolestringRequiredThe role of the message author.
One of
userassistantsystemcontentstring | object[]Required
Returns 200 · application/json
Errors400401402403405409410413424429500502What each means here
- 400Bad Request. Invalid input, missing required fields, or malformed JSON. The response body contains details about the validation errors.
- 401Missing, invalid, inactive, or expired VIABLE API key. HTTP 401 with WWW-Authenticate: Bearer realm="VIABLE API".
- 402No key can pay: the key is out of credit or over its limit (provider_insufficient_funds), or every key in the provider key is (provider_keys_exhausted; details give counts and next_available_at). Add funds or keys, raise a limit, or let the provider key fall back to VIABLE credits. details.attempts lists what each key’s provider said.
- 403Access denied. Request source (Origin/Referer via CORS) might be invalid (temporary check), or insufficient permissions (future).
- 405Method Not Allowed. The HTTP method used is not supported for this endpoint.
- 409The provider key this request needs is paused (provider_key_paused) or has no keys (provider_key_empty).
- 410The requested model is no longer supported by VIABLE Lab. Choose an available ID from /v1/models and update the request explicitly. Requests are never redirected.
- 413Serialized model JSON bodies are limited to 32 MiB, other non-multipart bodies to 12 MiB, and multipart bodies to 26 MiB including framing. These gateway byte limits are independent of model token/context limits. See Request size limits for the endpoint categories. Model providers can also return 413 with the generic service_error/model_provider_error shape, without max_bytes.
- 424The provider rejected the only key in the provider key (provider_key_rejected), or it can no longer be read. Replace it under Provider keys.
- 429Too Many Requests. Rate limit exceeded. Provider keys add provider_rate_limited (every available key is rate-limited by its provider; Retry-After says when one is free) and provider_keys_rotating (more keys are left than one request tries; retry now to reach them, Retry-After: 1). details.attempts lists what each key’s provider said.
- 500Internal Server Error. An unexpected error occurred during processing.
- 502Upstream model service error. Error occurred when communicating with a model provider. With a provider key: the provider failed, or its endpoint could not be reached (provider_endpoint_unreachable) or redirected (provider_endpoint_redirected).
Behavior
- The chat model and the verification model can come from different providers.
- Both model configs accept
maxTokensormaxCompletionTokensand formalreasoningEffort. Setconfig.requireClosingQuestion: falseto remove the built-in closing-question rule while keeping safety guidance. The same explicit controls are available in Agent VETTING JSON. verification.attemptsrecords each draft, whether it passed, and the verifier output.usageseparates chat and verification tokens so you can see what verification costs.prompts.chatSystemPromptandprompts.verificationSystemPromptacceptenc::values.- Use VETTING chat instead when you do not need answer keys or per-attempt detail.
Run a published Agent
agents:executeCall a workflow you built and published in the dashboard. One request can chain models, branch, loop within budgets, generate images, and return text plus assets, behind a stable URL.
When to useOne product turn needs orchestration: classify then respond, decide whether to draw, or run several model steps. If a single model call is enough, use chat completions instead.
POST /v1/agents/{slug}/runs
Path and header parameters
slugstringpathRequiredPublished agent slug.
Idempotency-KeystringheaderOptional stable key for the same logical request. Completed requests replay; mismatched payloads and pending requests return 409. Never change the key to retry an uncertain outcome.
Request body application/json
inputstring | object | object[]Latest user turn for an Agent. Text is retained exactly. Inline image/file data is bounded, archived byte-for-byte in private R2, and represented in research logs by an authenticated asset reference. HTTPS URLs are retained exactly as restricted provenance and are not fetched by VIABLE Lab.
conversation_idstringContinue an existing agent conversation. Omit (or send null) on the first turn, store the returned value, and resend it for later turns from the same end-user session.
messagestringBackward-compatible text-only alias for input. Omit input when using this field.
messagesobject[]With history_mode=client, preserves supported system/developer/user/assistant messages in order; final message must be user. In server mode (the default) without conversation_id (absent or null), two or more messages start a new conversation holding all but the last, which the run answers: a conversation branched from an edited message. The final message must be user; at most 400 messages, each bounded like client history. The returned conversation_id continues it. With conversation_id, explicit server mode takes one message (more return 400), and without an explicit mode legacy last-user extraction remains unchanged. Tools are not supported.
2 child attributes
rolestringRequiredThe role of the message author.
One of
userassistantsystemcontentstring | object[]Required
history_modestringDefault"server"server preserves legacy conversation behavior: steps get the conversation's latest messages, the oldest left out only when a model's context window is full (the step output then has context_trimmed: true); an agent whose Input step sets config.history to "summarize" folds the oldest turns into a running summary stored with the conversation once its history nears the models' context budget. Without conversation_id, two or more messages start a new conversation holding all but the last (see messages). client uses the supplied complete messages transcript; none is a single turn without a conversation. Both client and none retain research logs and usage, and reject conversation_id.
One of
serverclientnonecontextobjectOptional current request data, max 128 KiB and nesting depth 16. Nodes explicitly opt in with includeContext or context templates. Never automatically appended to conversation history; research traces retain snapshots. With user, the turn also keeps it as what the learner had open (workspace in turns.csv): fields VIABLE composes itself are left out, and the largest fields until it fits 32,000 characters.
userstringOptional opaque identifier for the end user, such as your app's student ID. Memory steps keep long-term facts per agent and user; without it, memory follows the server conversation. Each turn is kept with the learner it names in the project's Learners (their id is shown with them, so their data joins back to your records). Do not send names or emails.
sourcesstringHow to list the reply's cited sources in its text: none (default here; read them from output.citations), markdown (a numbered Sources list with links, [n] markers linked), or text ("Sources:" and one line per source with its link).
One of
nonemarkdowntextstreambooleanDefaultfalsetrue streams the run as server-sent events, so callers can show what it is doing while it works.
learner_groupstringOptional app-reported task/group label for this turn. Requires user. Stored as learner_group on the learning turn and available through the learner Group filter and research turns export. Does not change identity, roster cohort, course enrollment, or model prompt/history. Keep it fixed on retries: present values participate in the request fingerprint; absent values preserve earlier fingerprints. Not a verified grade or access rule.
includestring[]Extras the result carries. trace adds result.trace (AgentRunTrace): the run's wall-clock time, each step's status, timing, model, tokens and cost, and each VETTING review round by round, for an app's own research log. Without include the result is unchanged. Any other value, or a value that is not a list, returns 400 invalid_include before anything is recorded, so a corrected retry may reuse its Idempotency-Key. include is not part of the request fingerprint: a run retried with the same key replays its result with or without the trace; a job keeps the first request's choice.
Returns 200 · application/json
Errors400401402403404409410413422424429503504What each means here
- 400Invalid request: invalid_include (include other than ["trace"]), invalid_request_structure (for example several messages with conversation_id, or seeding messages whose last is not the user's), invalid_json, or another validation error.
- 401Missing, invalid, inactive, or expired VIABLE API key. HTTP 401 with WWW-Authenticate: Bearer realm="VIABLE API".
- 402A step’s model runs on the project’s own provider key, and that provider account is out of credit (provider_insufficient_funds), or every key in its pool is (provider_keys_exhausted). Add funds with the provider or change the key; retrying won’t help until then.
- 403The API key may not run this agent or lacks the agents:execute scope.
- 404Agent not found or not published.
- 409The Idempotency-Key was used for a different request (idempotency_conflict), or the same request is still processing or needs recovery (agent_request_pending). Retry with the same key; never submit a new turn for an uncertain outcome.
- 410The requested model is no longer supported by VIABLE Lab. Choose an available ID from /v1/models and update the request explicitly. Requests are never redirected.
- 413Request body exceeds 12 MiB for non-multipart bodies, or 26 MiB including multipart framing. Agent routes retain smaller route-specific limits. This gateway byte limit is separate from the model context window.
- 422The request was valid, but the agent couldn’t finish it: its graph or a step’s setting is broken (invalid_graph, agent_expression_error, vetting_answer_key_missing), a step’s input was unusable (nothing_heard, speech_text_empty, transcribe_audio_invalid, llm_json_parse_failed), the run reached its limits (agent_budget_exceeded, agent_state_too_large), or a sub-agent couldn’t be called (subagent_unavailable, subagent_disabled, subagent_cycle, subagent_depth_exceeded). The run history shows the step; fix the agent or the input before retrying.
- 424The model provider rejected the project’s own provider key for a step’s model (provider_key_rejected). Replace or re-enable the key; retrying won’t help until then.
- 429Too many requests for this key (rate_limit_exceeded), the key or account is out of its spending limit (quota_error), or the model service is busy. Respect Retry-After when present.
- 503The run could not be queued or its saved result could not be read just now (agent_queue_unavailable, agent_replay_unavailable). Retry with the same Idempotency-Key.
- 504A model step timed out (provider_timeout). The run history keeps what happened; retry with the same Idempotency-Key.
GET /v1/agents/{slug}/runs/{run_id}
Path and header parameters
slugstringpathRequiredAgent slug.
run_idstringpathRequiredThe run_id a run or job result returned.
Returns 200 · application/json
Errors401403404429What each means here
- 401Missing, invalid, inactive, or expired VIABLE API key. HTTP 401 with WWW-Authenticate: Bearer realm="VIABLE API".
- 403The API key lacks the agents:execute scope.
- 404run_not_found: no run with this id was made by this key through runs, jobs or chat completions for this agent.
- 429agent_poll_rate_exceeded: the key read jobs and runs more than AGENT_JOB_POLL_RATE_PER_MINUTE (6,000) times this minute. Honor Retry-After.
POST /v1/agents/{slug}/runs/{run_id}/rating
Path and header parameters
slugstringpathRequiredAgent slug.
run_idstringpathRequiredThe run_id a run or job result returned.
Request body application/json
ratingintegerRequired1 helpful, -1 not helpful, null to take a rating back.
One of
1-1
Returns 200 · application/json
Errors400401403404429What each means here
- 400invalid_rating: rating is missing or not 1, -1 or null. invalid_json: the body isn’t JSON.
- 401Missing, invalid, inactive, or expired VIABLE API key. HTTP 401 with WWW-Authenticate: Bearer realm="VIABLE API".
- 403insufficient_api_key_scope: the key lacks agents:execute.
- 404run_not_found: no run with this id was made by this key through runs, jobs or chat completions for this agent.
- 429api_key_rate_limit_exceeded or rate_limit_exceeded. Honor Retry-After.
Behavior
- Store the returned
conversation_idper end-user thread and agent; resend it to continue. It is bound to the key or user that created it. history_mode: "client"sends the fullmessagestranscript instead;"none"runs one independent turn. Neither acceptsconversation_id.- To start a server conversation from an existing thread, send
messages(two or more, the last from the user) withoutconversation_id: the earlier messages become the new conversation’s history, the last one runs, and the response carries the newconversation_idfor later turns. Withconversation_id, send one message. contextis current-request JSON for steps that opt in. It is retained in research traces, not added to future chat history.- Send
user(an opaque ID such as your student ID, at most 128 characters) when the agent remembers people across conversations; memory steps keep facts per agent and user. - When an Ask user step ends the turn,
output.awaiting_inputis true and the reply is a question; send the answer with the sameconversation_idto continue. - For image input, send
input_textplusinput_imageparts; the receiving LLM step needs an image-capable model. - Inline images are limited to 8 MiB each and 10 MiB per request. Exact bytes are archived privately; logs and traces keep authenticated asset references, not base64.
run_ididentifies one execution. Request logs show the run and every internal model call, tagged by agent, run, and step.- Send
include: ["trace"]to gettracewith the result (jobs: inresult): the run’sstarted_at,completed_atandduration_ms(wall-clock; don’t add up steps, branches overlap), andstepsin order:step_number,node_id,node_type,status(completedorfailed),duration_ms,model,tokens,cost,error.trace.vettinghas one entry per VETTING step:outcome,stop_reason,attemptsandrounds, each withattempt,source(draft: an answer given to review;generated: written by the reply model),verdict(pass,fail,safety,error), the reviewer’sfeedbackand thereplyit reviewed. Otherincludevalues return 400invalid_includebefore anything is recorded;includeis not part of the idempotency fingerprint. Without it the response is unchanged. GET /v1/agents/{slug}/runs/{run_id}reads a run afterwards with the key that made it through runs, jobs or chat completions:status, timing,output,usage(total),costand itstrace. Runs made another way, or by another key, return 404run_not_found. Reads share the job-polling budget.POST /v1/agents/{slug}/runs/{run_id}/ratingkeeps a learner’s rating of the reply (1,-1, ornullto take it back) with its turn, for a run the same key made; others return 404run_not_found. Withuser, a run also keeps itscontextas the turn’sworkspacein the research export.- The same graph is also callable as
POST /v1/agents/{slug}/chat/completions, or as a background job at/v1/agents/{slug}/jobswith anIdempotency-Key. - Explicit platform model-name refusals get at most two short retries inside the existing call budget. Other 400s do not; BYOK rotates its keys. Exhausted calls return an error, not assistant content, and never restart image steps.
- Pin endpoint, body and request key in each app receipt. Completed results replay their attachments without regenerating images; pending work must not switch between
/runsand/jobs. Unknown execution still requires recovery. - A deadline stops new paid/external work. Bounded deterministic steps can finalize a completed image; any explicit first-review failure fallback must use fixed text, preserve unverified status and exclude content rejections.
Integration formats
Use this agent in your app
All supported endpoints are available after publishing. The selection below changes the example only, not the agent.
https://api.viablelab.org/v1/agents/my-agent/runsUse a server-side API key with agents:execute. Never include it in browser code.
Send the latest message as input. Save the returned conversation_id per user and thread; include it on subsequent turns.
When the agent answers from a knowledge base, output.citations lists the sources its reply cites as [1], [2]… (one per page or file, with title, link and snippet), so your app can show where an answer comes from. To have them listed in output.text instead, send sources: "markdown" or "text".
If the Reply step returns result data, read it as output.data: what the run decided or found (a label, a score, a handoff and its reasons) for your app or system to act on. It is never part of the reply text.
Send user with a stable ID for the person in your app (up to 128 characters; avoid emails or names). Memory steps remember facts per user across conversations; without it, memory lasts only for one conversation.
/runs keeps this HTTP request open until the reply is ready. Read output.text. Start here for ordinary request/response integration; no polling is needed.
curl 'https://api.viablelab.org/v1/agents/my-agent/runs' \
-H "Authorization: Bearer $VIABLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "Hello!",
"history_mode": "server",
"user": "student-042"
}'Show progress and stream the reply
Add "stream": true to get server-sent events while the agent works: run.progress with a stage (thinking, listening, searching, reading, checking, creating, writing, speaking) as the kind of work changes, run.delta with the reply as it is written, then run.completed with the same body as without streaming. Stages never name the agent’s steps, so you can show them to your users.
The reply streams when it goes straight from a model step to the Reply step. A reply checked first (by a VETTING step, say) arrives whole in run.completed, after the stages. Without stream, nothing changes: existing integrations keep working as they are.
const response = await fetch("https://api.viablelab.org/v1/agents/my-agent/runs", {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.VIABLE_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ input: 'Hello!', user: 'student-042', stream: true })
})
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader()
let buffer = ''
for (;;) {
const { value, done } = await reader.read()
if (done) break
buffer += value
const events = buffer.split('\n\n')
buffer = events.pop()
for (const block of events) {
const name = block.match(/^event: (.+)$/m)?.[1]
const data = JSON.parse(block.match(/^data: (.+)$/m)?.[1] ?? 'null')
if (name === 'run.progress') showStage(data.stage) // thinking, searching, reading, checking, creating, writing
if (name === 'run.delta') appendToReply(data.text) // the reply as it is written, when it streams
if (name === 'run.completed') showReply(data.output) // final: replaces what streamed
if (name === 'run.failed') showError(data.error)
}
}Next turn & reference data
{
"input": "Tell me more.",
"conversation_id": "agtc_REPLACE_WITH_RETURNED_ID",
"user": "student-042",
"context": {
"current_document": "The latest version only"
}
}When the workflow asks the user something (an Ask user step), the reply is that question and output.awaiting_input is true. Send the user’s answer as the next turn with the same conversation_id; the workflow continues after the question.
Use history_mode: "none" for independent requests. Use history_mode: "client" with messages to manage the full history yourself; omit conversation_id.
context accepts your own JSON fields. Turn on Reference data (under What it reads) on each LLM step that needs those fields, or reference them explicitly in its prompt. Send the latest snapshot each time; it is retained in research traces, not automatically added to future chat history. This does not remove older material already stored in a conversation.
Retries, limits and safe integration
Keep API keys on your server. A shared key's rate limit is shared by all its users. Blank key limits inherit the account tier; they do not mean unlimited.
For each logical turn, generate an Idempotency-Key (8–128 letters, digits, dots, underscores, colons or hyphens). Reuse it with the same body after a lost response. Never create a new key to bypass an uncertain pending request.
429 is a rejection with bounded provider retries, not a promise of background completion. Respect Retry-After. A 409 pending response requires retrying the original key or checking logs; a timeout does not prove the provider did no work.
How background jobs and queues work
Background jobs are an optional delivery mode for Native Agent, not a third conversation format. View the background job example above for its URL and code. Your server submits → the platform queues and runs the workflow → your server retrieves the result.
A 202 response includes id and status_url. GET that URL with the same API key every few seconds, respecting Retry-After. A completed job includes the native response in result.
States: queued → running → completed, failed, or recovery_required. Queue waiting expires after 15 minutes; execution still has a separate deadline. Keep one turn outstanding per conversation. Recovery-required means the result is uncertain—do not automatically submit the workflow again.
The platform manages the queue; application developers do not need a Cloudflare account or queue credentials. Neither synchronous endpoint automatically switches to it. It does not bypass key quotas or turn every provider 429 into an eventual success. A 503 before admission means the capability is unavailable; do not switch execution routes after an uncertain response.
Send learning data from your app
learning:writeRecord what your learners do, write and score in your app’s own screens, so it joins their learner record in Teach → Learners and the research export beside their agent runs.
When to useYour app has its own UI and sign-in and calls agents from its server: send its writing-process events, drafts and hand-ins, and scores with the same `user` as its runs.
POST /v1/learning/events
Path and header parameters
X-Platform-IDstringheaderRequiredYour app’s stable ID, such as writing-app. Required: the activity is stored under it, and Build → Apps counts what it sent.
Request body application/json
userstringRequiredYour app’s stable, opaque ID for the person (at most 128 characters, no control characters): the learner a run with this user is. Never a name or an email.
activitystringRequiredYour app’s ID for the task. Stored as the activity app:<X-Platform-ID>:<activity>.
learner_groupstringOptional task group, such as "Grade 8", kept on each event (learner_group in workspace_events.csv), as on a run’s turn. Not a verified grade or enrollment.
sessionstringOptional ID of the learner’s visit in your app, to tell visits apart.
eventsobject[]Required5 child attributes
typestringRequiredA course workspace event kind, by its full name, or app.event for your app’s own. Each kind keeps only its own fields from data (the research export’s data dictionary lists them, under workspace_events.csv); others are dropped. writing.progress: field (plan or draft), keys, deleted, pasted, pastes, pasted_from_ai, pauses, pause_ms, longest_pause_ms, p_bursts, r_bursts, burst_chars, inside_edits, leading_edits, active_ms, visible_ms, words, chars. writing.snapshot: field, text (20,000 characters), words. writing.submit: words, chars. panel.view: visible_ms, active_ms. questions.answer: prompt, response, correct, score, attempt, rt_ms and more. Never send keystrokes.
One of
panel.viewpage.layoutwriting.startwriting.progresswriting.snapshotwriting.rubricwriting.rubric_checkwriting.submitembed.eventembed.statereading.pagereading.highlightreading.notereading.searchquestions.viewquestions.hintquestions.answerapp.eventatstringWhen it happened, on your app’s clock (client_at in the export).
seqintegerIts order in the session.
itemstringWhat it concerns, such as a question’s ID.
dataobjectThe event’s fields. For app.event, a flat object of up to 20 keys (letters, numbers, _ . -; 40 characters) whose values are numbers, booleans or strings of up to 200 characters; nested values are dropped.
Returns 200 · application/json
Errors400401403413429What each means here
- 400platform_id_required: no X-Platform-ID, or a malformed one. invalid_learning_request: user, activity, learner_group, session or events (1–200) is missing or malformed. invalid_json: the body isn’t JSON.
- 401Missing, invalid, inactive, or expired VIABLE API key. HTTP 401 with WWW-Authenticate: Bearer realm="VIABLE API".
- 403insufficient_api_key_scope: the key lacks the learning:write scope. project_archived, project_suspended or project_inactive: the key’s project is not active.
- 413request_too_large: the body exceeds 262,144 bytes.
- 429api_key_rate_limit_exceeded (the key’s requests per minute) or rate_limit_exceeded (the address’s). Honor Retry-After.
POST /v1/learning/drafts
Path and header parameters
X-Platform-IDstringheaderRequiredYour app’s stable ID, such as writing-app. Required: the activity is stored under it, and Build → Apps counts what it sent.
Request body application/json
userstringRequiredYour app’s stable, opaque ID for the person (at most 128 characters, no control characters): the learner a run with this user is. Never a name or an email.
activitystringRequiredYour app’s ID for the task. Stored as the activity app:<X-Platform-ID>:<activity>.
planstringThe plan as it is now. Each save replaces the last: a save without plan leaves none.
draftstringRequiredThe text as it is now (the whole text, each time).
submitbooleanDefaultfalsetrue hands it in now: submitted_at in writing.csv. A later save keeps the hand-in time.
Returns 200 · application/json
Errors400401403413429What each means here
- 400platform_id_required: no X-Platform-ID, or a malformed one. invalid_learning_request: user, activity, draft (at most 20,000 characters), plan or submit is missing or malformed. invalid_json: the body isn’t JSON.
- 401Missing, invalid, inactive, or expired VIABLE API key. HTTP 401 with WWW-Authenticate: Bearer realm="VIABLE API".
- 403insufficient_api_key_scope: the key lacks the learning:write scope. project_archived, project_suspended or project_inactive: the key’s project is not active.
- 413request_too_large: the body exceeds 262,144 bytes.
- 429api_key_rate_limit_exceeded (the key’s requests per minute) or rate_limit_exceeded (the address’s). Honor Retry-After.
POST /v1/learning/outcomes
Request body application/json
measurestringRequiredThe instrument, such as rubric or posttest (letters, numbers, spaces, . _ - :). Sending a measure again replaces each learner’s value.
rowsobject[]Required3 child attributes
userstringRequiredYour app’s stable, opaque ID for the person (at most 128 characters, no control characters): the learner a run with this user is. Never a name or an email.
valuenumber | stringRequiredA number, or text of up to 500 characters.
recorded_onstringWhen it was measured (YYYY-MM-DD).
Returns 200 · application/json
Errors400401403413429What each means here
- 400invalid_outcome: the measure name is malformed, or rows is not a list of 1–1,000 rows. invalid_json: the body isn’t JSON.
- 401Missing, invalid, inactive, or expired VIABLE API key. HTTP 401 with WWW-Authenticate: Bearer realm="VIABLE API".
- 403insufficient_api_key_scope: the key lacks the learning:write scope. project_archived, project_suspended or project_inactive: the key’s project is not active.
- 413request_too_large: the body exceeds 262,144 bytes.
- 429api_key_rate_limit_exceeded (the key’s requests per minute) or rate_limit_exceeded (the address’s). Honor Retry-After.
Behavior
useris the learner a run with the sameuseris; events and drafts create the learner on first sight. Keep it opaque: never a name or an email.- Events and drafts need
X-Platform-ID(400platform_id_requiredwithout it): they belong to the activityapp:<X-Platform-ID>:<activity>. - Events: up to 200 per call in a 256 KB body.
typeis a VIABLE event kind (writing.progress,writing.snapshot,writing.submit,panel.view,questions.answer…) orapp.eventwith a flatdataobject; each kind keeps only its own fields, and skipped events come back by index inrejections. - Drafts: the whole
draft(andplan) as it is now, up to 20,000 characters each;submit: truehands it in. One draft per learner and activity. - Scores: one
measurefor up to 1,000 rows; users no run, event or draft has named yet return inunmatched. Sending a measure again replaces each learner’s value. - Ratings of replies go to
POST /v1/agents/{slug}/runs/{run_id}/rating(Agent runs). None of these spend credits. App data is never part of a study.
Create embeddings
model:requestOpenAI-compatible vectors for retrieval, semantic search, clustering, and reranking.
When to useIndex course material, match questions to resources, or build retrieval for a tutor.
Request body application/json
modelstringDefault"nomic-embed-text-v1.5"Prefix a model with
@<provider-key>/to call it with one of the paying account's provider keys (BYOK), for example@openrouter/anthropic/claude-sonnet-4.5or@lab-openai/gpt-6-luna. The rest of the ID is sent to that provider unchanged. Catalog models also use a provider key automatically when the project owner marked one "use for catalog models" for that provider.inputstring | string[]Requiredencoding_formatstringDefault"float"One of
floatbase64dimensionsintegerOptional dimensions if supported by the selected model.
Returns 200 · application/json
Errors400401402403404409410413424429500502What each means here
- 400Invalid request. Provider key models can also be malformed (invalid_provider_key_model), unknown here (provider_key_not_found), not enabled for this project (provider_key_not_in_project), or unsupported for this operation (provider_key_unsupported_operation).
- 401Missing, invalid, inactive, or expired VIABLE API key. HTTP 401 with WWW-Authenticate: Bearer realm="VIABLE API".
- 402No key can pay: the key is out of credit or over its limit (provider_insufficient_funds), or every key in the provider key is (provider_keys_exhausted; details give counts and next_available_at). Add funds or keys, raise a limit, or let the provider key fall back to VIABLE credits. details.attempts lists what each key’s provider said.
- 403Forbidden, including provider_model_unavailable when every eligible key refuses the model with 400/401/403 (for example UF/LiteLLM Invalid model name). Ordinary 400 parameter errors do not rotate keys.
- 404No key in the provider key can use this model (provider_model_unavailable).
- 409The provider key this request needs is paused (provider_key_paused) or has no keys (provider_key_empty).
- 410The requested model is no longer supported by VIABLE Lab. Choose an available ID from /v1/models and update the request explicitly. Requests are never redirected.
- 413Serialized model JSON bodies are limited to 32 MiB, other non-multipart bodies to 12 MiB, and multipart bodies to 26 MiB including framing. These gateway byte limits are independent of model token/context limits. See Request size limits for the endpoint categories. Model providers can also return 413 with the generic service_error/model_provider_error shape, without max_bytes.
- 424The provider rejected the only key in the provider key (provider_key_rejected), or it can no longer be read. Replace it under Provider keys.
- 429Too many requests. Provider keys add provider_rate_limited (every available key is rate-limited by its provider; Retry-After says when one is free) and provider_keys_rotating (more keys are left than one request tries; retry now to reach them, Retry-After: 1). details.attempts lists what each key’s provider said.
- 500Server error.
- 502With a provider key: the provider failed, or its endpoint could not be reached (provider_endpoint_unreachable) or redirected (provider_endpoint_redirected).
Behavior
- The default model is
nomic-embed-text-v1.5, hosted by VIABLE Lab. - Cost is logged when provider usage and registry pricing are available.
Generate and edit images
model:requestGenerate images with FLUX, GPT Image, or Gemini; edit source images with connections that support editing.
When to useIllustrations, diagrams, and visual prompts for learning materials.
Request body application/json
modelstringDefault"flux.2-klein"Use an image_generation model from GET /v1/models. Supports flux.2-klein, Gemini 3.1 Flash/Lite Image, and GPT Image models; chat models cannot be used here.
promptstringRequirednintegerDefault1Integer 1–10 for Flux and GPT Image; Gemini accepts only 1. No automatic fan-out or retries.
sizestringGPT Image: provider-supported WIDTHxHEIGHT or auto. Gemini: 0.5K, 1K, 2K, 4K (Lite: 1K only), or square pixel equivalents. The 1024x1536 and 1536x1024 presets request 1K at 2:3 and 3:2, not exact pixel dimensions. See parameter_support.size_values in the catalog.
qualitystringGPT Image: auto/low/medium/high; GPT Image 2.5 also accepts xhigh/max. Gemini does not accept quality. Flux uses provider-supported values.
response_formatstringDefault"url"urlreturns an API-hosted HTTPS asset URL.b64_jsonreturns inline base64 to the caller; logs store only a redacted size summary plus an asset preview URL when asset storage is available.One of
urlb64_jsonaspect_ratiostringGemini only. See parameter_support.aspect_ratio_values. Defaults to 1:1; cannot be combined with portrait/landscape pixel presets. Use a resolution tier with this field.
output_formatstringGPT Image and Flux provider output format. Gemini returns its native image MIME type and does not accept this field.
One of
pngjpegwebpstreambooleanOnly non-streaming generation is supported. partial_images is rejected.
One of
falseoperationstringDefaults to generate without images and edit when images are supplied. Explicit edit requires images; explicit generate with images is rejected. Unsupported edits fail before provider execution without fallback.
One of
generateeditimagesobject[]Actual edit/reference images. GPT Image: up to 16; Gemini: up to 14. All sources and the optional mask share a 10 MiB decoded budget, including downloaded URLs. Presence implies editing unless operation=generate, which is rejected. See capabilities.image_editing and parameter_support.images_max in /v1/models.
1 child attribute
image_urlstringRequiredPublic HTTPS image URL or PNG/JPEG/WebP base64 data URL. The gateway downloads remote sources and masks with public-network, redirect, timeout and size checks and sends actual image bytes to the provider. No credentials, custom ports, private/local names or IP literals. Sources and mask together must be at most 10 MiB decoded, including downloaded bytes. Provider file IDs are not accepted.
maskobject1 child attribute
image_urlstringRequiredPublic HTTPS image URL or PNG/JPEG/WebP base64 data URL. The gateway downloads remote sources and masks with public-network, redirect, timeout and size checks and sends actual image bytes to the provider. No credentials, custom ports, private/local names or IP literals. Sources and mask together must be at most 10 MiB decoded, including downloaded bytes. Provider file IDs are not accepted.
Returns 200 · application/json
Errors400401402409410413424429500502What each means here
- 400Invalid request, missing edit source, unsupported editing connection, invalid source URL/data, source too large, or unsupported model controls. No generation fallback is performed.
- 401Missing, invalid, inactive, or expired VIABLE API key. HTTP 401 with WWW-Authenticate: Bearer realm="VIABLE API".
- 402No key can pay: the key is out of credit or over its limit (provider_insufficient_funds), or every key in the provider key is (provider_keys_exhausted; details give counts and next_available_at). Add funds or keys, raise a limit, or let the provider key fall back to VIABLE credits. details.attempts lists what each key’s provider said.
- 409The provider key this request needs is paused (provider_key_paused) or has no keys (provider_key_empty).
- 410The requested model is no longer supported by VIABLE Lab. Choose an available ID from /v1/models and update the request explicitly. Requests are never redirected.
- 413Serialized model JSON bodies are limited to 32 MiB, other non-multipart bodies to 12 MiB, and multipart bodies to 26 MiB including framing. These gateway byte limits are independent of model token/context limits. See Request size limits for the endpoint categories. Model providers can also return 413 with the generic service_error/model_provider_error shape, without max_bytes.
- 424The provider rejected the only key in the provider key (provider_key_rejected), or it can no longer be read. Replace it under Provider keys.
- 429Too many requests. Provider keys add provider_rate_limited (every available key is rate-limited by its provider; Retry-After says when one is free) and provider_keys_rotating (more keys are left than one request tries; retry now to reach them, Retry-After: 1). details.attempts lists what each key’s provider said.
- 500Server error.
- 502Upstream provider failure, empty/blocked image output, or unusable provider-local URL. Returns model_provider_error or image_provider_non_public_url. With a provider key: the provider failed, or its endpoint could not be reached (provider_endpoint_unreachable) or redirected (provider_endpoint_redirected).
Behavior
urlresponses are API-hosted HTTPS assets;b64_jsonreturns inline data and logs keep only a redacted summary.- Parameter support differs by model family. Read
parameter_supportin/v1/modelsfor sizes, quality, and aspect ratios. - GPT Image and Gemini costs come from reported token usage; FLUX uses a per-image estimate.
- For edits, send operation="edit" and images=[{image_url:"https://…"}]. Check capabilities.image_editing; missing sources or unsupported models return 400 without redrawing.
- GPT Image also accepts a PNG mask. Gemini accepts source images but not masks. Edits guide preservation without guaranteeing unchanged pixels.
Speech and transcription
model:requestTurn text into speech, or transcribe uploaded audio, with OpenAI-compatible request shapes.
When to useRead-aloud support, spoken feedback, and transcribing recorded explanations or interviews.
POST /v1/audio/speech
Request body application/json
inputstringRequiredThe text to generate audio for. Maximum length: 4096 characters.
modelstringDefault"kokoro"Speech model. Defaults to kokoro. Retired IDs return 410 model_retired without redirection.
One of
kokorovoicestringKokoro voice ID; defaults to af_heart. Read voice_values from /v1/models for all 54 choices.
instructionsstringKokoro does not support instructions; non-empty values return 400.
response_formatstringDefault"mp3"The audio output format. Inspect parameter_support.response_format_values because formats vary by model.
One of
mp3opusflacwavpcmspeednumberDefault1The speed of the generated audio (0.25 to 4.0).
Returns 200 · audio/mpeg
Binary content. The format follows the request's response_format.
Errors400401402403405409410413424429500502What each means here
- 400Bad Request. The request payload is malformed, contains invalid parameters, or failed validation.
- 401Missing, invalid, inactive, or expired VIABLE API key. HTTP 401 with WWW-Authenticate: Bearer realm="VIABLE API".
- 402No key can pay: the key is out of credit or over its limit (provider_insufficient_funds), or every key in the provider key is (provider_keys_exhausted; details give counts and next_available_at). Add funds or keys, raise a limit, or let the provider key fall back to VIABLE credits. details.attempts lists what each key’s provider said.
- 403Access denied. Request source (Origin/Referer via CORS) might be invalid (temporary check), or insufficient permissions (future).
- 405Method Not Allowed. The HTTP method used is not supported for this endpoint.
- 409The provider key this request needs is paused (provider_key_paused) or has no keys (provider_key_empty).
- 410The requested model is no longer supported by VIABLE Lab. Choose an available ID from /v1/models and update the request explicitly. Requests are never redirected.
- 413Serialized model JSON bodies are limited to 32 MiB, other non-multipart bodies to 12 MiB, and multipart bodies to 26 MiB including framing. These gateway byte limits are independent of model token/context limits. See Request size limits for the endpoint categories. Model providers can also return 413 with the generic service_error/model_provider_error shape, without max_bytes.
- 424The provider rejected the only key in the provider key (provider_key_rejected), or it can no longer be read. Replace it under Provider keys.
- 429Too Many Requests. Rate limit exceeded. Provider keys add provider_rate_limited (every available key is rate-limited by its provider; Retry-After says when one is free) and provider_keys_rotating (more keys are left than one request tries; retry now to reach them, Retry-After: 1). details.attempts lists what each key’s provider said.
- 500Internal Server Error. An unexpected error occurred on the server.
- 502Upstream model service error. Error occurred when communicating with a model provider. With a provider key: the provider failed, or its endpoint could not be reached (provider_endpoint_unreachable) or redirected (provider_endpoint_redirected).
POST /v1/audio/transcriptions
Request body multipart/form-data
filefileRequiredAudio file upload, bounded by the 26 MiB multipart request limit. For attributed requests the exact bytes are archived as a private research asset.
modelstringDefault"whisper-large-v3"Transcription model. Defaults to whisper-large-v3. Retired IDs return 410 model_retired.
One of
whisper-large-v3gpt-transcribelanguagestringOptional ISO-639-1 code for whisper-large-v3. For gpt-transcribe use languages[]; singular language returns 400 invalid_transcription_language.
promptstringresponse_formatstringDefault"json"gpt-transcribe supports non-streaming json through this gateway; other formats or stream=true return 400 unsupported_transcription_format. whisper-large-v3 supports every listed format.
One of
jsontextsrtverbose_jsonvtttemperaturenumberduration_secondsnumberOptional client duration for cost estimates when provider duration is unavailable. Provider duration takes precedence.
duration_minutesnumberOptional client duration for cost estimates when provider duration is unavailable. Provider duration takes precedence.
languages[]string[]Optional language hints for gpt-transcribe. Repeat languages[] multipart fields for multiple ISO-639-1 codes.
keywords[]string[]Optional keyword hints for gpt-transcribe. Repeat keywords[] multipart fields. Each keyword must be one line without angle brackets.
streambooleanDefaultfalseKeep false or omit. gpt-transcribe stream=true is unsupported by this gateway and returns 400.
Returns 200 · application/json
Errors400401402409410413424429500502What each means here
- 400Invalid request.
- 401Missing, invalid, inactive, or expired VIABLE API key. HTTP 401 with WWW-Authenticate: Bearer realm="VIABLE API".
- 402No key can pay: the key is out of credit or over its limit (provider_insufficient_funds), or every key in the provider key is (provider_keys_exhausted; details give counts and next_available_at). Add funds or keys, raise a limit, or let the provider key fall back to VIABLE credits. details.attempts lists what each key’s provider said.
- 409The provider key this request needs is paused (provider_key_paused) or has no keys (provider_key_empty).
- 410The requested model is no longer supported by VIABLE Lab. Choose an available ID from /v1/models and update the request explicitly. Requests are never redirected.
- 413Serialized model JSON bodies are limited to 32 MiB, other non-multipart bodies to 12 MiB, and multipart bodies to 26 MiB including framing. These gateway byte limits are independent of model token/context limits. See Request size limits for the endpoint categories. Model providers can also return 413 with the generic service_error/model_provider_error shape, without max_bytes.
- 424The provider rejected the only key in the provider key (provider_key_rejected), or it can no longer be read. Replace it under Provider keys.
- 429Too many requests. Provider keys add provider_rate_limited (every available key is rate-limited by its provider; Retry-After says when one is free) and provider_keys_rotating (more keys are left than one request tries; retry now to reach them, Retry-After: 1). details.attempts lists what each key’s provider said.
- 500Server error.
- 502With a provider key: the provider failed, or its endpoint could not be reached (provider_endpoint_unreachable) or redirected (provider_endpoint_redirected).
Behavior
- Speech defaults to
kokorowith 54 voices;af_heartis the default voice. - Transcription uses multipart/form-data, not JSON.
whisper-large-v3is the default model and returnsjson(default),text,srt,verbose_jsonorvtt. - Audio is priced per minute of the duration the model reports; the JSON transcript includes it as
usage.seconds.duration_secondsis only a fallback when no duration is reported.
Encrypt and decrypt prompt values
utils:cryptoEncrypt prompt text for VETTING fields that accept enc:: values, so guidance is not shipped to a browser as plain text. Prefix the returned encryptedText with enc:: before using it.
When to useA browser-facing VETTING integration has to carry system guidance in client code. Calling the API from your own backend keeps guidance off the client entirely and is the stronger option.
POST /v1/utils/encrypt
Request body application/json
textstringRequiredMust contain non-whitespace characters.
Returns 200 · application/json
Errors400401403413429500What each means here
- 400Invalid JSON, missing/empty field.
- 401Missing, invalid, inactive, or expired VIABLE API key. HTTP 401 with WWW-Authenticate: Bearer realm="VIABLE API".
- 403Missing utils:crypto scope.
- 413Serialized model JSON bodies are limited to 32 MiB, other non-multipart bodies to 12 MiB, and multipart bodies to 26 MiB including framing. These gateway byte limits are independent of model token/context limits. See Request size limits for the endpoint categories. Model providers can also return 413 with the generic service_error/model_provider_error shape, without max_bytes.
- 429API key quota/rate limit exceeded.
- 500Encryption key unavailable or encryption failed.
POST /v1/utils/decrypt
Request body application/json
encryptedTextstringRequiredMust contain non-whitespace characters. A leading enc:: prefix is accepted.
Returns 200 · application/json
Errors400401403413429500What each means here
- 400Invalid JSON, missing/empty field or decryption failed.
- 401Missing, invalid, inactive, or expired VIABLE API key. HTTP 401 with WWW-Authenticate: Bearer realm="VIABLE API".
- 403Missing utils:crypto scope.
- 413Serialized model JSON bodies are limited to 32 MiB, other non-multipart bodies to 12 MiB, and multipart bodies to 26 MiB including framing. These gateway byte limits are independent of model token/context limits. See Request size limits for the endpoint categories. Model providers can also return 413 with the generic service_error/model_provider_error shape, without max_bytes.
- 429API key quota/rate limit exceeded.
- 500Encryption key unavailable.
Behavior
- Automatic
enc::handling applies to documented VETTING prompt fields only, not every endpoint. - Supported
enc::fields are decrypted before logging, so research exports contain the prompt the model actually used. - The dashboard Encryption Tool is a convenience UI over these endpoints.
List models
The live catalog: IDs, modes, providers, modalities, context windows, parameter support, and pricing notes. No key required.
When to usePopulate a model picker, check which parameters a model accepts, or keep pricing in sync without hardcoding.
Returns 200 · application/json
Behavior
GET /v1/docs/metadatareturns the catalog plus endpoints and quickstarts in one compact payload for portals and agents.- VIABLE aliases (
viable-1,viable-2,viable-3) are stable names whose underlying models are managed by VIABLE Lab.
Machine-readable files
For code generators, CI checks, coding agents, and AI search. All public and requiring no key.
