API Reference
API reference
An overview of the gateway API: the shape every route shares, and what each field of the reply means.
Request and response schemas follow the OpenAI Chat API, with a few small differences, so a client written against that shape works here after a base URL change — no adapter, no rewritten call sites. Base URL for these routes is https://api.gtokens.io/api/v1.
What differs is listed here rather than left to be discovered: the reply carries the amount charged, the catalogue identifier you send is echoed back unchanged, and an image model puts its frame into the assistant message instead of text.
Two shapes, one key
The rest of this page describes the OpenAI-shaped routes, which serve both text and image models. Text models also answer on the Messages route, which passes the upstream body through in its native shape — the same key, the same accounts and the same billing, a different envelope. Pick the shape by the client library you hold; knowing which one you are on matters before you write the parser, because the two disagree on the first field you will read.
| What | OpenAI-shaped routes | Messages route |
|---|---|---|
| Serves | Text and image models | Text models |
| Paths | /api/v1/chat/completions/api/v1/images | /api/v1/messages/v1/messages |
| Base URL | https://api.gtokens.io/api/v1 | https://api.gtokens.io |
| Reply | The shape below | Native, passed through |
| Charge | usage.cost | Not in the reply |
| Error body | { error: { code, message, metadata } } | { type: "error", error, request_id } |
Scroll the table horizontally for the Messages route column.
An image model answers in the OpenAI shape only: named on the Messages route it is refused with 400, and named on /api/v1/images a text model is refused the same way. Nothing is quietly routed somewhere it does not belong. The Messages route
Requests
The body of a call, as a TypeScript type. Both routes take the same fields; they differ in where the prompt comes from — messages on one, prompt plus input_references on the other — and the kind of model named in model decides which of the optional fields are read.
// Body of POST /api/v1/chat/completions and POST /api/v1/images.
type Request = {
// Required. A catalogue identifier, exactly as listed: "vendor/model".
// Unknown or missing -> 404. Listed but not served yet -> 400.
// The catalogue decides what the model answers with: text or a frame.
model: string;
// chat/completions: the whole conversation is read, not just the last turn.
messages?: Message[];
// images: an explicit prompt, plus the frames to work from (up to 16).
prompt?: string;
input_references?: (string | ImageContentPart)[];
stream?: boolean; // default false
n?: number; // image models: frames per request, 1..10; text models: 1
// -- Text models on chat/completions --------------------------------------
max_tokens?: number; // ceiling on the reply; not sent -> 4096
temperature?: number; // removed silently on models that refuse it
top_p?: number; // same
top_k?: number;
stop?: string | string[];
tools?: Tool[];
tool_choice?: 'auto' | 'none' | 'required'
| { type: 'function'; function: { name: string } };
parallel_tool_calls?: boolean;
stream_options?: { include_usage?: boolean }; // accepted; usage is sent anyway
cache_control?: { type: 'ephemeral'; ttl?: string }; // automatic prompt caching
// -- Image models ---------------------------------------------------------
size?: string; // "1080x1920" | "2K" | "auto"
resolution?: string; // same values as size
image_size?: string; // same values as size
aspect_ratio?: string; // "9:16", "16:9", "1:1", "9:19.5", ...
quality?: 'auto' | 'low' | 'medium' | 'high' | 'xhigh' | 'max'; // provider default
output_format?: 'png' | 'jpeg' | 'webp';
output_compression?: number; // 0..100, jpeg and webp only
background?: 'auto' | 'transparent' | 'opaque';
// The image fields above may also be sent nested here. Both places are
// read, and every value sent is validated, not just the first one found.
image_config?: Record<string, unknown>;
// Accepted, and deliberately not acted on. On a text model the same goes
// for response_format, logit_bias, logprobs and the two penalties.
seed?: number;
user?: string;
};
// Subtypes:
type TextContentPart = { type: 'text'; text: string; cache_control?: CacheControl };
type ImageContentPart = {
type: 'image_url';
image_url: { url: string }; // https://... or data:image/png;base64,...
cache_control?: CacheControl;
};
type CacheControl = { type: 'ephemeral'; ttl?: string };
type Message =
// "system" and "developer" are the standing instruction: an image model
// reads them first, a text model gets them as its system prompt.
| { role: 'system' | 'developer'; content: string | TextContentPart[] }
| { role: 'user'; content: string | (string | TextContentPart | ImageContentPart)[] }
// Earlier answers of a text model, tool calls included.
| { role: 'assistant'; content?: string | TextContentPart[]; tool_calls?: ToolCall[] }
// The result of a tool call, matched by id.
| { role: 'tool'; tool_call_id: string; content: string | TextContentPart[] };
type Tool = {
type: 'function';
function: { name: string; description?: string; parameters?: object };
};
type ToolCall = {
id: string;
type: 'function';
function: { name: string; arguments: string }; // arguments is a JSON string
};The whole of messages is used, not the last turn alone. A text model gets it turn by turn, tool calls and results included; an image model joins the text into one prompt and collects the image parts as references. For the full list of fields and what each one does, see Parameters; for how geometry decides the delivered size, see the image generation guide.
Headers
Two, and no others are required or read:
Authorization: Bearer sk-sl-…— the key, on every route including the Messages one. AuthenticationContent-Type: application/json— on every POST. A body that is not a JSON object comes back400, not500.
const r = await fetch('https://api.gtokens.io/api/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: 'Bearer sk-sl-your-key', // required on every route
'Content-Type': 'application/json', // required on every POST
},
body: JSON.stringify({
model: 'anthropic/claude-sonnet-5',
messages: [{ role: 'user', content: 'Describe a lighthouse at dawn' }],
max_tokens: 256,
}),
})On the way back, one header is worth reading: Retry-After, in seconds, present on every 429 and 503 — and exposed to browser JavaScript, so a front end can honour it too.
Send stream: true and the reply arrives as text/event-stream, closed by data: [DONE]. A text model writes in pieces, one chat.completion.chunk per event, and the last chunk carries usage. A frame is never partial: it arrives whole, in one terminal chunk. Anything that can fail before the model starts still fails before the first byte, with a real HTTP status rather than a 200 with an error buried in it. Streaming
seed and user are accepted and have no effect — refusing them would break clients that always send them. On a text model the same goes for response_format, logit_bias, logprobs and the two penalties: accepted, dropped, noted in our log. But an unrecognised value of a field we do read — an image field such as size or quality, a tool_choice that is neither a keyword nor a function, a role a text model does not know — is a 400 naming the field. A swallowed typo would come back as a frame in the wrong format or a model answering a different question, and you would go looking for it in your prompts. One exception: n outside 1..10 on an image model is clamped to the range rather than refused.
Responses
choices is always an array with one entry, and that entry carries message — or delta instead, when the request asked for a stream. That way one parser handles both.
// POST /api/v1/chat/completions
type Response = {
id: string; // "gen-...", the identifier this request is logged under
object: 'chat.completion' | 'chat.completion.chunk';
created: number; // unix seconds
model: string; // the catalogue identifier you asked for
// Always an array with one element. It carries "message", or "delta"
// instead when stream: true was sent.
choices: Choice[];
usage: Usage; // always present, never zeroed
};
type Choice = {
index: 0;
// Image models: always "stop". Text models: one of the four; null on every
// streamed chunk except the last.
finish_reason: 'stop' | 'length' | 'tool_calls' | 'content_filter' | null;
// Text models: the executor's own word for it, e.g. "end_turn", "max_tokens".
native_finish_reason?: string;
message?: AssistantMessage;
delta?: Partial<AssistantMessage>;
};
type AssistantMessage = {
role: 'assistant';
content: string; // the answer of a text model; "" for an image model
tool_calls?: ToolCall[]; // text models, when the model calls a tool
images?: ImageOut[]; // image models: the frames
};
// The frame itself: a data URL, ready to decode or to hand to a browser.
type ImageOut = { type: 'image_url'; image_url: { url: string } };
type Usage = {
prompt_tokens: number; // text models: cached input included
completion_tokens: number;
total_tokens: number;
cost: number; // US dollars charged for this request
prompt_tokens_details?: { cached_tokens: number }; // when a cache was read
};
// POST /api/v1/images returns the same usage in a different envelope.
type ImagesResponse = {
created: number;
data: { b64_json: string; media_type: string }[];
usage: Usage;
};The call from the headers section, answered by a text model:
{
"id": "gen-3b7e9c1d5a2f4086",
"object": "chat.completion",
"created": 1772668800,
"model": "anthropic/claude-sonnet-5",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "A lighthouse at dawn stands pale against a sky just turning gold. Its beam, still lit, fades as the sun takes over."
},
"finish_reason": "stop",
"native_finish_reason": "end_turn"
}
],
"usage": {
"prompt_tokens": 26,
"completion_tokens": 58,
"total_tokens": 84,
"cost": 0.000316
}
}finish_reason follows the OpenAI vocabulary: stop when the model finished or hit one of your stop strings, length when it hit max_tokens, tool_calls when it wants a tool run, content_filter when it declined. The executor's own word travels next to it in native_finish_reason. On an image model it is always stop: a frame is either complete or the request failed with a status and was not charged. Failures arrive as HTTP status codes with a typed body, never as a successful reply with an error inside it. Errors
content is the answer of a text model and an empty string for an image model — there the frame lives in images, and media_type on the /images route is read from the bytes actually returned, so it describes the frame you got rather than the format you asked for.
Usage and cost
usage is returned on every successful call, and none of its fields is ever a placeholder zero: a paid request that consumed nothing would look like a bookkeeping fault, so where a counter is not reported upstream it is estimated rather than blanked.
- prompt_tokens
- Input. On a text model it is the conversation as the model read it, cached input included — the part served from the prompt cache is repeated in
prompt_tokens_details.cached_tokens, and it is billed at the cache-read rate rather than the input rate. On an image model it is the prompt plus every attached frame, each counted at a fixed estimate of about 2,900 tokens (the catalogue'spricing.imageis that count at the input rate); attachments dominate it, and input is billed, so six references cost noticeably more than a bare prompt. - completion_tokens
- Output: the answer of a text model, or the frames of an image model. For a frame the count is the renderer's where it reports one, otherwise estimated from the size and shape of the delivered frame — a square takes more tokens than an oblong frame of the same size.
- total_tokens
- The two above, added.
- cost
- The money. US dollars actually taken from the balance for this request — not an estimate and not a list figure. It is the same number the request log in your account shows under this
id. If you record one number per call, record this one.
There is no separate endpoint for fetching statistics after the fact: token counts and the charge live in the reply body, and account-wide totals come from GET /api/v1/credits and GET /api/v1/key. On the Messages route, where the reply is passed through untouched and has no cost field, those two routes and the request log in your account are where the charge shows up.
Methods
| Method | Path | What it does |
|---|---|---|
| POST | /api/v1/chat/completions | Text and image models in the OpenAI chat shape: answers, tool calls, frames. |
| POST | /api/v1/images | Edit or extend frames given as references. |
| POST | /api/v1/messages | Text models in the native Messages API shape, with or without streaming. |
| GET | /api/v1/models | The catalogue, its rates and what is served. |
| GET | /api/v1/credits | Bought and spent, all time. |
| GET | /api/v1/key | The calling key, its spend and any ceiling. |
Scroll the table horizontally for the description.
Every field a request body accepts