Skip to content
youbot
Docs menu

API reference

Create a task

On this page

POST /api/v1/generate

Submit a generation. We validate your key, debit credits for the model, forward the request upstream, and return either an inline result (chat) or a task id to poll (media).

FieldTypeDescription
modelIdstringModel id from the Market (e.g. veo-3-1, nano-banana-pro). The alias model is also accepted.
inputobjectModel-specific parameters — see each model's page for its fields.
callbackUrlstring?Optional https webhook; we POST the finished task to it (see Webhooks).
bash
curl -X POST https://you.bot/api/v1/generate \
  -H "Authorization: Bearer $YOUBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "modelId": "veo-3-1", "input": { "prompt": "a city at dusk", "aspectRatio": "16:9" } }'

Media model response (poll the task id):

json
{ "taskId": "a1b2c3d4e5f6g7h8", "model": "veo-3-1", "creditsCharged": 172.86 }

Chat/LLM model response (inline, no polling):

json
{
  "model": "claude-fable-5",
  "text": "…",
  "creditsCharged": 0.54,
  "usage": { "inputTokens": 5014, "outputTokens": 120, "cachedInputTokens": 4709, "cacheWriteTokens": 0 }
}

usage — the token counts this turn was charged from. All four are always present on a non-streaming chat response, as whole numbers, and they are the figures creditsCharged was computed from, so you can reconcile a bill from the response alone — except a searched turn on a model that lists a per-search price, which also adds that price for each search it ran (see Web search below). A streamed turn does not carry them — its charge is settled after the stream ends, so look the taskId up in your usage log. cachedInputTokens and cacheWriteTokens are parts of inputTokens, not additions to it: the portion billed at the cached rate (cheaper) and at the cache-write rate (dearer) instead of the fresh input rate. They are reported by the model, not requested — there is no field to ask for caching, and a model that reports none returns 0 for both, which is a fact about your invoice rather than a guess about the model. Note that inputTokens counts everything the model read for this turn, which includes system text the platform adds and, on a searched turn, the search results — so it can be much larger than the words you sent. Media models return no usage: a video has no tokens.

json
{
  "model": "claude-fable-5",
  "text": "",
  "creditsCharged": 0.04,
  "usage": { "inputTokens": 97, "outputTokens": 1, "cachedInputTokens": 0, "cacheWriteTokens": 0 },
  "finishReason": "max_tokens"
}

finishReason — the model's own word for why it stopped. It appears on one specific turn: when the reply came back empty and the model itself said it stopped at the max_tokens you set. Both halves are required — an empty reply the model gives no reason for is still an error, because staying silent about it would hide more than it explains. The value is the model's own word, passed through raw and never translated; on the Claude models it reads "max_tokens". Such a turn succeeds with HTTP 200 and an empty text, and is billed for the input tokens you sent, since the model read them before it stopped. So: an empty text carrying finishReason means raise your max_tokens — nothing else is wrong. A truncated (but non-empty) answer does not carry this field; compare usage.outputTokens with the max_tokens you sent to spot that one. And when the model gives back nothing and names no reason, the call is refused with 422 instead, naming the number you sent and saying plainly whether it was charged — it is, at the model's ordinary rates, whenever the model reported token counts for the turn, because it read your input before it stopped. So a max_tokens too small to answer with is always reported to you one way or the other, never as a silent empty success.

maxTokensApplied — when your balance could not hold the max_tokens you sent. Before a chat call we hold enough credits for the longest reply your max_tokens allows, and refund whatever is unused. If your balance cannot hold that amount, we do not refuse the call: we lower max_tokens to the largest value your balance can hold (never below 4096) and run it. The response then carries maxTokensApplied with the value that was used. You pay for the tokens actually used, as always. The reply is only shortened if it would have run longer than your balance could pay for. The field is absent when your own max_tokens was used. A streamed turn does not carry it. If even the smallest value does not fit, the call is refused with 402 insufficient_credits as before.

model is the answer to "which model served this request?" On a chat call it is read from the model's own reply, not from our request — so you can assert it per call in your own code instead of asking the assistant, which cannot report its own identity. It is always the same model id you requested; if it were ever anything else, the request would have failed instead. (On a media create call it echoes the model the task was accepted for; media task APIs return no model of their own.)

Conversations (chat models). Send messages inside input instead of prompt to pass a whole conversation, exactly as the model receives it: an array of { "role", "content" } objects with role one of user, assistant or system (plus tool, for function results, on models that list tools — see function calling below). There is no limit of ours on how many messages a conversation has, and we never drop old ones — the model's context window is the limit. content is a string, or an array of content blocks for a model that accepts images; a message may also carry images. Send prompt or messages, not both — a request with both is refused (400 prompt_and_messages) rather than one of them being ignored. Anything malformed is refused too, naming the message: a role we do not accept, a content that is not a string or array, or an empty message. Nothing is dropped silently, and nothing is charged for a refused request.

bash
curl -X POST https://you.bot/api/v1/generate \
  -H "Authorization: Bearer $YOUBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "modelId": "claude-opus-5",
    "input": {
      "messages": [
        { "role": "user", "content": "I want to build an AI reseller site" },
        { "role": "assistant", "content": "Happy to help — a few technical decisions first." },
        { "role": "user", "content": "List the Claude input and output prices." }
      ]
    }
  }'

Function calling (chat models that list tools). Send tools inside input, the functions the model may call, and the model can answer with calls to them instead of (or as well as) text. Each entry is { "type": "function", "function": { "name", "description", "parameters" } }, with parameters a JSON Schema object; the flat form ({ "type": "function", "name", … }) and Anthropic's { "name", "description", "input_schema" } are accepted too, so an existing definition list works as it is. Up to 64 functions per request, each named with letters, digits, _ or -. tool_choice is auto (the default when tools is set), none, required (at least one call), or { "type": "function", "function": { "name": "…" } } to require one function; a model's own API reference lists which of these it accepts, and the others are refused before any charge, as is strict: true where that reference says it is not accepted. Function calling and web_search cannot be combined in one request yet. When the model calls a function, the response carries toolCalls: an array of { "id", "type": "function", "function": { "name", "arguments" } }, where arguments is a JSON string for you to parse. text holds whatever the model wrote before the call, which is often nothing. Only calls to functions you declared are returned, and every returned call is complete: its arguments always parse to an object. A call the model was still writing when the turn reached its token limit is not returned; finishReason, the model's own stop reason (for example tool_use, tool_calls or max_tokens), comes with every turn that returns calls, and with a turn whose only calls were cut off, whenever the model reports one, so a turn cut short by the limit is visible, and raising max_tokens gets the whole call. A turn that returns calls is a successful turn, charged for its tokens like any other; your function definitions are part of the input, so they count as input tokens on every turn that sends them.

json
{
  "taskId": "a1b2c3d4e5f6g7h8",
  "model": "MODEL_ID",
  "text": "",
  "creditsCharged": …,
  "usage": { … },
  "toolCalls": [
    { "id": "call_0b1c2d", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\":\"Paris\"}" } }
  ],
  "finishReason": "tool_use"
}

Sending results back. Run the functions yourself, then call again with the whole conversation in messages: the assistant message with its calls under tool_calls, exactly as you received them in toolCalls (its content may be empty), then one message per call with role tool, that call's tool_call_id, and the function's result as a string in content. Send the same tools on every turn of the loop. Every call needs its result right after the assistant message that made it, and every result must answer one of those calls; a conversation that breaks this is refused with 400 message_tool_call_invalid before anything is charged. Content blocks of type tool_use or tool_result are not accepted inside content; send calls and results in the shape above instead (400 message_tool_block_unsupported). functions, the older name for tools, is not accepted, and parallel_tool_calls: false is not supported yet: one turn may return several calls, so run them all and send every result back. On a model with a thinking parameter, send thinking: false on the turns that return results: the model would need its own thinking from the turn that made the call, which this API does not return, so such a turn is refused (400 capability_not_supported) rather than sent.

bash
curl -X POST https://you.bot/api/v1/generate \
  -H "Authorization: Bearer $YOUBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "modelId": "MODEL_ID",
    "input": {
      "tools": [
        { "type": "function", "function": {
            "name": "get_weather", "description": "Current weather for a city",
            "parameters": { "type": "object", "properties": { "city": { "type": "string" } }, "required": ["city"] } } }
      ],
      "messages": [
        { "role": "user", "content": "What is the weather in Paris right now?" },
        { "role": "assistant", "content": "", "tool_calls": [
            { "id": "call_0b1c2d", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\":\"Paris\"}" } } ] },
        { "role": "tool", "tool_call_id": "call_0b1c2d", "content": "{\"temp_c\": 17, \"sky\": \"light rain\"}" }
      ]
    }
  }'

Where function calling is available. On the chat models whose own API reference lists tools. The parameter appears there only where we have measured that the model, on the route that serves it, calls the functions you declare and reads their results. On any other model a request with tools, or with function calls in messages, is refused with 400 capability_not_supported and nothing is charged, rather than answered as if the functions did not exist.

Web search (chat models that list web_search). Send web_search: true inside input and the model searches the web before answering. On the model+route pairs where we have measured that the search can be required, we require it — so a request that asks for search gets a search, instead of the model deciding to answer from memory. Two things follow, and both are visible on your bill and your clock: the search results are fed back to the model as input tokens, so a searched turn can cost many times a plain one (measured on one model: 39 input tokens becomes 16,221), and a searched turn is slower — a few seconds becomes up to ten. On some models each search is also billed at a fixed per-search price, on top of the tokens; where that applies, the price is stated next to web_search on that model's page and in its API reference, and a model that states none charges nothing extra per search. Send web_search: true on the turns that need fresh facts, not on every turn. Search is opt-in: a request that omits web_search never searches, whatever the parameter table shows as its example value — so an integration that wants search has to ask for it on every turn that needs it, and one that never asks is never billed for one. If the reply comes back as raw search results instead of an answer, the request is refused with 422 search_not_performed — you do not receive it as an answer, and the tokens the model reported for the turn (plus the per-search price for the searches it ran, on a model that lists one) are still charged, because it read your input and the search results before we could tell.

Prompt caching (Claude models). When a long prefix repeats across requests — a system brief, a document, a few-shot block — mark the end of it with cache_control on that content block and the model can read the prefix from its cache on later requests instead of processing it again. We forward the marker to the model exactly where you put it, on claude-opus-5, claude-opus-4-8, claude-sonnet-5, claude-fable-5 and claude-haiku-4-5; other chat models do not accept it and it is left out of their requests rather than being sent and ignored. Put it on a text block: a marker on an image or any other block type is not forwarded, and the request is otherwise sent exactly as you wrote it. The cut point is yours: everything up to and including the marked block is the cacheable prefix, so put the stable text first and the part that changes every call after it.

json
{
  "modelId": "claude-opus-5",
  "input": {
    "messages": [
      { "role": "user", "content": [
        { "type": "text", "text": "<the long prefix that repeats>", "cache_control": { "type": "ephemeral" } },
        { "type": "text", "text": "<the question that changes every call>" }
      ]}
    ]
  }
}

What it costs, and how you can tell. A request that writes the cache is billed at the model's Cache Writes rate and one that reads it at the model's Cached Input rate — both are published per model on its own page, beside Input and Output. The usage object on the response reports what actually happened: cacheWriteTokens on the request that stored the prefix, cachedInputTokens on the ones that read it, and both are counted in creditsCharged. A model whose page shows no cache rows has no cache pricing with us, so nothing about that model changes whether you send the marker or not.

The prompt-length cap counts every content block together, not each block on its own — a request over the cap is refused with 413 whether the text arrives as one block or ten, so splitting text across blocks is not a way around it. And sending cache_control never changes what the model is asked: the same blocks, in the same order, with the marker added where you put it.
Do not flatten a conversation into one prompt with User: / Assistant: labels. The model reads that as a single message, and a model using web search will search for the whole block of text instead of your question. Send messages and each turn keeps its role.

Streaming (chat models). Send stream: true inside input and the response is an event stream (Content-Type: text/event-stream) instead of a JSON object. It is opt-in on this endpoint: omit it and you get the single JSON object described above, so existing integrations are unaffected. The frames are:

text
event: start
data: {"model":"claude-fable-5","taskId":"a1b2c3…"}

data: {"text":"Once upon"}

data: {"text":" a time"}

event: done
data: {"taskId":"a1b2c3…"}

start carries the same model value the JSON response would, and a taskId. Each middle frame carries the text added since the previous frame — concatenate them for the full reply. done closes the stream. The exact amount charged is settled from the model's own token counts after the stream ends, so it is not in the stream; look the taskId up in your usage log for the final figure. The same taskId is also returned in the X-Task-Id response header, and the model in X-Model. If the model has not started answering within about 2 seconds — typically while it searches the web — the stream opens without waiting for it: you receive : keep-alive comment lines (skip every line that starts with :, as the event-stream format requires) until start arrives, X-Task-Id is not set on that response (read the taskId from start instead), and a failure decided after that point arrives as an error frame carrying code, message and status rather than as an HTTP status. Streaming applies to chat models that list stream as an input parameter; media models are unaffected.

Function calls on a stream: the tool_calls frame. When the model calls functions on a streamed turn, one event: tool_calls frame carrying { "toolCalls": [ … ] }, the same array the JSON response carries, arrives after the last text frame and before done, with finishReason beside it whenever the model reports one. The calls in it are complete; there are no partial-argument frames to assemble. A streamed turn without that frame made no complete calls; when its only call was cut off at the token limit, finishReason rides on the done frame instead.

text
event: start
data: {"model":"MODEL_ID","taskId":"a1b2c3…"}

event: tool_calls
data: {"toolCalls":[{"id":"call_0b1c2d","type":"function","function":{"name":"get_weather","arguments":"{\"city\":\"Paris\"}"}}],"finishReason":"tool_use"}

event: done
data: {"taskId":"a1b2c3…"}

A voided turn on a stream: the error frame. The HTTP status is already 200 by the time the first byte leaves, so a turn that turns out to be unusable is reported inside the stream instead. Three cases produce it today: the reply came back without an answer when web_search was requested (search_not_performed), the model answered that it did not receive the image you attached (image_not_received), and the turn produced no answer text at all (empty_reply — including when your own max_tokens is too small for us to explain it in words). All three follow one rule: the turn is refused rather than delivered, and it is charged only when the model reported token counts for the turn — it read your input and produced those tokens before we could tell the answer was unusable, and they are billed at the model's ordinary rates; when it reported no token counts, nothing is charged. The charged field on the frame carries the figure, and 0 means exactly that. Either way the text already sent is not an answer, so discard it rather than store it.

text
event: start
data: {"model":"claude-opus-5","taskId":"a1b2c3…"}

data: {"text":"I'll search for …"}

event: error
data: {"code":"search_not_performed","charged":0.07,"message":"Web search was requested but the reply came back without an answer. Retry without `web_search`, or ask again shortly. The tokens this turn used were charged; see your usage log for the amount.","taskId":"a1b2c3…"}

event: done
data: {"taskId":"a1b2c3…","refused":true}

Treat any error frame as fatal for that turn: the code is the same string the non-streamed path returns in its JSON error body (search_not_performed, image_not_received, empty_reply), and done then carries refused: true. A stream with no error frame delivered a real reply. There is no way to un-send the bytes already written, which is why this is a frame and not a status code — if your client stores replies, key the write on having seen done without refused.