/v1/auth/configWhat signing in and up offers: {providers, captcha: {provider, siteKey} | null, legal: {termsVersion, privacyVersion}}.
TOKEN ENGINE GATEWAY
OpenAI-compatible model calls, plus Token Engine’s own APIs for accounts, billing, channels and the model catalog.
https://api.rstoken.ai/v1Create an account and an API key in the console, export the key as TOKEN_ENGINE_API_KEY and send a first request. Every public endpoint lives under /v1: OpenAI SDKs take https://api.rstoken.ai/v1 as their base URL, and a model is named by its catalog id, such as zai-org/glm-5.3-flash (GET /v1/models lists the ones your key may call).
curl https://api.rstoken.ai/v1/chat/completions \
-H "Authorization: Bearer $TOKEN_ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "zai-org/glm-5.3-flash", "messages": [{"role": "user", "content": "Hello"}]}'
# pip install openai
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.rstoken.ai/v1",
api_key=os.environ["TOKEN_ENGINE_API_KEY"],
)
reply = client.chat.completions.create(
model="zai-org/glm-5.3-flash",
messages=[{"role": "user", "content": "Hello"}],
)
print(reply.choices[0].message.content)
// npm install openai, save as first-request.mjs, run: node first-request.mjs
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.rstoken.ai/v1",
apiKey: process.env.TOKEN_ENGINE_API_KEY,
});
const reply = await client.chat.completions.create({
model: "zai-org/glm-5.3-flash",
messages: [{ role: "user", content: "Hello" }],
});
console.log(reply.choices[0].message.content);
Inference and the model catalog use an API key: Authorization: Bearer sk-... (or x-api-key: sk-..., as Anthropic's SDKs send it). A key is shown once, when it is created, and can never be read again. Revoking or disabling it takes effect on its next request. Account management uses the JWT that POST /v1/auth/login returns, as Authorization: Bearer <JWT>: it names the session that sign-in started and works while that session does, for JWT_EXPIRES_IN at most (signing out, with the cookie or with the JWT, ending the session from the Sessions list or with every other, and a password change end it); the console uses a session cookie instead (HttpOnly; __Host-rt_session when the site is served over https, rt_session over plain http), which scripts do not need. Every request a session cookie authenticates that changes something must send the header X-RunToken-CSRF: 1 (else 403 csrf_required), and so must sign-in, every sign-up step, password-reset requests and resets, and re-authentication, whatever Authorization header they carry, and signing out whenever a session cookie comes with it. Other requests authenticated by a bearer JWT need no such header. Sign-up proves the email address with a 6-digit code before the account exists, and opens it without an API key: create keys with POST /v1/keys. The billing webhook is verified by the provider’s signature.
An error’s body has one of two shapes, by the endpoint. The OpenAI-compatible endpoints — GET /v1/models, POST /v1/chat/completions, POST /v1/completions, POST /v1/embeddings and POST /v1/responses — answer as OpenAI does, so an OpenAI SDK raises their errors as its own (Anthropic's Messages API answers in Anthropic's shape instead: see there): {"error": {"code": "<code>", "message": "<sentence>", "type": "invalid_request_error", "param": "temperature"}}. type is the kind of error (invalid_request_error, rate_limit_error, server_error, upstream_error), and param, there only when one field of the request is at fault, names it. A body they cannot read is 400 invalid_request, or 413 request_too_large past the size limit. A name the API looks up — the model, the provider field, a provider or fallback model in runtoken or an X-RunToken-* header — with a control character in it (a NUL, a line break) is 400 invalid_request with param naming it, before anything is reserved or charged. Message content and the other fields only the provider reads are sent on as they are, a NUL included. Each of their answers, errors included, carries the request’s id in its X-RunToken-Id header. An API key refused by any endpoint that takes one is answered in this shape too: 401 invalid_api_key, with a message saying when the key has expired or is disabled, or when its secret was rotated and the old one’s overlap has ended.
Two refusals of a model call say how to put it right. 404 model_not_found: no model on sale has that id. Where the name is a catalog id’s without its vendor prefix, or under another one, the message names it (The model 'glm-5.3' does not exist. Did you mean 'zai/glm-5.3'?); else it says that ids take the form <vendor>/<model> and that GET /v1/models lists the ones the key can use; a model taken off sale “is no longer available”, and one on sale that cannot be served for the moment “is not available right now”. The model is checked before the balance, so a wrong name is reported even with nothing to spend. (A model the key may not use is 403 model_not_allowed.) 402 insufficient_credits: nothing is left to spend. The message gives the balance and the console’s Billing page, where credit is added — for an organization’s key, by an owner or admin of the organization.
Every other endpoint answers {"error": "<code>", "message": "<sentence>"}, and so does a request the API cannot read: 400 invalid_json, 413 body_too_large, 415 unsupported_media_type, 400 bad_url, and 404 not_found for a path it has no endpoint for. A body an endpoint cannot take is 400 invalid_request with its issues as well, each naming the field (path) and what is wrong with it; message says the first of them (name is too long: at most 400 characters). A NUL character (U+0000) in a value these endpoints store or search for — a name, a note, a filter, an id in the path — is 400 invalid_request, never a server error.
/v1/auth/configWhat signing in and up offers: {providers, captcha: {provider, siteKey} | null, legal: {termsVersion, privacyVersion}}.
/v1/auth/signup/startBegin a sign-up with {email, password, name?, referralCode?, agree: true, marketingOptIn?, captchaToken}: a 6-digit code is emailed, valid 10 minutes. An address is emailed at most once a minute and five times a day: sooner, 429 rate_limited, with reason: "code_just_sent" within the minute. Answers 202 {attemptId, email, codeExpiresAt}. Passwords are 8–128 characters (72 bytes at most), not a common password and not the email address (400 weak_password).
/v1/auth/signup/verifyFinish it with {attemptId, code}: opens the account, its address verified, and answers 201 {user}. Five wrong codes end the sign-up (400 code_attempts_exceeded). When the Terms or the Privacy Policy changed since the sign-up began: 409 legal_version_outdated with the versions in force, nothing used up; send the code again with {termsVersion, privacyVersion} once the person agreed to them.
/v1/auth/signup/resendEmail a new code with {attemptId}; at most one a minute and five a day per address (within the minute, 429 with reason: "code_just_sent").
/v1/auth/loginExchange {email, password} for {user, token}, the token a JWT for scripts. After repeated failures a CAPTCHA token is needed as well (400 captcha_required).
/v1/auth/password/forgotEmail a password reset link with {email, captchaToken}; always 202, whether or not an account has the address.
/v1/auth/password/resetSet a new password with {token, password} from the link (valid once, for 30 minutes); every existing session and JWT of the account ends.
/v1/auth/verify-email?token=…A link that verifies the account’s email address, opened in a browser: it changes nothing, and answers 303 to the console’s confirmation page, which confirms with the POST below. Mail scanners that open the link verify nothing.
/v1/auth/verify-emailVerify the address with {token} from the link (valid 24 hours): {status: "verified" | "already_verified"}, and nothing else about the account. 409 token_used, 400 token_expired, 400 invalid_token. Verifying grants the trial credit when its other condition, a card on file, is met.
/v1/auth/verify-email/resendEmail the signed-in user a new verification link: 202 {sent: true, email, expiresAt}. One a minute and five a day (429 rate_limited with Retry-After); 409 email_already_verified; 503 email_unavailable where the deployment sends no email.
/v1/auth/:provider/startA browser’s sign-in with a provider (google, microsoft or x): ?intent=signin (with ref and next), link or reauth. The round trip is bound to the browser by an HttpOnly cookie (__Host-rt_oauth over https) and uses PKCE (and, for Google and Microsoft, an OpenID Connect nonce); the callback, GET /v1/auth/:provider/callback, signs the browser in or sends it to complete a sign-up, and a failure comes back as /console/login?error=<code>.
/v1/auth/signup/completeComplete a sign-up that began with a provider, in the browser that holds it, with {email?, agree: true, marketingOptIn?, referralCode?, captchaToken}: 201 {user} for the address the provider vouches for, else a code is emailed (202) and /v1/auth/signup/verify finishes it. GET /v1/auth/signup/pending says who is signing up.
/v1/auth/wallet/nonceA nonce for signing in with a Web3 wallet (Sign-In with Ethereum, EIP-4361), where the deployment offers it: {nonce}, valid 5 minutes and once. ?intent=link or reauth need the browser’s session.
/v1/auth/wallet/verifySign in with {message, signature, intent}: an EIP-4361 message for this site carrying the nonce, signed by the wallet (an ordinary wallet’s signature is checked offline). Signs the browser in (200) or begins a sign-up to complete (202, a wallet has no email address); 400 wallet_expired or wallet_failed otherwise.
/v1/auth/logoutSign out: ends the session the cookie names and clears the cookie; with a JWT, ends the session it was issued with, and so the JWT. 204.
/v1/auth/reauthProve again who is using a console session with {password}, before a sensitive action within the next 10 minutes. 204; 403 invalid_credentials; 400 session_required with a bearer JWT (sign in again instead).
/v1/meRead the current account: {user, legal, orgs:[{orgId,name,role}], orgRemovals:[{id,orgId,orgName,removedAt,contact,disabledKeys}]}. orgRemovals lists the organizations an owner or admin took the caller out of in the last 90 days (not one they were added back to since), with whom to ask (contact, or null) and how many of the keys they issued were disabled with the removal (disabledKeys, 0 when none were).
/v1/me/sessionsThe account’s live sessions: {sessions:[{id, createdAt, lastSeenAt, ip, userAgent, method, current}]}.
/v1/me/sessions/:idEnd one of the account’s sessions. 204; 404 session_not_found. Ending another one than the caller’s needs a sign-in or re-authentication within 10 minutes, else 403 reauth_required.
/v1/me/sessions/revoke-othersEnd every other session, and every JWT issued with one: {revoked}. The console's own session is replaced by a fresh cookie, which ends the JWT its sign-in returned. Needs a sign-in or re-authentication within 10 minutes, else 403 reauth_required.
/v1/me/identitiesThe account’s sign-in methods: {identities:[{id, provider, display, email, createdAt, lastUsedAt}], hasPassword}.
/v1/me/identities/:idRemove a sign-in method. 204; 409 last_sign_in_method when it is the only way in; needs a sign-in or re-authentication within 10 minutes (403 reauth_required).
/v1/me/legal/acceptAccept the Terms of Service and Privacy Policy in force with {termsVersion, privacyVersion} (the versions /v1/me names): {legal}; 409 legal_version_outdated for any other version.
/v1/me/preferencesGive or withdraw consent to marketing email with {marketingOptIn}: {preferences: {marketingOptIn}}. Each change is recorded with when it was made; agreeing again keeps the first time.
/v1/me/trialClaim the trial credit when both its conditions, a verified email address and a card on file, came true while the trial was switched off; otherwise it is granted by itself when the second is met. {outcome, trial}: outcome is granted, or why not (already_granted, email_unverified, no_card, not_eligible, refused, disabled, …), and trial the trial’s status, as /v1/me shows it.
/v1/meChange the display name with {name} (1–80 characters).
/v1/me/passwordChange the password with {currentPassword, newPassword}. Every other session and JWT ends; the answer carries a new token.
/v1/me/emailChange the account’s email address with {email}: a 6-digit code is emailed to the new address, valid 10 minutes (202 {changeId, email, codeExpiresAt}), at most one a minute and five a day per address. Needs a sign-in or re-authentication within 10 minutes (403 reauth_required); 400 disposable_email. POST /v1/me/email/verify with {changeId, code} changes it: every other session and JWT ends, the answer carries a new token, and the old address is told.
/v1/me/exportA copy of the personal data Token Engine holds about the account, as one JSON document: profile, sign-in methods, sessions of the last six months, API key metadata (with where each key was last used from), organizations, legal acceptances, consents, trial, balance, cards and invoices. It holds no secret. Needs a sign-in or re-authentication within 10 minutes, else 403 reauth_required; five an hour, then 429 with Retry-After.
/v1/me/closeClose the account with {confirmEmail}, its address typed. Needs a sign-in or re-authentication within 10 minutes. Every API key and session ends at once; 409 with blockers while it is the only owner of an organization with other members, its balance is below zero, or reseller commission or a payout is open. Its personal details are anonymised 30 days later.
/v1/keys?orgId=…List personal API keys, or an organization’s with orgId. expiringWithin=<days> (1 to 365) lists only the keys not yet expired whose expiry comes within that many days, soonest first; status=active|disabled|expired only the keys in that state. To whoever may manage a key — its own user, an organization’s owners and admins, or the member who issued it — each key also shows lastUsed: {at, ip, userAgent, requestId, model} of the request that last recorded its use (at most once a minute; null before any), kept six months at most.
/v1/keys/:idOne key as the list shows it, to whoever may see it there.
JWT/v1/keys/:id/usage?days=30A key’s daily trend: {keyId, from, to, rows}, one row per UTC day of the last days days (1 to 90, 30 when left out, today so far included) with its requests, failed requests, tokens and chargeCredits (and each token sum and the charge as an exact decimal string, …Exact), as /v1/usage?group_by=day counts them, for this key alone.
/v1/keys/bulkDisable, enable or delete several keys at once with {action: "disable"|"enable"|"revoke", ids: […], orgId?}: 1 to 100 keys of the personal account, or of the organization orgId names. All of them change, or none: a key that is not the account’s is 404 key_not_found, and the rules of PATCH and DELETE apply to each (403 insufficient_role, 403 key_held). The answer is {action, changed, unchanged}: the ids it changed, and those already as asked. Each key stops, or works again, from its next request.
/v1/keysCreate keys with {name, orgId?, expiresAt?, spendCap?: {credits, window: "daily"|"monthly"|"total"}, allowedModels?, quantity?} (the older monthlyQuotaCredits is still accepted). An expiresAt must be in the future: one now or earlier is refused with 400 invalid_request (omit it, or pass null, for a key that never expires). allowedModels takes exact ids of models GET /v1/models lists and vendor/* wildcards (omit it, or pass ["*"], for every model); an id it does not list is refused with 400 invalid_request, as a mistyped one is, and a request for any other model is refused with 403 model_not_allowed. A key belongs to the personal account by default; name an organization and its usage is charged to that organization’s shared balance. allowedModels takes exact catalog model ids and vendor/* wildcards (omit it, or pass ["*"], for every model); any other model is refused with 403 model_not_allowed. A quantity above one returns {data:[…]}. This response is the only time a key’s secret is shown.
/v1/keys/:idRename, disable or re-enable a key, or change its expiry (to a time in the future, or null for none), spend cap, allowed models or routing defaults. A model the key already allows, or falls back to, is kept when sent back with the rest, even once GET /v1/models no longer lists it; one added is checked as on creation. A deleted key is 404 key_not_found.
/v1/keys/:id/rotateGive a key a new secret and keep everything else — its id, limits, budgets, routing and usage — with {overlapSeconds?}: how long the replaced secret keeps working, a whole number of seconds from 0 (it stops at once) to 86400, an hour when left out. After that the old secret is refused with 401 invalid_api_key, saying it was rotated; rotating again during an overlap stops the earlier secret at once. The answer is the key as listed — while the old secret works, with its previousKeyPrefix and previousKeyExpiresAt — and its new secret, shown this once. Whoever may change a key may rotate it; an expired key is refused with 409 key_expired.
/v1/keys/:idRevoke one of the caller’s API keys.
JWTEvery user has a personal account. Organizations are optional: members share the organization’s own balance, quota, usage and invoices. Without an orgId, keys and billing act on the personal account. An endpoint that acts for an organization — its orgId in the path, the query or the body — refuses someone who is not its member with 403 not_a_member, a role that may not make the change with 403 insufficient_role, and an organization a Token Engine administrator disabled with 403 org_disabled.
/v1/orgsCreate an organization with {name}; the creator becomes its owner.
/v1/orgsList the organizations the caller belongs to, with their role.
JWT/v1/orgs/:orgId/membersList an organization’s members; the caller must be one.
JWT/v1/orgs/:orgId/membersAdd an existing user with {email, role}; owner or admin only. The person added is emailed the organization’s name, their role and who added them.
/v1/orgs/:orgId/members/:userIdChange a member’s role with {role: "owner" | "admin" | "member"}: the membership, {id, userId, orgId, role}. Granting or revoking admin or owner is an owner’s call (403 insufficient_role); anyone may lower their own role, nobody raise it (403 cannot_escalate_own_role). The last active owner cannot be demoted (409 last_owner); 404 member_not_found. The member is emailed when someone else changes their role.
/v1/orgs/:orgId/members/:userIdRemove a member: owners remove anyone, admins plain members; removing yourself is leaving. 204. Add ?disableKeys=true to disable, in the same step, every key of the organization’s that the member issued and that still works: 200 {disabledKeys:[{id,name,keyPrefix}]}; each stays disabled until an owner or admin enables it again. Otherwise the organization’s API keys are the organization’s and keep working: revoke any the member may still hold a copy of. Someone removed by someone else is emailed who removed them.
/v1/orgs/:orgId/leaveLeave an organization. 204; 409 last_owner for its last active owner: make someone else an owner first.
/v1/orgs/:orgId/auditThe organization’s activity, newest first, for its owners and admins: who added or removed members, changed roles, keys, budgets, rate limits, routing defaults or content logging, and credit adjustments. ?since=&until= (ISO 8601), action or actionPrefix (member.), limit (up to 100) and offset: {data:[{id, createdAt, action, targetType, targetId, actor, before, after, reason}], total}. actor.kind is member (with its email and name), former_member, staff (Token Engine staff, never named) or system; no client address is shown, before/after only for the changes listed above and only their own settings, and reason is null (an operator’s reason is not shown). 403 insufficient_role for a plain member.
/v1/orgs/:orgId/audit/export.csvThe same activity as CSV, newest first, at most 50,000 rows; X-RunToken-Export-Rows and X-RunToken-Export-Truncated say how many and whether more matched. The export appears in the activity as audit.export.
When you call a third-party Provider’s model, you must also comply with that Provider’s usage policies, license terms and content rules; open-weight models are additionally subject to their licenses (Terms §7.1). The catalog links them where they are recorded: a provider’s policy_url, a model’s license_url. For endpoints labeled “Token Engine First-Party” (first_party: true), Token Engine acts directly as the inference provider and is bound by the additional commitments for First-Party Services in the Terms of Service and the Privacy Policy (Terms §3.3).
/v1/modelsList the enabled models this key may call, their providers and customer prices, and each model’s license_url (null when none is recorded), the API endpoints it is served on (api_endpoints), the capabilities its providers serve it with (supports) and when its price took effect (pricing.updated_at): see what a model is served on.
/v1/catalogThe same catalog for a signed-in session, which the console browses.
JWT/v1/catalog/:vendor/:modelOne model with the providers serving it (endpoints): each one’s customer price and when it took effect (pricing.updated_at), regions and data policy, its usage policy (policy_url, null when not recorded), whether it is Token Engine First-Party (first_party), the API endpoints it serves the model on (api_endpoints) and the values it takes for each declared parameter (parameters).
/v1/key-test/holdWhat a test message holds while it runs, for the console’s key test: the body is the chat request it will send (at most 16 KB), ?orgId= for an organization’s prices. Answers {"model", "hold"}: the most the request may hold on any deployment of the model at your prices, in credits — the same bound a request’s reservation takes — or null when no deployment would take it. Nothing is sent or charged; 404 model_not_found for an unknown model.
/v1/chat/completionsCreate an OpenAI-style chat completion, streaming over SSE if asked. A max_tokens (or max_completion_tokens) above the model’s maximum output is refused 400 invalid_request before anything is held or sent. Send an Idempotency-Key to retry without paying twice; below are what a stream cut short is billed, how a reasoning model counts its tokens, and how to ask for a service_tier.
/v1/completionsCreate an OpenAI-style text completion from a prompt (the legacy completions API), streaming over SSE if asked. A chat-only model answers it through chat.
API Key/v1/embeddingsCreate OpenAI-style embeddings with a catalog model that produces them; metered by input tokens.
API Key/v1/responsesCreate a response with OpenAI’s Responses API (client.responses.create), streaming over SSE if asked. Every chat model serves it through chat completions, and it is billed and limited as the chat request it stands for; what it takes and refuses is in the Responses API.
/v1/messagesCreate a message with Anthropic's Messages API — what Claude Code and Anthropic's SDKs send — streaming over SSE if asked, on every chat model. It is served through chat completions and billed and limited as the chat request it stands for; POST /v1/messages/count_tokens estimates a request's input tokens. See Anthropic's Messages API and Claude Code.
An answer is OpenAI’s, and its model, like every stream chunk’s, is the catalog id of the model that served the request: the one you sent, or the fallback model’s when one stood in. Its headers say which request it was and who served it: X-RunToken-Id, the id GET /v1/generation looks it up by, and X-RunToken-Provider, the provider. An answer in one piece also has X-RunToken-Cost, what it was charged in credits, and X-RunToken-Tokens, its prompt and completion tokens (prompt/completion); a stream’s charge is in GET /v1/generation once it is recorded, a moment after it ends.
A request that times out, or whose connection drops, may already have reached the provider: sent again as it is, it runs again and is billed again. The OpenAI SDKs retry such a request on their own, and send no key with it. Send an Idempotency-Key header — 1 to 255 printable ASCII characters, a new one for each request and the same one on each retry of it — and the request runs, and is billed, once however often it is sent. With the Quickstart’s client:
import uuid
# One key per request: the SDK's own retries send it again.
reply = client.chat.completions.create(
model="zai-org/glm-5.3-flash",
messages=[{"role": "user", "content": "Hello"}],
extra_headers={"Idempotency-Key": str(uuid.uuid4())},
)
import { randomUUID } from "node:crypto";
// One key per request: the SDK's own retries send it again.
const reply = await client.chat.completions.create(
{ model: "zai-org/glm-5.3-flash", messages: [{ role: "user", content: "Hello" }] },
{ headers: { "Idempotency-Key": randomUUID() } },
);
/v1/completions, /v1/embeddings and /v1/responses take it as chat does; a replayed response keeps its id.X-RunToken-Id and X-RunToken-Cost, a stream as the same events — marked Idempotent-Replayed: true. It calls no provider and is not charged.409 idempotency_in_progress with Retry-After.422 idempotency_key_reused. A request refused before it reached a provider (402, 404, a refused field, a rate limit) keeps nothing, so a corrected retry with the same key runs.422 idempotency_response_too_large naming the original request, and is not run again; send it with a new key to run it again.What a request that does not finish is billed:
Idempotency-Key: the provider’s work is cancelled, and the request is billed for what the provider had counted for it where it had said, else for its prompt and the output you had been sent, the reasoning a model showed included. Its record’s errorCode is client_closed. A client gone before its request reached a provider costs nothing./v1/responses, an error event and then response.failed).tokenSource (in /v1/generation and /v1/usage/events) is then estimated; provider means every count is the provider’s own.POST /v1/responses takes requests in OpenAI’s Responses API and answers in its format, on every chat model in the catalog — each lists responses in its api_endpoints. Token Engine turns each request into the chat completion it stands for, sends that to the provider and turns the answer back, so a response is billed, rate-limited, held to budgets and failed over exactly as that chat request would be — the same prices, long-context tiers and service tiers — and its record’s request kind is response. Nothing is stored: every request carries the whole conversation in input, and a response says "store": false. A response’s id is resp_ and the request’s X-RunToken-Id without its dashes.
curl https://api.rstoken.ai/v1/responses \
-H "Authorization: Bearer $TOKEN_ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "zai-org/glm-5.3-flash", "instructions": "Answer in one sentence.", "input": "Hello"}'
# With the Quickstart's client
response = client.responses.create(
model="zai-org/glm-5.3-flash",
instructions="Answer in one sentence.",
input="Hello",
)
print(response.output_text)
# Streamed: OpenAI's events, the text in response.output_text.delta
with client.responses.stream(
model="zai-org/glm-5.3-flash", input="Hello"
) as stream:
for event in stream:
if event.type == "response.output_text.delta":
print(event.delta, end="")
// With the Quickstart's client
const response = await client.responses.create({
model: "zai-org/glm-5.3-flash",
instructions: "Answer in one sentence.",
input: "Hello",
});
console.log(response.output_text);
// Streamed: OpenAI's events, the text in response.output_text.delta
const stream = await client.responses.create({
model: "zai-org/glm-5.3-flash",
input: "Hello",
stream: true,
});
for await (const event of stream)
if (event.type === "response.output_text.delta") process.stdout.write(event.delta);
model; input, a string or a list of items — message items with the roles user, system, developer and assistant (parts input_text, input_image with an image_url, and an assistant’s output_text and refusal), function_call and function_call_output items; instructions; max_output_tokens, held to the model’s maximum output as max_tokens is; temperature, top_p and stream; function tools with tool_choice (auto, none, required or {"type": "function", "name": "…"}) and parallel_tool_calls; reasoning.effort, as chat’s reasoning_effort; text.format (text, json_object or json_schema) and text.verbosity, which a provider that has no verbosity ignores; service_tier, as in Service tiers; user, safety_identifier, prompt_cache_key, prompt_cache_retention and top_logprobs. Each is judged as its chat field is by the provider that serves the request: Anthropic takes a temperature up to 1, for example. A function tool’s strict is sent only when you set it.store, include, metadata (echoed back), reasoning.summary (no summary is written), truncation: "disabled", background: false, stream_options, and reasoning items in input, which are dropped: they carry nothing a chat model reads.400 unsupported_parameter naming the field, before anything is held or sent: previous_response_id and conversation (no response is stored), prompt (stored prompts), background: true, truncation: "auto" (an input too long for the model is refused, never cut), any tool but a function — web_search, file_search, code_interpreter, mcp and the other hosted tools —, an image by file_id, input_file and input_audio parts, and any field not named here.output items in order — the reasoning a model shows (reasoning, with reasoning_text), its message (output_text, or a refusal) and each function_call — and usage: input_tokens, input_tokens_details.cached_tokens, output_tokens, output_tokens_details.reasoning_tokens and total_tokens, what the request was billed for. status is completed, or incomplete with incomplete_details.reason max_output_tokens (the limit stopped it, billed all the same) or content_filter. Its model is the catalog id, as a chat answer’s is. It also has created_at (Unix seconds), service_tier when the provider reported the tier it served the request at, "store": false, and the request’s parameters echoed as OpenAI echoes them: instructions, max_output_tokens, temperature, top_p, tools, tool_choice, parallel_tool_calls, text, reasoning and metadata.type and numbered by its sequence_number: response.created and response.in_progress; for each output item response.output_item.added, response.content_part.added, its deltas — response.output_text.delta, response.refusal.delta, response.reasoning_text.delta, or response.function_call_arguments.delta for a function call —, their .done events and response.output_item.done; and last one response.completed (or response.incomplete) carrying the whole response and its usage. There is no [DONE]. A stream that breaks once it began ends with an error event (code, message) and then response.failed, whose response.error has the same code, and is billed as a chat stream cut short is. The codes are OpenAI’s where one fits: server_error when the provider’s stream broke, the request ran out of time, or the response could not be completed after it was delivered; client_closed, Token Engine’s own, ends the record of a stream whose client had gone, which that client never sees.POST /v1/messages takes requests in Anthropic's Messages API and answers in its format — a message, or its SSE events (message_start, the content blocks, message_delta, message_stop) — on every chat model in the catalog; each lists messages in its api_endpoints. As with the Responses API, Token Engine turns each request into the chat completion it stands for, so it is billed, rate-limited and failed over exactly as that chat request would be, and its record's request kind is messages. The key may be sent as x-api-key or Authorization: Bearer, and errors take Anthropic's shape: {"type": "error", "error": {"type": "rate_limit_error", "message": "…"}}.
tool_use and tool_result blocks, tools and tool_choice, temperature, top_p, stop_sequences and metadata.user_id. A max_tokens above the model's maximum output is held to that maximum rather than refused. output_config, context_management and cache_control are accepted and not sent: each model caches as it is served. thinking is not sent either, but carried over as the provider declares: a request without it, or with disabled, is sent reasoning_effort: "none" where the provider takes none; one asking for thinking is sent the effort the provider declares for that. Where it declares nothing for the case the request goes as it is, never refused, and the model reasons as it is served. A model's reasoning is answered as thinking blocks when the request asks for thinking and does not omit it. top_k, documents and Anthropic's hosted tools are refused with 400.export ANTHROPIC_BASE_URL=https://api.rstoken.ai
export ANTHROPIC_AUTH_TOKEN=$TOKEN_ENGINE_API_KEY
export ANTHROPIC_MODEL=zai-org/glm-5.3-flash
export ANTHROPIC_DEFAULT_HAIKU_MODEL=zai-org/glm-5.3-flash
claude
A reasoning model — one whose supports lists reasoning in /v1/models, and marked so on the console’s Models page — thinks before it answers, and the tokens it thinks with are output tokens: billed once, at the output price, inside the completion tokens the provider reports, and counted against max_tokens (or max_completion_tokens). How many of them were reasoning is the provider’s own count, where it reports one, in the usage it answered with, passed on as it came: OpenAI’s in usage.completion_tokens_details.reasoning_tokens, some providers’ as usage.reasoning_tokens. Either way the bill is the completion_tokens they are part of. A limit too small for the thinking ends the answer before any text — finish_reason: "length" and an empty content — and the tokens used are billed all the same. Leave the limit out, as the examples above do, and the answer may run to the model’s maximum output (max_output_tokens in /v1/models), which the credit held while the request runs covers; or set one with room for the thinking. Some models stream the reasoning they show beside the answer, as reasoning_content or reasoning in each delta.
Some providers serve a model at service tiers besides the standard one — flex, slower and cheaper, or priority, faster and dearer — each at prices of its own. A request asks for one with "service_tier": "flex"; default asks for the standard tier, and auto, or no service_tier, for the provider’s default, which is the standard tier unless the provider says otherwise. Which tiers each provider takes, and at what prices, are on the pricing page and in GET /v1/catalog/:vendor/:model (each endpoint’s parameters.service_tier and pricing.service_tiers); /v1/models and /v1/catalog give each model’s lowest price per tier in its own pricing.service_tiers, present only when a provider offers one. A request is charged at the tier the provider reports serving it at, or at the one it asked for where the provider does not say. A provider may serve a request at the standard tier instead of the one asked — OpenAI serves priority at default past its limits — and it is then charged the standard price; while it runs, the credit held covers the dearer of the two. A tier no provider of the model takes, or takes but has no price for, is refused with 400 naming the values it may take, before anything is sent or charged.
Every model in /v1/models and /v1/catalog lists the API endpoints it is served on in api_endpoints, by the same rules that route its requests, and each provider in /v1/catalog/:vendor/:model lists its own:
chat, responses and completions — every model that writes text, on every provider. /v1/responses is served through chat; a legacy completion goes to the provider’s own /v1/completions where the model’s supports lists legacy_completions, and through chat everywhere else (Anthropic always), its prompt sent as the one user message.embeddings — a model that produces embeddings (embedding_out among its modalities), and nothing else, on the providers that embed: OpenAI-compatible ones, not Anthropic.A request to an endpoint the model does not list is refused before anything is held or sent: 400 invalid_request (param: "model") for an embedding model on a text endpoint or a text model on /v1/embeddings, 503 no_available_provider where none of its providers serves the endpoint. A provider that could serve none of a model’s endpoints, or that Token Engine is not routing to for the moment, is not listed for it, and a model none of whose providers can serve it is not listed at all.
supports lists a capability only where a provider serving the model provides it, never merely because the model is said to: streaming and tools; json_mode, a JSON response_format (Anthropic takes none); vision, images in; reasoning, a model that reasons before it answers (see Reasoning models) or takes reasoning_effort; legacy_completions, a model that writes text whose legacy completions a provider of it answers natively, as above — never an embedding model, which serves no completions however it is described. Each price says when it took effect in pricing.updated_at (ISO 8601): a provider’s, the start of its price in effect; a model’s, the latest of its providers’, since its price is made of theirs. The pricing page shows that day for each model.
Two request fields take values that differ by model and provider: reasoning_effort (on /v1/responses, reasoning.effort) and service_tier. A provider whose deployments declare one lists it in the endpoint’s parameters in GET /v1/catalog/:vendor/:model — {"reasoning_effort": {"values": ["low", "medium", "high"], "default": "medium"}} — with the values a request may send to that provider and the one it uses when the field is left out (default: null when not stated or when its deployments differ; it may be a value a request cannot send itself). The console’s Models page shows them as chips beside each provider. A request is routed only to a provider that takes its value; when none of the model’s does, it is refused with 400 naming the values it may take, before anything is held or sent.
A provider that declares neither follows Token Engine’s own rules: reasoning_effort is passed as sent to OpenAI-compatible providers and refused by Anthropic, which has no such field; service_tier takes auto and default (the standard tier) on any provider, and flex or priority only where a provider declares the tier and prices it (Service tiers). The values change as providers do: read them from the catalog when you build a request, rather than writing them into your code.
Chat completions also take enable_thinking, a boolean. false asks for no reasoning: it is honoured by a provider whose reasoning_effort values include none (sent as reasoning_effort: "none") and by Anthropic, and refused with 400 unsupported_parameter by any other. true asks for reasoning: it is honoured by a provider that declares the effort to send for it, or that reasons by default, and refused with 400 unsupported_parameter, naming the efforts it takes, by any other: send reasoning_effort there. It is never ignored. When none of a model's providers would honour the value sent, the request is refused before anything is held. Sent with a reasoning_effort that contradicts it, or as anything but true or false, the request is refused with 400 invalid_request.
A provider may serve a model that does not reason at all. There, enable_thinking: false and reasoning_effort: "none" are accepted and not sent, since there is nothing to turn off; enable_thinking: true and any other reasoning_effort are refused with 400 unsupported_parameter saying the model does not reason. When some of a model's providers reason and others do not, each judges the request by what it declares, and the request goes to one that can serve it. Anthropic Messages requests are never refused for thinking.
What the account’s requests used and cost, and what became of one. An API key reads its own account’s, whichever of its keys made the request; a signed-in user their personal account’s, or an organization’s with orgId for its members. from and to are a date (2026-09-01, midnight UTC) or a date and time with its offset (2026-09-01T08:00:00Z), the range [from, to). Usage is shown for the last 12 months: an earlier from is moved up to it, and a range that ends before it is refused with 400 range_too_old and availableFrom. A request’s records arrive a moment after its answer, so one looked up at once may answer 404 for a few seconds. Credits are US dollars.
/v1/usage?from=…&to=…&group_by=…&orgId=…Totals over the range by group_by: model or key (highest charge first), member (the user who issued each key, by id — '' for keys issued before issuers were recorded; the organization’s members list names them), or day (every UTC day of the range, oldest first, days without requests included). By key and member, every other key (or issuer) the account had in the range — issued before its end, not revoked or expired before its start — follows at zero. Answers {groupBy, from, to, rows: [{key, label, requests, requestsFailed, errorRate, promptTokens, completionTokens, cachedTokens, cacheWriteTokens, totalTokens, chargeCredits, promptTokensExact, completionTokensExact, cachedTokensExact, cacheWriteTokensExact, totalTokensExact, chargeCreditsExact}], refused: {requests, retentionDays, max}}: requestsFailed are the requests that reached a provider and failed, errorRate their share of requests (0 to 1, null without a request); requests refused before any provider are not among requests but counted in refused.requests, of those kept (the latest 100 of 7 days, so at least that many; null when they cannot be read), chargeCredits a number; each token sum and the charge is also given as an exact decimal string under its name with Exact added ("totalTokensExact": "5400000000", "chargeCreditsExact": "10000000000.00000005"), every digit kept where a JSON number would round (past 2^53 tokens, or about 15 significant digits of credits); by key each row also has createdByUserId (null when not recorded); from as clipped to the 12 months. from, to and group_by are required.
/v1/usage/events?from=…&to=…&model=…&key=…&cursor=…&orgId=…The requests themselves, newest first, 50 a page: {data: [{requestId, model, provider, requestKind, promptTokens, completionTokens, cachedTokens, cacheWriteTokens, totalTokens, tokenSource, chargeCredits, latencyMs, ttfbMs, status, errorCode, createdAt}], nextCursor}. Narrow them to a model (its catalog id) or an API key (its id); for the next page send nextCursor as cursor with the same filters.
/v1/requests?from=…&to=…&status=…&model=…&key=…&cursor=…&orgId=…The request log: the account’s requests, newest first, 50 a page — those served or failed, as /v1/usage/events gives them, and those refused before any provider was called (a disabled or expired key, no credit, a model that does not exist, a rate limit…), kept for 7 days, the latest 100 of an account, and at most 10 a minute of one key (one of them a rate limit’s): {data: [{requestId, createdAt, outcome, status, statusCode, errorCode, message, model, provider, requestKind, apiKey: {id, name, createdByUserId}, promptTokens, completionTokens, cachedTokens, cacheWriteTokens, totalTokens, tokenSource, chargeCredits, latencyMs, ttfbMs}], nextCursor, refusals: {retentionDays, max}}. status is all (the default), succeeded, failed or refused. A refused request has the HTTP statusCode, the errorCode and the message it was answered with, and nothing used or charged. One sent with a key that does not exist, or was deleted, is no one’s, and is not listed.
/v1/usage/events/export?from=…&to=…&model=…&key=…&orgId=…The account’s usage records over [from, to) as CSV, oldest first, at most 12 months per request and none older than 12 months: created_at, request_id, model, provider, request_kind, status, error_code, api_key_id, api_key_name, prompt_tokens, completion_tokens, cached_tokens, cache_write_tokens, total_tokens, token_source, charge_credits, latency_ms, ttfb_ms. Narrow it to a model (its catalog id) or an API key (its id), as /v1/usage/events. An organization’s with orgId for its members; 404 for any other account.
/v1/generation?id=…One request by its id, the X-RunToken-Id header of its answer: {id, status, model, provider, requestKind, tokens: {prompt, completion, cached, cacheWrite, total}, tokenSource, chargeCredits, latencyMs, ttfbMs, errorCode, createdAt}. chargeCredits is what it was charged, a number. 404 not_found for an id that is not the caller’s, or older than 12 months.
/v1/requests/:requestId?orgId=…One request by its id, served, failed or refused, with what its row in the request log says and more: {requestId, createdAt, outcome, status, statusCode, errorCode, message, stage, model, provider, requestKind, apiKey, promptTokens, completionTokens, cachedTokens, cacheWriteTokens, totalTokens, tokenSource, chargeCredits, latencyMs, ttfbMs, startedAt, finishedAt, ledger, attempts, route}. ledger is the debit its charge is — {transactionId, type: "debit", amountCredits, balanceAfterCredits, createdAt}, exact decimal strings, the amount negative — or null when nothing was charged; attempts how many times it was sent to a provider; route where each attempt went (provider, providerName, firstParty, regions), what it answered and who served it. A failed request’s message says what its error code means; a refused one’s is the sentence it was answered with, found while kept (7 days; an organization’s with orgId). 404 not_found for an id that is not the caller’s, or older than 12 months.
/v1/requests/:requestId/routeHow a request was routed, those routing refused included (an unknown model, no compliant provider, a price ceiling, the credit hold); a request refused before routing — a disabled key, no credit, an invalid body, a rate limit — has no route and answers 404, and is in the request log (GET /v1/requests) instead. Kept for a limited time (7 days by default): {id, runs: [{startedAt, durationMs, model, preferences, excluded, candidates, reservation, attempts, result, …}]}, oldest run first. preferences are the routing settings that applied and where each came from; excluded the providers left out and why; candidates the ones tried in order, with their prices; attempts what each answered; result the status sent and who served it.
/v1/content-logging?orgId=…Whether the bodies of the account’s requests and their answers are kept: {enabled, enabledAt, retentionDays: 90, available}. Off unless turned on; available says whether the deployment can keep them. An organization’s for its members.
/v1/content-logging?orgId=…Turn it on or off with {enabled}: the setting as above. While it is on, each request an API key of the account makes is kept, sealed, for 90 days or until deleted, 256 KiB of each part at most; turning it off keeps no new ones and leaves the others. An organization’s, its owners and admins; 503 content_logging_unavailable to turn it on where the deployment cannot keep them.
/v1/requests/:requestId/contentA kept request and its answer: {id, runId, createdAt, expiresAt, model, requestKind, stream, statusCode, request, response, truncated: {request, response}}. A user reads their own, an organization’s owners and admins its; 404 not_found for any other, or one expired or deleted.
/v1/requests/:requestId/contentDelete what was kept of a request, under the same rule. 204; 404 not_found once it is gone.
/v1/content-logs?orgId=…Delete every kept request of the account at once: {deleted}. An organization’s, its owners and admins.
Limits and defaults for every request of an account, whichever of its keys makes it. A key’s own are set with the key (POST and PATCH /v1/keys); a request’s routing preferences override the defaults field by field, and data-policy and region requirements only tighten. A change applies from the account’s next request. A signed-in user sets their own; with orgId, an organization’s members read its and its owners and admins change them.
A request sets its own in a runtoken object beside model in its body, or — for an SDK that cannot add a field to the body — in one X-RunToken-* header per field; a field the body sets wins over its header. Every field may be left out, and null counts as left out. An unknown field or a value that is not valid is refused with 400 invalid_request, naming it (runtoken.providers.order, say, or the header), before anything is sent or charged.
route (X-RunToken-Route): cheapest, fastest, balanced, or pinned to follow providers.order as given, which it needs.providers.order (X-RunToken-Providers-Order): provider slugs, tried first and in this order, at most 20; providers.allow_fallbacks (X-RunToken-Providers-Allow-Fallbacks, true or false): whether the model’s other providers may follow them — true unless set.fallback_models (X-RunToken-Fallback-Models): catalog model ids to try next, in order, at most 10: each one the API key may use, or the request is refused with 403 model_not_allowed.max_price.input_per_1m and max_price.output_per_1m (X-RunToken-Max-Price-Input-Per-1M, X-RunToken-Max-Price-Output-Per-1M): the most a million tokens may cost you, in US dollars, as a decimal string; a provider dearer than that is not used.data_policy.no_training and data_policy.max_retention_days (X-RunToken-Data-Policy-No-Training, X-RunToken-Data-Policy-Max-Retention-Days): only providers recorded as not training on requests, or keeping them at most that many days.regions (X-RunToken-Regions): region codes such as us, eu or eu-de; only providers in one of them.A header that takes a list takes it comma-separated: X-RunToken-Regions: us,eu. In the body: "runtoken": {"route": "cheapest", "max_price": {"output_per_1m": "8.00"}}.
/v1/budgets?orgId=…The account’s budgets per window, the current UTC day, the current UTC month and in total: {budgets: {daily, monthly, total}, held: {daily, monthly, total}, canEdit}, each budget null or {credits, spentCredits, reservedCredits, windowStart, resetsAt}. held is a ceiling a Token Engine administrator set.
/v1/budgets?orgId=…Replace them with {daily?, monthly?, total?}, credits from 0.00000001 to 10,000,000: a window left out or null has none. A request past a budget is refused with 402 budget_exceeded; lowering one below what was spent refuses new requests and reverses nothing. A held budget may be lowered, not raised past its hold or removed (403 budget_held).
/v1/routing?orgId=…The account’s routing defaults: {routing, version, platform}, routing null when none are set, platform the deployment’s own data-policy and region defaults under them. version is also the ETag.
/v1/routing?orgId=…Replace them with {route?: "cheapest" | "fastest" | "balanced" | "pinned", providers?: {order, allow_fallbacks}, fallback_models?, max_price?: {input_per_1m, output_per_1m}, data_policy?: {no_training, max_retention_days}, regions?}, or null to remove them: {routing, version}. pinned needs providers.order; an unknown provider or model is 400 invalid_request (a fallback model already saved is kept, even once no longer listed). Send the version read as If-Match to make it conditional: 412 routing_changed when they changed since.
Balances, top-ups, cards and statements, for the personal account or, with orgId, an organization. Card payments go through Stripe: where the deployment has no payment provider, the endpoints that need one answer 503 billing_unavailable. Amounts in cents are integers; credits are decimal strings, one credit per US dollar.
/v1/billing/topupCreate a top-up intent with {amount, currency?, orgId?}; the personal account by default.
/v1/billing/balance?orgId=…Read the personal balance. A JWT may pass orgId for an organization’s; an organization’s API key reads its own.
/v1/billing/usage?from=…&to=…&orgId=…Summarize usage over a period, for the personal account or an organization: {requests, inputTokens, outputTokens, cachedTokens, cacheWriteTokens, totalTokens, costCredits, series: [{date, requests, tokens, costCredits}]}, each token sum and costCredits also as an exact decimal string, …Exact, as for /v1/usage. Usage is shown for the last 12 months: an earlier start is moved up to it, and a period entirely older is refused with 400 range_too_old.
/v1/billing/ledger?from=…&to=…&type=…&cursor=…&limit=…&orgId=…The ledger statement: every change to the balance over [from, to), newest first, 50 a page (limit up to 100): {from, to, type, summary: {openingCredits, closingCredits, netCredits, byType, entries, balanceCredits}, data: [{id, createdAt, type, amountCredits, balanceAfterCredits, description, reference}], nextCursor}. type is topup, debit (a request’s charge), refund, adjustment, commission or expiry; amountCredits the signed change, balanceAfterCredits the balance once it was recorded; reference the request, payment, refund or dispute it refers to. The summary covers every entry of the range, whatever type narrows the page to: opening plus the entries is the closing balance, and a range that ends now closes at the balance. At most 366 days per request, for the last 12 months. An organization’s, its owners and admins.
/v1/billing/ledger/export.csv?from=…&to=…&type=…&orgId=…The same entries as CSV, oldest first: created_at, transaction_id, type, description, amount_credits, balance_after_credits, request_id, payment_id, refund_id, dispute_id, related_transaction_ids.
/v1/billing/invoices?orgId=…List invoices, personal or for an organization.
JWT/v1/billing/invoices/:id.pdfDownload a monthly statement as a PDF. Statements are issued automatically once a month closes.
JWT/v1/billing/expiring?orgId=…Purchased credit that expires within 30 days, 365 days after its purchase (Terms §4.4): {withinDays: 30, totalCredits, lots: [{purchasedAt, expiresAt, amountCredits}]}, soonest first. Promotional and trial credit are not listed.
/v1/billing/reconcile?orgId=…Credit the caller’s own card top-ups that are paid but still waiting, should Stripe’s notice of one have gone missing — once, as that notice would: {checked, applied, pending, errors, hasMore}; with hasMore, call it again. For the console’s Refresh balance after a checkout.
/v1/billing/cardSave a card on the personal account, for the trial credit: {id, url}, a Stripe Checkout page that saves the card and charges nothing. Send the browser to url; the card is on file once Stripe confirms it.
/v1/billing/payment-methods?orgId=…The saved cards: {billingAvailable, methods: [{id, brand, last4, expMonth, expYear, status, supportsOffSession, isDefault}], version}, version what a change of the default sends back. Without a payment provider: {billingAvailable: false, methods: [], version: 0}. An organization’s, its owners and admins.
/v1/billing/payment-methods/setupSave a card with {orgId?, manualPaymentRedisplayConsent: true}: {id, url}, a Stripe Checkout page that saves it and charges nothing, as POST /v1/billing/card does. Five card checkouts an hour per user, the two together (429 rate_limited with Retry-After).
/v1/billing/customer/emailRead your personal payment email: {billingAvailable, email}. Before a Customer exists, defaults to your verified sign-in email without creating a Stripe Customer.
/v1/billing/customer/emailSend {email, expectedEmail} with a UUID Idempotency-Key to update your personal Stripe Customer email. Returns {email}. A changed value since you read it returns 409 version_conflict. Recorded unpaid top-up pages you opened are closed, including organization top-ups. Open a new checkout after saving. Sign-in email, cards, history and the independent Link account are unchanged. Organization scope is not accepted; Stripe failures return 502 payment_provider_error.
/v1/billing/payment-methods/:idMake a saved card the default with {orgId?, makeDefault: true, expectedVersion} and an Idempotency-Key header (a UUID): {id, isDefault: true}. 409 version_conflict when the cards changed since expectedVersion; 404 not_found for a card that is not active or not the account’s.
/v1/billing/payment-methods/:id?orgId=…Remove a saved card, with an Idempotency-Key header: {id, status: "detached"}, also when it already was. The default card stays: 409 default_payment_method, make another the default first.
/v1/billing/payments?orgId=…&limit=…&cursor=…Card payments, newest first: {payments: [{id, kind: "manual" | "auto_topup", amountCents, currency, status, createdAt, updatedAt, succeededAt, failedAt, cancelledAt, receiptEligible}], hasMore, nextCursor}; limit 1–100, 25 by default, and nextCursor as cursor for the next page. An organization’s, its owners and admins.
/v1/billing/payments/:id/adjustments?orgId=…The refunds and disputes of a succeeded payment: {paymentId, adjustments: [{id, kind: "refund" | "dispute", amountCents, currency, providerStatus, latestProviderEventAt, netCreditImpact, ledgerEntryCount}]}, newest first; netCreditImpact is the credit it took back, a negative decimal string. 404 not_found for a payment that is not the account’s or did not succeed.
/v1/billing/payments/:id/receiptStripe’s receipt of a top-up the caller paid: {receiptUrl}, a page on pay.stripe.com to open. Only whoever paid gets it, for an organization too; 404 not_found for anyone else, 404 receipt_unavailable while Stripe has none.
/v1/billing/auto-topup?orgId=…Automatic top-ups are not available yet: the setting can be read and saved, switched off, and nothing is charged automatically. The setting: {status, enabled, version, paymentMethodId, thresholdCredits, amountCents, monthlyLimitCents, monthSucceededCents, monthReservedCents, monthRemainingCents, windowMonth}. An organization’s, its owners and admins.
/v1/billing/auto-topupSave it with {orgId?, expectedVersion, enabled, thresholdCredits, amountCents, monthlyLimitCents} (with enabled: true, also paymentMethodId, the default card, and offSessionConsent: true): the setting. amountCents is within the top-up limits and monthlyLimitCents at least that. Turning it on is not available yet: enabled: true is refused with 409 off_session_authorization_unavailable, and nothing is charged automatically. 409 billing_customer_required before a first card or checkout; 409 version_conflict when it changed since expectedVersion.
/v1/billing/auto-topupSwitch it off and clear it, with {orgId?, expectedVersion} in the body: the setting.
/v1/billing/refundable?orgId=…Top-ups whose unused credit can go back to the card that paid, within 7 days of their purchase (Terms §4.5): {windowDays: 7, topups: [{transactionId, purchasedAt, refundableUntil, amountCredits, refundableCredits, refund}]}, newest first, refund the latest request (pending, refunded or failed) or null. An organization’s, its owners and admins.
/v1/billing/topups/:transactionId/refundRefund what is unused of a top-up: 200 {refund} once Stripe refunded it, 202 {refund} while its answer is pending. Needs a sign-in or re-authentication within 10 minutes (403 reauth_required); ten an hour. 404 topup_not_found; 409 refund_window_closed, nothing_to_refund, refund_in_progress, refund_busy or refund_unavailable; 502 refund_failed when Stripe refused, the credit back on the balance.
Apply to become a reseller; a Token Engine administrator approves the application and sets the commission rate, a share of Token Engine’s markup on referred usage. A user is attributed to a reseller when their account is created: pass referralCode to /v1/auth/signup/start, or start a provider’s sign-in at /v1/auth/:provider/start?intent=signin&ref=CODE (used if it leads to a new account). Attribution is permanent — nobody else can refer the account later — and an organization a referred user creates is attributed to the same reseller; commission, and the reseller’s markup, apply only to usage within the 12 calendar months (UTC) after the referred user signs up (for an organization, its creator). A reseller never earns commission on their own usage.
/v1/resellers/programThe program’s terms on this deployment, for anyone signed in: {maxCommissionPct, maxMarkupPct, commissionMonths, payouts} — the most commission Token Engine gives, as a fraction of its markup (the rate itself is set on approval), the most markup a reseller may add, as a percentage of the platform’s price, the months after a customer signs up that both apply, and payouts: stripe (to a Stripe account the reseller connects) or manual (Token Engine pays directly and records each payout).
/v1/resellersApply to become a reseller; the application starts pending. The account’s address is emailed that it arrived, and again when a Token Engine administrator decides it.
JWT/v1/resellers/referralsCreate a referral code (approved resellers): 4–32 letters, digits, - or _, unique regardless of case.
/v1/resellers/referralsList the caller’s codes and how many users each referred.
JWT/v1/resellers/commissionsList accrued or paid commissions.
JWT/v1/resellers/summaryStatus, rate, referred users and commission totals.
JWT/v1/resellers/markup?limit=…&offset=…The markup the reseller adds to the prices of the customers they referred, which those customers pay and the catalog shows them: {appliedMarkupPct, current, scheduled, maxMarkupPct, hold, data, total}, percentages as decimal strings, data every version newest first (limit 1–100, 20 by default).
/v1/resellers/markupChange it with {markupPct, effectiveFrom?} (approved resellers): from the start of a UTC day, the next one unless effectiveFrom names a later one; 0 removes it. It replaces a change still scheduled. At most maxMarkupPct (400 markup_above_cap); 400 markup_not_at_day_start, 400 markup_in_past; 403 markup_held above a hold a Token Engine administrator set.
/v1/resellers/markup/preview?markupPct=…&effectiveFrom=…What the reseller’s customers would pay under a markup before it is set: {markupPct, effectiveFrom, data: [{id, name, platform: {input_per_1m, output_per_1m}, customer: {input_per_1m, output_per_1m}}], total} — the catalog’s first three models at the platform’s price and with the markup, at the prices for the start of the next UTC day or the later effectiveFrom, of total models on sale. 400 markup_above_cap above maxMarkupPct.
/v1/resellers/markup/scheduledWithdraw the scheduled change: {voided, current}; the current markup runs on.
/v1/resellers/payout-accountThe Stripe account commission is paid to: {account}, null before one is connected, else its state (payoutsEnabled, requirements, ready, …). 503 payouts_unavailable where the deployment has no Stripe.
/v1/resellers/payout-accountConnect or finish connecting it with {country?} (approved resellers): 201 {account, onboardingUrl, expiresAt}. Send the browser to onboardingUrl, Stripe’s one-time page; it comes back to the console, and an expired link is replaced by calling this again.
/v1/resellers/payableWhat the next payout would transfer, of commission dated before today (UTC): {payableCredits, carriedCredits, transferCents, remainderCredits, heldCents, currency, blockers}; blockers say what stops it (payout_account_missing, payouts_not_enabled, nothing_to_pay, …). A Token Engine administrator starts each payout.
/v1/resellers/payouts?limit=…&offset=…Payouts made, newest first: {data: [{id, method, status, amountCredits, transferCents, settledAt, createdAt, …}], total}.