API Reference
Create a message
Sends a conversation to a text model and returns the reply, as one JSON object or as a stream of events.
POSThttps://api.gtokens.io/api/v1/messages
POSThttps://api.gtokens.io/v1/messages
Two addresses, one method: same body, same reply, same billing. The short one exists because client libraries take a base URL and append the path themselves — pointed at the gateway root, they send the request to /v1/messages. If you build the URL by hand, use /api/v1/messages, the same prefix as every other route.
Every request carries Authorization: Bearer sk-sl-… and Content-Type: application/json. There are no other headers to set, and x-api-key is not read here. How the key is passed
This route serves the text models of the catalogue in the native Messages API shape — the one a Messages client library or an agent runtime speaks. The same models also answer on chat/completions in the OpenAI shape, served by the same core: the same accounts, the same billing and the same statuses, in a different envelope. Image models answer on the OpenAI-shaped routes only; named here they are refused with 400.
Request body
| Field | Type | Description |
|---|---|---|
| model | stringrequired | Catalogue identifier, vendor/model. |
| messages | object[]required | The conversation so far. |
| max_tokens | integerrequired | Ceiling on the generated tokens. |
| system | string | object[] | Instructions outside the conversation. |
| stream | boolean | Events instead of one JSON object. |
| temperature | number | Sampling temperature. |
| top_p | number | Nucleus sampling cutoff. |
| top_k | integer | The k likeliest tokens only. |
| stop_sequences | string[] | Strings that end the generation. |
Scroll the table sideways for the description.
An entry of messages has a role — user or assistant — and content, which is either a string or an array of blocks. Send the whole conversation every time: nothing is kept between requests. When a string from stop_sequences ends the generation, that same string comes back in stop_sequence.
The body is forwarded as it arrives, so a field the model does not accept is refused with 400 carrying the provider's own explanation rather than a generic one. Which parameters a model reads is published per model in the catalogue. Parameters in detail
from anthropic import Anthropic
# The client appends /v1/messages to the base URL itself, and the key
# goes in as auth_token: api_key would be sent in a header we do not read.
client = Anthropic(
base_url="https://api.gtokens.io",
auth_token="sk-sl-your-key",
)
message = client.messages.create(
model="anthropic/claude-sonnet-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Say hello"}],
)
print(message.content[0].text, message.usage.output_tokens)const r = await fetch('https://api.gtokens.io/api/v1/messages', {
method: 'POST',
headers: {
Authorization: 'Bearer sk-sl-your-key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'anthropic/claude-sonnet-5',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Say hello' }],
}),
})
const message = await r.json()
console.log(message.content[0].text)curl https://api.gtokens.io/api/v1/messages \
-H "Authorization: Bearer sk-sl-your-key" \
-H "Content-Type: application/json" \
-d '{
"model": "anthropic/claude-sonnet-5",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Say hello"}]
}'No default is substituted for it. A body without max_tokens comes back 400, and the text of that refusal is the provider's. The same number also sizes the amount held on your balance while the request runs: the hold is an estimate at the ceiling you named, and it is settled against the tokens actually spent as soon as the reply is complete. Balance and holds
The reply
Without stream the answer is one JSON object with 200:
{
"id": "msg_01XFDUDYJgAACzvnptvVoYEL",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-5",
"content": [{ "type": "text", "text": "Hello. How can I help?" }],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 12,
"output_tokens": 18,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0
}
}| Field | Type | Description |
|---|---|---|
| id | string | Identifier of this message. |
| type | string | Always "message" here. |
| role | string | Always "assistant". |
| model | string | The model that answered. |
| content | object[] | Blocks of the answer. |
| stop_reason | string | null | Why generation ended. |
| stop_sequence | string | null | The sequence that matched. |
| usage | object | Token counters, see below. |
Scroll the table sideways for the description.
A text block is { "type": "text", "text": … }, and in the ordinary case there is exactly one of them. model is the identifier the provider echoes: the model is the one you asked for, spelled the way it spells it, so read the field but do not compare it for equality with the string you sent.
- stop_reason: "end_turn"
- The model finished on its own. The usual case.
- stop_reason: "max_tokens"
- The ceiling from
max_tokenswas reached and the answer is cut off mid-thought. The tokens produced up to that point are billed, so retrying with a higher ceiling costs the truncated attempt as well. - stop_reason: "stop_sequence"
- One of your
stop_sequencesappeared; which one is instop_sequence, which isnullin every other case.
The value is passed on as the model set it. Treat one you do not recognise as “the model stopped”, not as a failure — a client that raises on an unfamiliar stop_reason breaks on a reply that is perfectly valid.
Token usage
Four counters come back, and each is billed at its own rate for that model:
| Field | Type | Description |
|---|---|---|
| input_tokens | integer | Read from the request. |
| output_tokens | integer | Generated in the reply. |
| cache_creation_input_tokens | integer | null | Written to the cache. |
| cache_read_input_tokens | integer | null | Served from the cache. |
Scroll the table sideways for the description.
Cached input is charged differently from input read afresh, which is why the two counters are separate rather than folded into input_tokens. All four rates are published per model in the catalogue and live nowhere else — reading them from there keeps your arithmetic and our invoice in step. Rates per model
This route answers in the shape of the client library that calls it, and that shape has no field for money — so the reply carries counters, not an amount. What the request cost is visible on the balance: GET /api/v1/credits for the running total, and the request log in your account for a single call.
Streaming
With stream: true the same 200 is answered as text/event-stream. Every frame is an event: line and a data: line with a JSON object whose type repeats the event name:
event: message_start
data: {"type":"message_start","message":{"id":"msg_01XFDUDY…","usage":{"input_tokens":12}}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":18}}
event: message_stop
data: {"type":"message_stop"}Seven event types arrive in a normal run: message_start, content_block_start, content_block_delta, content_block_stop, message_delta, message_stop and ping, which carries nothing and only keeps the connection warm. The text is assembled from the content_block_delta frames; the stream ends after message_stop, and there is no separate end-of-stream sentinel on this route, so do not wait for one.
The counters arrive in two places rather than one: input_tokens in message_start, the final output_tokens in the last message_delta, alongside stop_reason. Billing follows the same numbers, and it happens even if you disconnect halfway: what was generated was generated.
Before the first byte nothing has been committed yet, so the failure is a real HTTP status with a JSON body — every code listed below arrives this way, headers and all.
After the first byte the status is already 200 and cannot be taken back, so the failure arrives as an error event in the same envelope and the stream ends there. A stream that simply stops without this event was cut by the network, not refused:
event: error
data: {"type":"error","error":{"type":"overloaded_error","message":"provider is temporarily unavailable, retry later","error_type":"provider_overloaded"},"request_id":"gen-4f1c9a2b7d0e5361","metadata":{"reason":"no_provider_available"}}Errors
Both addresses answer failures in the envelope the Messages API client expects, not in the one the other routes use — the client picks its exception class from error.type, and error.error_type next to it carries the same reason in the vocabulary shared with every other route:
{
"type": "error",
"error": {
"type": "authentication_error",
"message": "invalid api key",
"error_type": "authentication"
},
"request_id": null,
"metadata": { "reason": "invalid_credentials" }
}request_id is always in the envelope, and on a JSON failure its value is null: the error event of a stream is the one place it carries an identifier. Read the field, but do not require a value from it.
- 400 · invalid_request_error
- The body is not a JSON object, the model is listed but not served yet, or the model refused the request — in the last case the text is the one it gave.
- 401 · authentication_error
- No bearer token, or a key that is unknown or revoked.
- 402 · billing_error
- The balance cannot cover the request, or a daily or monthly ceiling on the account has been reached. Nothing is charged.
- 403 · permission_error
- The account is suspended.
- 404 · not_found_error
- No model in the catalogue goes by that identifier.
- 429 · rate_limit_error
- The provider refused without naming when to come back, or asked for extra usage.
Retry-Afteris set, but a repeat on a schedule will not change the answer — this is not an exhausted limit. An exhausted limit looks different: see503below. - 500 · api_error
- Our fault. Worth reporting, not worth retrying in a loop.
- 502 · api_error
- A reply arrived that cannot be used, or the request was turned away before it reached the model. No Retry-After: a repeat will not help.
- 503 · overloaded_error
- No capacity for this model right now: every account for it is busy or paused after a limit, or the provider refused.
Retry-Afteris always set, to the moment the nearest account comes back; repeat the same request after it. - 504 · timeout_error
- No answer within the time allowed. A request that heavy will not get lighter on retry, so shorten it rather than repeat it.
A limit hit upstream does not reach you while another account for the model is free: the request moves to it, and the account that hit the limit is taken out of rotation until the time the provider named. Only when none is left does the 503 arrive, with Retry-After set to that time. That is why an exhausted limit comes back as 503 rather than 429: for the length of that interval the account is not available at all.
A request that failed is not charged: the amount held before the call is released in full. A stream that broke mid-flight is the one exception, and only for what it managed to produce before it broke.
Typed codes and what to do with each