Documentation

Tokenless exposes an OpenAI-compatible API. If you've called the OpenAI Chat Completions endpoint, you already know how to use us — point your SDK at our base URL and drop in a key.

Quickstart

  1. Create an account, verify your email, and grab a key from the dashboard. (Email verification is required before a key can be created.)
  2. Add a little balance under Billing (your first $1 is free).
  3. Make your first call:
quickstart.sh
curl https://api.tokenless.store/api/v1/chat/completions \
  -H "Authorization: Bearer $TOKENLESS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "opus-4.8",
    "messages": [{ "role": "user", "content": "Hello!" }]
  }'
Base URL. All requests go to https://api.tokenless.store/api/v1. The API is OpenAI-compatible, so most SDKs work by setting this as the base URL and your key as the API key.

Example scripts

A tiny command-line script in three flavors. Paste your API key in place of sk-tk-YOUR_KEY_HERE (or set TOKENLESS_API_KEY), set MODEL and REASONING, and pass your prompt as a command-line argument. The TypeScript and Python versions need no SDK.

ask.ts
// Run:  npx tsx ask.ts "How many moons does Jupiter have?"
const API_KEY = process.env.TOKENLESS_API_KEY ?? "sk-tk-YOUR_KEY_HERE";
const BASE_URL = "https://api.tokenless.store/api/v1";

// --- set these however you like ---
const MODEL = "opus-4.8";      // any id: fable-5, gpt-5.5, sonnet-5, ...
const REASONING = "";          // "" | low | medium | high | extra | ultra
const prompt = process.argv.slice(2).join(" ") || "Hello!";

const res = await fetch(BASE_URL + "/chat/completions", {
  method: "POST",
  headers: {
    "Authorization": "Bearer " + API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: MODEL,
    messages: [{ role: "user", content: prompt }],
    ...(REASONING ? { reasoning_effort: REASONING } : {}),
  }),
});

const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? ("HTTP " + res.status));
console.log(data.choices[0].message.content);
ask.py
# Run:  python ask.py "How many moons does Jupiter have?"
import os, sys, json, urllib.request, urllib.error

API_KEY = os.environ.get("TOKENLESS_API_KEY", "sk-tk-YOUR_KEY_HERE")
BASE_URL = "https://api.tokenless.store/api/v1"

# --- set these however you like ---
MODEL = "opus-4.8"        # any id: fable-5, gpt-5.5, sonnet-5, ...
REASONING = ""            # "" | low | medium | high | extra | ultra
prompt = " ".join(sys.argv[1:]) or "Hello!"

body = {"model": MODEL, "messages": [{"role": "user", "content": prompt}]}
if REASONING:
    body["reasoning_effort"] = REASONING

req = urllib.request.Request(
    BASE_URL + "/chat/completions",
    data=json.dumps(body).encode(),
    headers={"Authorization": "Bearer " + API_KEY, "Content-Type": "application/json"},
)
try:
    with urllib.request.urlopen(req) as r:
        print(json.load(r)["choices"][0]["message"]["content"])
except urllib.error.HTTPError as e:
    print("Error:", e.read().decode(), file=sys.stderr)
ask.sh
#!/usr/bin/env bash
# Needs curl + jq.  Run:  ./ask.sh "How many moons does Jupiter have?"
API_KEY="sk-tk-YOUR_KEY_HERE"     # or: API_KEY="$TOKENLESS_API_KEY"
BASE_URL="https://api.tokenless.store/api/v1"

# --- set these however you like ---
MODEL="opus-4.8"                  # any id: fable-5, gpt-5.5, sonnet-5, ...
REASONING=""                      # "" | low | medium | high | extra | ultra
PROMPT="$*"
[ -z "$PROMPT" ] && PROMPT="Hello!"

BODY=$(jq -n --arg m "$MODEL" --arg p "$PROMPT" --arg r "$REASONING" \
  '{model:$m, messages:[{role:"user",content:$p}]} + (if $r=="" then {} else {reasoning_effort:$r} end)')

curl -s "$BASE_URL/chat/completions" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d "$BODY" | jq -r '.choices[0].message.content'

Swap MODEL for any id from Models and set REASONING to any level from Reasoning effort. To stream the response, add stream: true. See Streaming.

Authentication

Authenticate with a bearer token in the Authorization header. Keys start with sk-tk- and are shown once at creation — store them securely and rotate from the dashboard anytime.

bash
Authorization: Bearer sk-tk-xxxxxxxxxxxxxxxxxxxxxxxx

Chat completions

POST /api/v1/chat/completions — the request and response follow the OpenAI schema, and it works for every model (GPT and Claude alike). This is the portable surface; reach for the native Responses or Messages APIs when you want a provider's own shape.

chat.py
from openai import OpenAI

client = OpenAI(
    base_url="https://api.tokenless.store/api/v1",
    api_key="sk-tk-...",
)

resp = client.chat.completions.create(
    model="opus-4.8",
    messages=[{"role": "user", "content": "Write a haiku about glass."}],
)
print(resp.choices[0].message.content)
print(resp.usage)  # includes prompt/completion tokens and cost

The response (OpenAI shape):

chat.response.json
{
  "id": "cmpl_...",
  "object": "chat.completion",
  "created": 1719800000,
  "model": "opus-4.8",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "Clear panes of morning…" },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 14, "completion_tokens": 19, "total_tokens": 33,
    "prompt_tokens_details": { "cached_tokens": 0 },
    "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0,
    "cost": 0.000512
  }
}

Responses API (OpenAI models)

POST /api/v1/responses — OpenAI's Responses schema. The prompt is input (a string or items), the cap is max_output_tokens, and effort nests under reasoning. OpenAI models only — Claude ids return a 400 pointing you at /chat/completions.

responses.sh
curl https://api.tokenless.store/api/v1/responses \
  -H "Authorization: Bearer sk-tk-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-5.5",
    "input": "Explain entanglement simply.",
    "reasoning": { "effort": "high" }
  }'

The response is the canonical Responses shape — an output array of typed items, with output_text as a convenience:

responses.response.json
{
  "id": "resp_...",
  "object": "response",
  "created_at": 1719800000,
  "model": "gpt-5.5",
  "status": "completed",
  "output": [
    // a reasoning item appears here only when include_reasoning is set (see below)
    {
      "id": "msg_...", "type": "message", "status": "completed", "role": "assistant",
      "content": [{ "type": "output_text", "text": "Entanglement is…", "annotations": [] }]
    }
  ],
  "output_text": "Entanglement is…",
  "usage": {
    "input_tokens": 12, "input_tokens_details": { "cached_tokens": 0 },
    "output_tokens": 88, "total_tokens": 100,
    "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0,
    "cost": 0.00135
  }
}

Messages API (Claude models)

POST /api/v1/messages — Claude's native format, so you can point the Anthropic SDK straight at the API. system is top-level, and max_tokens defaults to 4096 if you omit it (native Anthropic requires it; we fill in a default). Claude models only.

messages.sh
curl https://api.tokenless.store/api/v1/messages \
  -H "Authorization: Bearer sk-tk-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "opus-4.8",
    "max_tokens": 1024,
    "system": "You are concise.",
    "messages": [{ "role": "user", "content": "Explain entanglement simply." }]
  }'

The response is Anthropic's native shape — a content array of typed blocks. With thinking on, a thinking block precedes the text (see Reasoning effort):

messages.response.json
{
  "id": "msg_...",
  "type": "message",
  "role": "assistant",
  "model": "opus-4.8",
  "content": [
    // { "type": "thinking", "thinking": "…", "signature": "…" }  ← only when thinking is on
    { "type": "text", "text": "Entanglement is…" }
  ],
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": {
    "input_tokens": 16, "output_tokens": 74,
    "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "cost": 0.00097
  }
}
Using the Anthropic SDK? Set base_url to https://api.tokenless.store/api — the SDK appends /v1/messages for you.

Tool calling

Pass tools and tool_choice exactly as you would to the provider directly. They are forwarded through untouched, and the model's tool calls come back in the standard shape, so agent frameworks work without changes. Supported on Chat completions and the Messages API, streaming and non-streaming alike. The Responses API does not support tool calling yet, so use Chat completions for tool-using agents on GPT models.

tools.py
from openai import OpenAI

client = OpenAI(
    base_url="https://api.tokenless.store/api/v1",
    api_key="sk-tk-...",
)

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get the current weather for a city",
        "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
        },
    },
}]

messages = [{"role": "user", "content": "What's the weather in Paris?"}]

resp = client.chat.completions.create(
    model="opus-4.8",
    messages=messages,
    tools=tools,
    tool_choice="auto",
)

msg = resp.choices[0].message
if msg.tool_calls:                     # finish_reason == "tool_calls"
    call = msg.tool_calls[0]
    result = run_your_tool(call.function.name, call.function.arguments)

    # Send the model's own turn back, then the result keyed by tool_call_id.
    messages.append(msg)
    messages.append({
        "role": "tool",
        "tool_call_id": call.id,
        "content": result,
    })

    final = client.chat.completions.create(
        model="opus-4.8", messages=messages, tools=tools,
    )
    print(final.choices[0].message.content)

When the model calls a tool, content is null and finish_reason is tool_calls:

tools.response.json
{
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_abc123",
            "type": "function",
            "function": {
              "name": "get_weather",
              "arguments": "{\"city\":\"Paris\"}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ]
}

When streaming, tool calls arrive as incremental deltas keyed by index. The function name comes first and the arguments JSON is split across later chunks, so concatenate the fragments before parsing. Official SDKs reassemble this for you.

Tool definitions are billed as input. Your tools schemas are part of the prompt on every call, and a large toolset can run to tens of thousands of tokens per request. Send only the tools a step actually needs, and check usage.prompt_tokens to see the real cost.

Reasoning effort

Optionally control how much a model thinks with reasoning_effort. Six levels, in order: minimal · low · medium · high · extra · ultra. It's opt-in — omit it and each provider's own default applies. Thinking tokens count as output, so higher effort costs more, and it's metered exactly.

For most Claude models, the level sets the extended-thinking token budget — literally how much thinking the model may do:

  • low 4K · medium 8K · high 16K · extra 32K thinking tokens
  • ultra — uncapped: all remaining room under the model's max output
  • minimal — thinking off

Budgets are a ceiling — Claude thinks adaptively and often uses far less. The thinking budget is only served on the Messages API, where we automatically raise max_tokens so the chosen level fits alongside the answer. (A few newer models take an effort setting rather than a fixed token budget — the level scales their thinking the same way.)

Higher levels hold more balance. Because we raise max_tokens to fit the thinking, a higher level places a larger up-front hold on your balance — and ultra holds against the model's full output cap. The unused hold is refunded after metering (thinking is adaptive and usually costs far less), so you're only charged for what you use — but a low balance can get a 402 on a big ultra request. Drop to a lower level or add balance if that happens.
  • Messages (Claude) — reasoning_effort selects the thinking budget, or pass Anthropic's native thinking block for full control.
  • Chat Completions — reasoning for GPT models. Claude thinking is not enabled on this surface; use the Messages API for Claude thinking.
  • Responses (OpenAI) — nested reasoning: { effort }.

One knob, two native mechanisms. GPT uses OpenAI's discrete effort enum (which tops out at high); Claude uses a thinking-token budget — or, on a few newer models (e.g. Fable 5), an effort setting. We translate your level to each:

reasoning_effortGPT (OpenAI effort)Claude (thinking budget*)
minimalminimaloff
lowlow4K tokens
mediummedium8K tokens
highhigh16K tokens
extrahigh32K tokens
ultrahighuncapped

* A few newer Claude models (e.g. Fable 5) use the effort setting directly rather than a token budget: extra xhigh, ultra max. Same knob, same scaling.

OpenAI's standard enum stops at high, so on most GPT models extra and ultra are sent as high. The GPT-5.6 family (Sol, Terra, Luna) reaches higher: extra maps to their max tier and ultra to ultra (Luna tops out at max). For full control on Claude, skip the level and pass Anthropic's native thinking block yourself (below); it's forwarded as-is. Note: Opus 4.8 and 4.7 run the thinking but don't return the thinking text (only that it ran, via the token count); Opus 4.6 and earlier return the readable thinking.

reasoning.json
// Messages — pick a level; max_tokens is auto-bumped to fit the budget
{ "model": "opus-4.8", "max_tokens": 2048, "messages": [/* … */],
  "reasoning_effort": "extra" }

// "ultra" — uncapped thinking (bounded only by the model's max output)
{ "model": "opus-4.8", "max_tokens": 2048, "messages": [/* … */],
  "reasoning_effort": "ultra" }

// …or the native Anthropic block, forwarded as-is
{ "model": "opus-4.8", "max_tokens": 8192, "messages": [/* … */],
  "thinking": { "type": "enabled", "budget_tokens": 4096 } }

// Chat Completions / Responses — reasoning for GPT models
{ "model": "gpt-5.5", "messages": [/* … */], "reasoning_effort": "high" }

Getting the thinking back

By default only the answer comes back. Whether — and how — you receive the thinking depends on the surface.

Claude — Messages API

Thinking is automatic whenever it's on — no flag. A thinking block appears in content before the text block; with stream: true it arrives as native content_block_delta events ahead of the answer. (Opus 4.8/4.7 emit the block but omit the text — use opus-4.6 to read it.)

messages-thinking.sh
curl https://api.tokenless.store/api/v1/messages \
  -H "Authorization: Bearer sk-tk-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "opus-4.8",
    "max_tokens": 2048,
    "messages": [{ "role": "user", "content": "Prove there are infinitely many primes." }],
    "reasoning_effort": "high",
    "stream": true
  }'
# → content_block (thinking) deltas, then content_block (text) deltas, then message_delta usage

GPT — Chat Completions & Responses

Opt in with include_reasoning: true. Where it lands:

  • Chat Completions message.reasoning_content (non-stream) or delta.reasoning_content chunks, emitted before the answer (stream).
  • Responses — a reasoning item with summary_text in output (non-stream) or response.reasoning_summary_text.delta events (stream).

OpenAI hides the raw chain-of-thought, so these fields may be empty even when reasoning ran — the token usage still reflects it.

gpt-reasoning.sh
curl https://api.tokenless.store/api/v1/chat/completions \
  -H "Authorization: Bearer sk-tk-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-5.5",
    "messages": [{ "role": "user", "content": "Prove there are infinitely many primes." }],
    "reasoning_effort": "high",
    "include_reasoning": true
  }'

# Non-streaming response carries the reasoning alongside the answer:
# {
#   "choices": [{ "message": {
#     "role": "assistant",
#     "reasoning_content": "…",   ← present when include_reasoning is set
#     "content": "There are infinitely many primes because…"
#   }}], "usage": { … }
# }

Streaming

Set stream: true to receive Server-Sent Events. Tokens stream incrementally over a long-lived connection — the final event includes usage and cost.

stream.ts
const stream = await client.chat.completions.create({
  model: "gpt-5.5",
  messages: [{ role: "user", content: "Stream a story." }],
  stream: true,
  stream_options: { include_usage: true },
});

for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}

Models

GET /api/v1/models returns the catalog with live per-token pricing. Pass a model's id as the model field.

Model idProviderIn /MOut /M
fable-5Anthropic$5.00$25.00
opus-5Anthropic$2.50$12.50
sonnet-5Anthropic$1.50$7.50
opus-4.8Anthropic$2.50$12.50
opus-4.7Anthropic$2.50$12.50
opus-4.6Anthropic$2.50$12.50
sonnet-4.6Anthropic$1.50$7.50
haiku-4.5Anthropic$0.50$2.50
gemini-3.1-proGoogle$1.00$6.00
gemini-3.5-flashGoogle$0.25$1.75
gemini-3.1-flash-liteGoogle$0.08$0.30
gemini-2.5-proGoogle$0.63$5.00
gemini-2.5-flashGoogle$0.15$1.25
gemini-2.5-flash-liteGoogle$0.05$0.20
gpt-5.6-solOpenAI$2.50$15.00
gpt-5.6-terraOpenAI$1.25$7.50
gpt-5.6-lunaOpenAI$0.50$3.00
gpt-5.5OpenAI$2.50$15.00
gpt-5.5-proOpenAI$15.00$90.00
gpt-5.4OpenAI$1.25$7.50
gpt-5.4-proOpenAI$15.00$90.00
gpt-5.4-miniOpenAI$0.38$2.25
gpt-5.4-nanoOpenAI$0.10$0.63
gpt-5.3OpenAI$0.88$7.00

Usage & cost

Every response carries a usage object. Costs are reported in USD at the Tokenless rate (50% of list) and drawn from your prepaid balance. Prompt-cache activity is broken out on all three surfaces — cache_read_input_tokens (cheaper cached reads) and cache_creation_input_tokens (writes) — so you can see cache savings.

usage.json
{
  "prompt_tokens": 18,
  "completion_tokens": 224,
  "total_tokens": 242,
  "prompt_tokens_details": { "cached_tokens": 0 },
  "cache_creation_input_tokens": 0,
  "cache_read_input_tokens": 0,
  "cost": 0.0028
}

Balance & billing

Tokenless is prepaid. Load a balance, and each request draws it down at the metered rate. Turn on auto-reload to recharge automatically when your balance gets low. If auto-reload is off and your balance reaches $0, API access pauses immediately — no overage, no debt.

Holds. Each request first places a short-lived hold on your balance for its worst-case cost (based on max_tokens), then refunds the difference the moment we meter the actual tokens — so you're only ever charged for what you use. A request needs enough balance to cover that hold; if it doesn't, it's rejected with 402 before any tokens are spent. High reasoning levels raise max_tokens, so they hold more.

Errors

StatusMeaning
400Invalid request (bad body, or wrong surface for the model).
401Missing or invalid API key.
402Insufficient balance — top up to continue.
404Unknown model id.
429Rate limited — slow down and retry.
500Something went wrong on our side.
502Model temporarily unavailable — retry.

Ready to build?

Grab a key and make your first call in under a minute.