TOKEN ENGINE GATEWAY

API Reference

OpenAI-compatible model calls, plus Token Engine’s own APIs for accounts, billing, channels and the model catalog.

Base URL (OpenAI SDK)https://api.rstoken.ai/v1

Quickstart

Create 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).

Chat completion
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"}]}'

Authentication

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.

Errors

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.

User account

GET/v1/auth/config

What signing in and up offers: {providers, captcha: {provider, siteKey} | null, legal: {termsVersion, privacyVersion}}.

Public
POST/v1/auth/signup/start

Begin 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).

Public + X-RunToken-CSRF
POST/v1/auth/signup/verify

Finish 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.

Public + X-RunToken-CSRF
POST/v1/auth/signup/resend

Email 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").

Public + X-RunToken-CSRF
POST/v1/auth/login

Exchange {email, password} for {user, token}, the token a JWT for scripts. After repeated failures a CAPTCHA token is needed as well (400 captcha_required).

Public + X-RunToken-CSRF
POST/v1/auth/password/forgot

Email a password reset link with {email, captchaToken}; always 202, whether or not an account has the address.

Public + X-RunToken-CSRF
POST/v1/auth/password/reset

Set a new password with {token, password} from the link (valid once, for 30 minutes); every existing session and JWT of the account ends.

Public + X-RunToken-CSRF
GET/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.

Public
POST/v1/auth/verify-email

Verify 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.

Public
POST/v1/auth/verify-email/resend

Email 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.

JWT
GET/v1/auth/:provider/start

A 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>.

Browser
POST/v1/auth/signup/complete

Complete 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.

Browser + X-RunToken-CSRF
GET/v1/auth/wallet/nonce

A 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.

Browser
POST/v1/auth/wallet/verify

Sign 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.

Browser + X-RunToken-CSRF
POST/v1/auth/logout

Sign 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.

Session cookie + X-RunToken-CSRF, or JWT
POST/v1/auth/reauth

Prove 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).

Session cookie + X-RunToken-CSRF
GET/v1/me

Read 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).

JWT
GET/v1/me/sessions

The account’s live sessions: {sessions:[{id, createdAt, lastSeenAt, ip, userAgent, method, current}]}.

JWT
DELETE/v1/me/sessions/:id

End 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.

JWT
POST/v1/me/sessions/revoke-others

End 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.

JWT
GET/v1/me/identities

The account’s sign-in methods: {identities:[{id, provider, display, email, createdAt, lastUsedAt}], hasPassword}.

JWT
DELETE/v1/me/identities/:id

Remove 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).

JWT
POST/v1/me/legal/accept

Accept 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.

JWT
PATCH/v1/me/preferences

Give 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.

JWT
POST/v1/me/trial

Claim 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.

JWT
PATCH/v1/me

Change the display name with {name} (1–80 characters).

JWT
POST/v1/me/password

Change the password with {currentPassword, newPassword}. Every other session and JWT ends; the answer carries a new token.

JWT
POST/v1/me/email

Change 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.

JWT
GET/v1/me/export

A 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.

JWT
POST/v1/me/close

Close 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.

JWT
GET/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.

JWT
GET/v1/keys/:id

One key as the list shows it, to whoever may see it there.

JWT
GET/v1/keys/:id/usage?days=30

A 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.

JWT
POST/v1/keys/bulk

Disable, 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.

JWT
POST/v1/keys

Create 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.

JWT
PATCH/v1/keys/:id

Rename, 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.

JWT
POST/v1/keys/:id/rotate

Give 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.

JWT
DELETE/v1/keys/:id

Revoke one of the caller’s API keys.

JWT

Organizations

Every 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.

POST/v1/orgs

Create an organization with {name}; the creator becomes its owner.

JWT
GET/v1/orgs

List the organizations the caller belongs to, with their role.

JWT
GET/v1/orgs/:orgId/members

List an organization’s members; the caller must be one.

JWT
POST/v1/orgs/:orgId/members

Add 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.

JWT
PATCH/v1/orgs/:orgId/members/:userId

Change 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.

JWT
DELETE/v1/orgs/:orgId/members/:userId

Remove 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.

JWT
POST/v1/orgs/:orgId/leave

Leave an organization. 204; 409 last_owner for its last active owner: make someone else an owner first.

JWT
GET/v1/orgs/:orgId/audit

The 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.

JWT
GET/v1/orgs/:orgId/audit/export.csv

The 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.

JWT

Models & inference

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).

GET/v1/models

List 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.

API Key
GET/v1/catalog

The same catalog for a signed-in session, which the console browses.

JWT
GET/v1/catalog/:vendor/:model

One 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).

JWT
POST/v1/key-test/hold

What 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.

JWT
POST/v1/chat/completions

Create 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.

API Key
POST/v1/completions

Create 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
POST/v1/embeddings

Create OpenAI-style embeddings with a catalog model that produces them; metered by input tokens.

API Key
POST/v1/responses

Create 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.

API Key
POST/v1/messages

Create 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.

API Key

What an answer says

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.

Retries and idempotency

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:

Send an Idempotency-Key
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())},
)
  • A key belongs to the account and the endpoint, and is kept for 24 hours. /v1/completions, /v1/embeddings and /v1/responses take it as chat does; a replayed response keeps its id.
  • Once the first request with a key has reached the provider, its answer is kept, a success or an upstream error alike. A retry with the same key and the same body gets that answer again — the same status, body, 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.
  • A request with a key is not cancelled when its client disconnects: it runs to its end and is billed as usual, and the retry gets the whole answer. While it still runs, a retry gets 409 idempotency_in_progress with Retry-After.
  • The same key with a different body is 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.
  • An answer over 1 MiB, such as a long stream, is not kept: its retry gets 422 idempotency_response_too_large naming the original request, and is not run again; send it with a new key to run it again.

Streams cut short

What a request that does not finish is billed:

  • Your client disconnects — it closes the connection, or stops waiting — before the answer is complete, on a request without an 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.
  • The provider fails, or the request runs out of time, before its answer or a stream’s first token: nothing is billed. Once a stream has begun, it is billed for the output it delivered, from the provider’s counts where it had sent them, and it ends with an error event in the shape of Errors (on /v1/responses, an error event and then response.failed).
  • Estimated counts. Where the provider did not report a count — it reported none, or the request ended before it could — Token Engine counts the tokens itself: the prompt, and the output delivered. The request’s tokenSource (in /v1/generation and /v1/usage/events) is then estimated; provider means every count is the provider’s own.

The Responses API

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.

Create a response
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"}'
  • Taken: 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.
  • Accepted and not acted on: 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.
  • Refused with 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.
  • The answer has its 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.
  • A stream sends OpenAI’s events, each named by its 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.

Anthropic's Messages API and Claude Code

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": "…"}}.

  • What is sent on. The system prompt, text and image blocks, 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.
  • Claude Code. Point it at Token Engine and name a catalog model for each of its roles:
Claude Code with Token Engine
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

Reasoning models

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.

Service tiers

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.

What a model is served on

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.

Parameters and legal values

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.

Usage & requests

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.

GET/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.

API Key or JWT
GET/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.

API Key or JWT
GET/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.

API Key or JWT
GET/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.

API Key or JWT
GET/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.

API Key or JWT
GET/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.

API Key or JWT
GET/v1/requests/:requestId/route

How 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.

API Key or JWT
GET/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.

JWT
PUT/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.

JWT
GET/v1/requests/:requestId/content

A 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.

JWT
DELETE/v1/requests/:requestId/content

Delete what was kept of a request, under the same rule. 204; 404 not_found once it is gone.

JWT
DELETE/v1/content-logs?orgId=…

Delete every kept request of the account at once: {deleted}. An organization’s, its owners and admins.

JWT

Budgets & routing

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"}}.

GET/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.

JWT
PUT/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).

JWT
GET/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.

JWT
PUT/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.

JWT

Billing

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.

POST/v1/billing/topup

Create a top-up intent with {amount, currency?, orgId?}; the personal account by default.

JWT
GET/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.

JWT / API Key
GET/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.

JWT
GET/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.

JWT
GET/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.

JWT
GET/v1/billing/invoices?orgId=…

List invoices, personal or for an organization.

JWT
GET/v1/billing/invoices/:id.pdf

Download a monthly statement as a PDF. Statements are issued automatically once a month closes.

JWT
GET/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.

JWT
POST/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.

JWT
POST/v1/billing/card

Save 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.

JWT
GET/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.

JWT
POST/v1/billing/payment-methods/setup

Save 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).

JWT
GET/v1/billing/customer/email

Read your personal payment email: {billingAvailable, email}. Before a Customer exists, defaults to your verified sign-in email without creating a Stripe Customer.

JWT
PATCH/v1/billing/customer/email

Send {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.

JWT
PATCH/v1/billing/payment-methods/:id

Make 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.

JWT
DELETE/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.

JWT
GET/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.

JWT
GET/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.

JWT
GET/v1/billing/payments/:id/receipt

Stripe’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.

JWT
GET/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.

JWT
PUT/v1/billing/auto-topup

Save 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.

JWT
DELETE/v1/billing/auto-topup

Switch it off and clear it, with {orgId?, expectedVersion} in the body: the setting.

JWT
GET/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.

JWT
POST/v1/billing/topups/:transactionId/refund

Refund 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.

JWT

Resellers & affiliates

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.

GET/v1/resellers/program

The 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).

JWT
POST/v1/resellers

Apply 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
POST/v1/resellers/referrals

Create a referral code (approved resellers): 4–32 letters, digits, - or _, unique regardless of case.

JWT
GET/v1/resellers/referrals

List the caller’s codes and how many users each referred.

JWT
GET/v1/resellers/commissions

List accrued or paid commissions.

JWT
GET/v1/resellers/summary

Status, rate, referred users and commission totals.

JWT
GET/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).

JWT
PUT/v1/resellers/markup

Change 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.

JWT
GET/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.

JWT
DELETE/v1/resellers/markup/scheduled

Withdraw the scheduled change: {voided, current}; the current markup runs on.

JWT
GET/v1/resellers/payout-account

The 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.

JWT
POST/v1/resellers/payout-account

Connect 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.

JWT
GET/v1/resellers/payable

What 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.

JWT
GET/v1/resellers/payouts?limit=…&offset=…

Payouts made, newest first: {data: [{id, method, status, amountCredits, transferCents, settledAt, createdAt, …}], total}.

JWT