API Reference

Generate an image

Draws one or more frames from a prompt, working from reference images when you pass them.

POST/api/v1/images

A JSON POST with one header for the key: Authorization: Bearer sk-sl-…. The route serves image models only — a text model named here is refused rather than answered. How keys are sent

This is the route for working from a frame you already have: the images to start from go in input_references, and the reply carries raw base64 in data[].b64_json. The same models answer on chat completions, where the prompt comes from messages and the frame comes back as a data URL.

Request body

Two fields are required; everything else has a default. What each field accepts, and where it stops, is in values and limits below.

Request body fields
FieldTypeDescription
modelstringrequiredCatalogue identifier of an image model.
promptstringrequiredWhat to draw. Checked before anything is charged.
input_referencesarrayFrames to start from, as URLs or content parts.
nintegerHow many frames to return. Default 1.
sizestringExact pixels or a tier. Pixels outrank the rest.
resolution, image_sizestringThe tier on its own; both names are one field.
aspect_ratio, aspectstringShape of the frame, written W:H.
qualitystringRendering quality. Default high.
output_formatstringEncoding of the returned bytes. PNG by default.
output_compressionintegerEncoding quality for jpeg and webp. Ignored for png.
backgroundstringTransparent, opaque, or left to the model.
streambooleanReply as server-sent events, not one JSON body.
seedintegerAccepted, no effect: results are not repeatable.
userstringAccepted, no effect. Not forwarded anywhere.

Scroll the table sideways for the description.

The model is named by its catalogue identifier and answers as itself: there are no aliases, and nothing is put in its place. prompt has to be a non-empty string — a missing or blank one is refused before the request can cost anything.

A reference is either a base64 data URL, an http(s) URL we fetch for you, or the content-part object {"type":"image_url","image_url":{"url":"…"}}. References are input: each one is counted as about 2,900 tokens in usage.prompt_tokens and billed at the model's input rate, so six of them cost noticeably more than a bare prompt.

seed and user are accepted, not executed

Both fields are part of the request shape, so sending them is not an error and never will be. Neither changes the result: identical requests with the same seed still return different frames. If your code relies on reproducibility, this is the place to find that out — not after an hour of comparing two frames and blaming the prompt.

Values and limits

These fields are enumerated. A value we do not recognise comes back as a 400 naming the field, never as a frame drawn on a guess. The same fields are also read from an image_config object — the form the chat route uses; every value you send is checked wherever it sits, and if a field is set in both places the top-level one is used.

size
WxH in pixels, a tier, or auto. Pixels are honoured exactly and are capped at 3840 on a side and 8.29 megapixels (8,294,400 px) in total; a side under 16 px is refused. Both ceilings are checked before anything is reserved. Pixels that contradict a tier or an aspect ratio in the same body are a 400 — choosing one of the two silently would mean ignoring the other without telling you.
resolution, image_size
512, 1K, 2K, 4K or auto. A tier is a normalised level rather than an exact frame: it sets the short edge of what is delivered, and the aspect ratio — from your request, or from the frame itself when you did not ask for one — sets the other side.
aspect_ratio, aspect
Any ratio written W:H, fractional sides included (9:19.5), or auto. Zero on either side is refused. Alongside exact pixels it has to agree with them.
quality
auto, low, medium, high; default high. Not every size has a quality knob to turn, so this is a request rather than a guarantee.
output_format
png, jpeg, webp. Encoding happens on our side once the frame is drawn, and an unknown value is refused up front rather than after you have paid for it. A frame already in the requested format is returned byte for byte instead of being squeezed a second time.
output_compression
An integer from 0 to 100, applied to jpeg and webp. Ignored for png.
background
auto, opaque, transparent. Transparency needs a format that carries alpha, so it cannot be combined with jpeg, and only some models support it — for a few, the requested frame matters too. Where it is not supported the request is refused with a 400 before anything is billed, and the refusal names the model rather than the frame size — better than an opaque frame you discover by opening the file.
n
1 to 10, default 1. Larger numbers are clamped to 10; a value that is not a number is a 400. Fewer frames can come back than you asked for, and you are billed for the ones that arrive.
input_references
At most 16 entries. A URL is fetched with a 10-second timeout and a 20 MB ceiling, has to answer with an image content type, and redirects are not followed. A reference that cannot be fetched is a 400 before the reserve — never a frame quietly drawn without it.

Response

200, application/json, one object for the whole request:

json
{
  "created": 1748372400,
  "data": [
    {
      "b64_json": "<base64-encoded-image>",
      "media_type": "image/png"
    }
  ],
  "usage": {
    "prompt_tokens": 2909,
    "completion_tokens": 5250,
    "total_tokens": 8159,
    "cost": 0.189811
  }
}
Response fields
FieldTypeDescription
createdintegerrequiredUnix time in seconds when the reply was built.
dataarrayrequiredOne entry per frame returned.
data[].b64_jsonstringrequiredImage bytes in base64, with no data: prefix.
data[].media_typestringMIME type, read from the returned bytes.
usage.prompt_tokensintegerInput counted: the prompt and every reference.
usage.completion_tokensintegerOutput tokens of the frames returned — the frames themselves.
usage.total_tokensintegerThe two above, added.
usage.costnumberUSD charged for the whole request, all frames included.

Scroll the table sideways for the description.

Two habits worth forming. Take the file extension from media_type rather than from what you asked for: it is read from the bytes that actually came back, so it stays right even when re-encoding did not happen. And read usage.cost as the amount charged for this request — the figures above are an example, not a rate. An image model is metered in tokens: the prompt and the references are input at the model's pricing.prompt, and the frame is output at its pricing.image_output, both from the catalogue. How many output tokens a frame takes is set by its shape rather than its area, so usage.completion_tokens is the number to watch; the guide lists measured counts for common frames. What a frame costs Read the catalogue from the API

Streaming

With stream: true the same 200 arrives as text/event-stream: one image_generation.completed event per frame, then the [DONE] sentinel.

text/event-stream
data: {"type":"image_generation.completed","b64_json":"<base64-encoded-image>","media_type":"image/png","created":1748372400,"usage":{"prompt_tokens":2909,"completion_tokens":5250,"total_tokens":8159,"cost":0.189811}}

data: [DONE]

Two properties to build on. A frame is drawn whole, so its event appears only when it is finished — there is no partial image to paint as it fills in. And usage rides on the last event only, because the charge is one per request: with n above 1 the earlier events carry none, and adding up what you see would count the same money twice.

How streams start, end and fail

Example

One frame, edited from a local file and written back to disk:

shell
curl https://api.gtokens.io/api/v1/images \
  -H "Authorization: Bearer sk-sl-your-key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-5.4-image-2",
    "prompt": "Replace the sky with a clear night sky",
    "size": "1080x1920",
    "input_references": ["data:image/png;base64,iVBORw0KG..."]
  }'
main.py
import base64
import requests

reference = base64.b64encode(open("frame.png", "rb").read()).decode()

r = requests.post(
    "https://api.gtokens.io/api/v1/images",
    headers={"Authorization": "Bearer sk-sl-your-key"},
    json={
        "model": "openai/gpt-5.4-image-2",
        "prompt": "Replace the sky with a clear night sky",
        "size": "1080x1920",                 # exact pixels, honoured as asked
        "input_references": ["data:image/png;base64," + reference],
    },
    timeout=300,
)
if r.status_code != 200:
    raise SystemExit(r.json()["error"]["message"])

frame = r.json()["data"][0]
open("out.png", "wb").write(base64.b64decode(frame["b64_json"]))
main.ts
import { readFile, writeFile } from 'node:fs/promises'

const reference = (await readFile('frame.png')).toString('base64')

const r = await fetch('https://api.gtokens.io/api/v1/images', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer sk-sl-your-key',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: 'openai/gpt-5.4-image-2',
    prompt: 'Replace the sky with a clear night sky',
    size: '1080x1920', // exact pixels, honoured as asked
    input_references: ['data:image/png;base64,' + reference],
  }),
})
if (!r.ok) throw new Error((await r.json()).error.message)

const { data } = await r.json()
await writeFile('out.png', Buffer.from(data[0].b64_json, 'base64'))

The reply is a single object, and data[0].b64_json is the new frame — exactly 1080x1920, because the size was named in pixels. Drop input_references and the same call generates from the prompt alone.

Errors

Every refusal arrives in one shape, and the field to branch on is metadata.error_type rather than the status alone — one status covers several cases.

json
{
  "error": {
    "code": 400,
    "message": "prompt must not be empty",
    "metadata": { "error_type": "invalid_prompt" }
  }
}
Statuses this route returns
Statuserror_typeMeaning
400invalid_requestMalformed body, unknown value, clashing geometry.
400invalid_promptprompt missing or empty, or an unreadable part.
400invalid_imageA data: reference that is not a decodable image.
400image_download_failedA reference URL could not be fetched.
401authenticationNo bearer token, or an unknown or revoked key.
402payment_requiredBalance cannot cover it, or a ceiling was reached.
403permission_deniedThe account is suspended; the key is fine.
404not_foundNo model with that identifier.
429rate_limit_exceededToo many requests upstream. Carries Retry-After.
500serverOur fault. Repeating the request is reasonable.
502provider_unavailableA reply arrived but could not be used.
503provider_overloadedNothing is free right now. Carries Retry-After.
504timeoutNo reply in the time allowed.

Scroll the table horizontally for the last column.

When there is something more specific to say, it arrives next to the typed code in metadata.reason: a 400 whose reason is model_not_available means the model is real and listed, but not served through this route yet — retrying will not change that, and no other model is substituted for it. A 503 means the same thing about this moment rather than about the model: wait out Retry-After and send the same request again. It is never quietly moved to a different source.

A refused request is not charged

Everything that can be checked — the prompt, the field values, the size ceilings, the references — is checked before any money is held, and a 402 costs nothing at all. If a request fails after the hold is taken, the hold is released; when it succeeds, only the frames that actually came back are billed, even if you asked for more.

Every status code and what to do with it