← Twenty Questions / API
Tokens

Drive the game from your own code

Twenty Questions is a session app: one game is one server-side conversation, and one question is one billed turn against the app's agent prompt. Everything below is the same API the page itself uses — there is no private endpoint.

Base URL https://api.skillsafe.ai/v1/app-api. Every response is the same envelope: {"ok": true, "data": {…}} on success, {"ok": false, "error": {"code", "message", "details"}} on failure. Pick your language once — the tabs switch together and the choice is remembered.

Errors you will actually hit

codeHTTPwhat it means
UNAUTHORIZED401No token, or a token that is not this app's. Mint one on /tokens.html.
FORBIDDEN403A guest token tried to take a turn. Turns are metered, so they need a signed-in token.
INSUFFICIENT_CREDITS402The balance cannot cover the hold for this turn. Estimate first; top up; the game carries on.
NOT_FOUND404The session id is unknown, deleted or belongs to another subject. Create a new one and replay the state.
VALIDATION_ERROR400The body was malformed - most often `content` missing, or sent as an object instead of a string.
RATE_LIMITED429Too many calls. Back off and retry; never tight-loop.
JOB_FAILED-The turn ran and failed upstream. The reply carries `error`; nothing advanced, so the move can be re-made.

1. Get a token

Every call takes Authorization: Bearer aut_…. The easy way to get one is the token page: it reads the token this browser already holds for the app, shows whether it is a guest or a personal token, and gives you a one-click shell export. A guest token can call /me and /estimate. A game turn is metered, so it needs a personal token - sign in on that page to get one.

2. Check who you are and what you can spend

GET /me returns exactly three fields: subject_type, subject_id and credits. There is no email, no name and no id beyond the subject id, so the signed-in test is subject_type === "user" and nothing else. credits is the balance every turn is billed against.

{"ok":true,"data":{"subject_type":"user","subject_id":"usr_...","credits":48210}}

3. Price a turn before you take one

POST /estimate is free, runs no job and charges nothing. Send it the same body you would send as a turn and it comes back with model, model_alias, markup_bps, hold_credits and min_credits. hold_credits is what gets reserved, not what you pay - the hold prices the full output cap and a twenty-questions turn is short, so the settled charge is usually far lower. A game is up to twenty turns, so multiply before you tell a player what a game costs.

{"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra","markup_bps":1000,"hold_credits":1130,"min_credits":24,"sponsor_enabled":false}}

4. Open a game

One game is one session. POST /sessions takes an empty body and returns a session_id; the app's agent prompt is attached server-side, and the conversation history is kept there too, so you never resend past messages. You may hold 20 live sessions per user and a session holds at most 200 messages, so delete a game when it ends.

{"ok":true,"data":{"session":{"session_id":"ses_...","created_at":"2026-08-26T10:00:00Z"}}}

5. Take a turn

POST /sessions/{id}/messages with {"content": "…"}. content is a string - the turn envelope below - not an object. The call returns a job_id; poll GET /jobs/{id} until it reaches a terminal state, and read the reply from output.output. A turn is a job, so it holds credits, settles to charged_credits, and can come back truncated: true if the reply hit the output cap. There is no idempotency key on a session turn. Neither the API nor the SDK accepts one, and resending is worse than a double charge - it appends a second copy of the move to the server-side history. If you are unsure whether a turn landed, call GET /sessions/{id} and count the assistant messages instead of resending.

{"ok":true,"data":{"job_id":"job_...","session_id":"ses_..."}}

# then GET /v1/app-api/jobs/job_...
{"ok":true,"data":{"status":"succeeded","charged_credits":214,"truncated":false,
  "output":{"output":"THINKING: Small, man-made and not powered, so tools and utensils are the live field.\nRULED_OUT: appliances, vehicles, furniture\nCONFIDENCE: 12\nASK: Is it usually kept indoors?"}}}

6. Stream a turn instead

Add "stream": true and the same endpoint answers text/event-stream: a job event, then delta events carrying {"text"}, then one done event with the settled charged_credits. Because the reply is labelled lines rather than JSON, each line is renderable the moment it completes - which is why this app streams by default.

7. Settle a turn you are not sure landed

GET /sessions/{id} returns the session and its full message history. Count the assistant messages: if the server holds more than your own transcript accounts for, your turn did land and its reply is sitting there to be adopted. That is how this app recovers from a reload mid-turn without ever paying for the same question twice, and it is the substitute for the idempotency key sessions do not have.

{"ok":true,"data":{"session_id":"ses_...","messages":[
  {"role":"user","content":"[TWENTY QUESTIONS | TURN 1] ..."},
  {"role":"assistant","content":"THINKING: ...\nCONFIDENCE: 0\nASK: Is it alive?"}
]}}

8. Close the game

DELETE /sessions/{id} removes the session and its messages. Do it when a game ends: twenty live sessions is the cap, and an app that leaks them stops being able to start games.

{"ok":true,"data":{"deleted":true}}

The turn envelope

content is a single string: a labelled block that restates the entire state of the game. It is regenerated from scratch on every turn on purpose. The platform truncates a long conversation oldest-pair-first, and in twenty questions the earliest answers are the ones everything later depends on — so nothing here relies on the conversation surviving. A useful side effect: because the envelope is self-sufficient, a game whose session has gone can be resumed by opening a new one and sending the same envelope.

A turn in guess mode, where the player holds the secret:

[TWENTY QUESTIONS | TURN 4]
MODE: guess
CATEGORY: object
DIFFICULTY: normal
QUESTIONS_USED: 3
QUESTIONS_LEFT: 17
ESTABLISHED FACTS (authoritative - never contradict these):
  01. Is it alive? -> no
  02. Is it man-made? -> yes
  03. Is it bigger than a microwave oven? -> no
PLAYER REPLY TO QUESTION 3: no
INSTRUCTION: ask question 4, or commit to a guess if you are confident.

A turn in hold mode, where the app holds it. Note the SECRET line — it is re-sent every single turn, so the model is reporting a property of a fixed string rather than inventing one that fits the questions asked so far:

[TWENTY QUESTIONS | TURN 4]
MODE: hold
CATEGORY: concept
DIFFICULTY: normal
QUESTIONS_USED: 3
QUESTIONS_LEFT: 17
SECRET: a rainbow
ANSWERS YOU HAVE ALREADY GIVEN (authoritative - never contradict these):
  01. Is it alive? -> no
  02. Is it man-made? -> no
  03. Is it something you can see? -> yes
PLAYER QUESTION: Can you touch it?
INSTRUCTION: answer this question about SECRET, truthfully and consistently with the answers above. Do not reveal SECRET.

The reply contract

Replies are plain labelled lines, not JSON: LABEL: value, one per line, a wrapped line continuing the label above it. Three reasons. They stream, so a half-arrived reply still renders. One stray comma cannot destroy a turn. And a formatting slip degrades into an odd-looking card rather than an empty one.

labelvalueturnsmeaning
THINKINGone lineanyOne sentence on what the facts now narrow to. Shown to the player.
RULED_OUTcomma list or noneaskWhat the last answer eliminated.
CONFIDENCE0–100askHow close the model is to naming it.
ASKa questionaskThe next question. Yes/no answerable, ends in a question mark.
GUESSa noun phraseaskA commitment. A wrong one costs a question.
ANSWERyes · no · sometimes · irrelevantanswerThe answer about the held secret.
NOTEone lineanswer, verdictA clarification of the question. Never a spoiler.
SECRETa noun phrasesetupThe chosen secret. Setup turn only.
CATEGORYone wordsetupobject, animal, person, place, food or concept.
HINTone linesetupVague, shown before the first question.
VERDICTcorrect · incorrectverdictJudging the player's final guess.
REVEALthe secretverdict, finishOnly when the game ends.
RESULTwin · lossfinishFrom the model's side.
SAYone or two sentencessetup, verdict, finishClosing the game.

Each turn shape declares what it must return. A reply missing a required label is a protocol failure: send the move again with a FORMAT CORRECTION: line naming the missing labels, and count it as the extra billed turn it is.

shapemoderequiredoptional
setupholdSECRETCATEGORY, HINT, SAY
askguessASK or GUESSTHINKING, RULED_OUT, CONFIDENCE
answerholdANSWERNOTE, THINKING
verdictholdVERDICTREVEAL, SAY, NOTE
finishbothRESULTSAY, REVEAL, GUESS

Rules a client has to keep for itself

Play a game · Tokens · llms.txt