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

Fields of the request body
FieldTypeDescription
modelstringrequiredCatalogue identifier, vendor/model.
messagesobject[]requiredThe conversation so far.
max_tokensintegerrequiredCeiling on the generated tokens.
systemstring | object[]Instructions outside the conversation.
streambooleanEvents instead of one JSON object.
temperaturenumberSampling temperature.
top_pnumberNucleus sampling cutoff.
top_kintegerThe k likeliest tokens only.
stop_sequencesstring[]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

main.py
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)
main.ts
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)
shell
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"}]
  }'
max_tokens is not optional here

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:

200 application/json
{
  "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
  }
}
Fields of the reply
FieldTypeDescription
idstringIdentifier of this message.
typestringAlways "message" here.
rolestringAlways "assistant".
modelstringThe model that answered.
contentobject[]Blocks of the answer.
stop_reasonstring | nullWhy generation ended.
stop_sequencestring | nullThe sequence that matched.
usageobjectToken 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_tokens was 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_sequences appeared; which one is in stop_sequence, which is null in 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:

Fields of usage
FieldTypeDescription
input_tokensintegerRead from the request.
output_tokensintegerGenerated in the reply.
cache_creation_input_tokensinteger | nullWritten to the cache.
cache_read_input_tokensinteger | nullServed 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

No price field in this reply

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:

200 text/event-stream, abridged
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.

A failure reaches you in one of two ways

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:

text/event-stream
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"}}
Reading events, and how streams end elsewhere

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:

401 application/json
{
  "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-After is set, but a repeat on a schedule will not change the answer — this is not an exhausted limit. An exhausted limit looks different: see 503 below.
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-After is 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