Neocortex
IntegrationsWeb API

Quiz

A whole quiz, game show or spoken test through one endpoint

Experimental

The quiz engine is currently an experimental feature. Its request and response shape may change.

One of your characters hosts a quiz. You send a character id and a question set id once, then forward whatever the player said for the rest of the run. Question order, retries, hints, scoring and when to stop are authored on the question set and decided on the server, so none of it lives in your game.

POST https://api.neocortex.link/v3/quiz
GET  https://api.neocortex.link/v3/quiz/session

Plan requirement

The quiz engine requires a Team plan. See Question Sets for authoring the set the run plays.

The whole integration

Begin a run

Two ids and nothing else. You get back the host's first words and, usually, the first question.

curl -X POST https://api.neocortex.link/v3/quiz \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "characterId": "YOUR_CHARACTER_ID",
    "questionSetId": "YOUR_QUESTION_SET_ID"
  }'

Forward what the player said

Every turn after the first is the session id plus the player's words. Send the transcript exactly as your speech recognition produced it. The host works out whether it was an answer, a request for a clue, a yes, a no, or something it could not make out.

{
  "sessionId": "RETURNED_SESSION_ID",
  "say": "ummm I think it's a triangle"
}

Put the scene on screen and reopen the microphone

Read question to build the scene, expecting to decide whether to listen, and done to know the run is over. Repeat until done is true.

Reading a turn

{
  "sessionId": "cmu9w20hi00527k9kpcoirzuw",
  "lines": [
    { "text": "Close! Think about the corners.", "emotion": "CURIOUS", "role": "reaction" },
    { "text": "What shape has three sides?", "emotion": "CURIOUS", "role": "question" }
  ],
  "expecting": "answer",
  "intent": "answer",
  "result": [{ "verdict": "INCORRECT", "score": 0, "participantId": "player-1" }],
  "leaderboard": [
    { "participantId": "player-1", "displayName": "Player", "totalScore": 0, "rank": 1 }
  ],
  "done": false
}
FieldDescription
sessionIdAlways sent. Identifies which run this payload belongs to, so a logged or queued response cannot be mistaken for another player's.
lines[]What the host says, in order, each with its own emotion and role.
expectingWhat to collect next: answer, confirmation or nothing.
intentHow the player was read. Omitted on a turn where nobody had spoken.
questionThe question now on screen. Sent on the turn it changes and not repeated after.
upcomingThe question after this one, so a scene can be prepared before it is asked.
progressasked is which question is on the table, total is how many the run serves. Travels with question.
result[]How the last answer was judged. Omitted on a turn that judged nothing.
leaderboard[]The standings. Sent only when a score could have moved.
pending[]Who the round is still waiting on. Omitted for a single player.
donetrue once the host has signed off.

Anything absent means there is nothing to report. A field that is always present and usually null teaches a reader nothing, so only what a turn actually carries is sent.

Line roles

role says what each line is doing, so you can time the scene to it rather than to the turn.

RoleMeaning
greetingWelcoming the players.
reactionResponding to what the player said.
questionThe words that actually put a question to them.
closingSigning off.

Show the next thing on screen as the question line begins, not while the host is still reacting to the last answer.

spokenText appears on a line only when it respells that line for speech, for example "Alex +100" spoken as "Alex plus one hundred points". When it is absent, speak text.

Intents

IntentWhat the player was doing
answerAttempting the question. A wrong answer is still an answer.
hintAsking for a clue, or saying they do not know.
repeatAsking for the question again.
affirm / declineAccepting or turning down another go, when one was offered.
stopAsking to finish.
unclearNothing usable came through. Costs the player no attempt.

Only answer can score. A request for a clue or a turn the host could not make out never spends an attempt.

The question on screen

{
  "question": {
    "questionId": "q_mfx2k1_a7c2",
    "keyword": "SHOW_RED_APPLE",
    "type": "multiple_choice",
    "questionText": "Which one is the apple?",
    "options": [
      { "id": "opt_a", "text": "Apple" },
      { "id": "opt_b", "text": "Pear" }
    ],
    "hasHint": true
  },
  "upcoming": { "questionId": "q_mfx2k4_b913", "keyword": "SHOW_GREEN_PEAR" },
  "progress": { "asked": 1, "total": 5 }
}

keyword is the author's own name for the scene a question needs, in the same shape as an action keyword. Question ids are generated and mean nothing to a game; key your scene on the keyword instead, and use the one on upcoming to preload the next.

hasHint is only present when the run will actually give a clue. A set with hints turned off never advertises one, even on a question whose author wrote hint text.

The answer key never reaches the player

Expected answers, rubrics, hint text and the rest of the queue stay on the server. A contestant's device would otherwise receive the answer along with the question. The correct answer appears only in the round history, and only once the run has finished.

Answering

You sendWhat it means
sayWhat the player said or typed, up to 512 characters.
optionIdThey tapped an option. Send this instead of, or alongside, say.
participantIdWho spoke. Required once there is more than one player.
say: ""An explicit pass. The round is recorded unanswered and the run moves on, with no model call.

An empty transcription is not a pass

Never route silence or a failed transcription through as say: "". Speech the host could not make out is an utterance, and the engine has a ladder for it: it asks again, then volunteers the clue, then moves the player along. Sending an empty string instead tells the engine the player chose to skip.

An authored welcome is its own beat

When the set has opening words written on it, the first call returns only the greeting: question is absent and expecting is nothing. The turn after that asks question one. This is so a game can greet the player, dress the scene, and only then put a question on screen.

Leave the opening blank and the run opens on the first question instead, in the same breath as the welcome.

The same applies to the closing words. With none written, the run ends on the host's last reaction.

Several players

{
  "characterId": "YOUR_CHARACTER_ID",
  "questionSetId": "YOUR_QUESTION_SET_ID",
  "participants": [
    { "id": "alex", "displayName": "Alex", "type": "human" },
    { "id": "maya", "displayName": "Maya", "type": "human" }
  ]
}

Up to ten participants, and their ids must be unique. Send participantId with every answer so the right person is credited. The round waits until everyone has answered before anything is judged, and pending tells you who is left.

Omit participants entirely for a single player.

AI contestants

A participant of type: "ai" is refused with ai_contestants_unsupported. Nothing generates their answers yet, so accepting one would leave the round waiting on a player that never speaks.

Reading a run back

If a client loses its place, restarts, or you want a scoreboard, read the whole run.

curl "https://api.neocortex.link/v3/quiz/session?sessionId=SESSION_ID" \
  -H "x-api-key: YOUR_API_KEY"
{
  "state": {
    "phase": "collecting",
    "roundNumber": 2,
    "totalQuestions": 5,
    "questionsRemaining": 3,
    "participants": [{ "id": "player-1", "displayName": "Player", "type": "human" }],
    "activeQuestion": { "questionId": "q_mfx2k4_b913", "type": "free_text", "questionText": "And this one?", "hasHint": false },
    "answeredParticipantIds": [],
    "pendingParticipantIds": ["player-1"],
    "leaderboard": [{ "participantId": "player-1", "displayName": "Player", "totalScore": 100, "rank": 1 }],
    "startedAt": "2026-09-20T09:14:02.881Z",
    "finishedAt": null,
    "nextAction": "evaluate",
    "expecting": "answer"
  },
  "leaderboard": [],
  "roundHistory": [],
  "questionSet": { "name": "Shape School", "evaluationProtocol": "educational" },
  "startedAt": "2026-09-20T09:14:02.881Z",
  "finishedAt": null
}

state.expecting is the same value a turn carries, so a client whose turn response was lost can reopen the microphone instead of guessing. roundHistory carries every round played, with the host's lines and the verdicts, and gains correctAnswer on each round once the run is finished.

Scores never come from the host

Every point is derived from the question's own scoring table. The host reads the player and reports a verdict for open answers; the engine works out the number. A host that proposes a retry the set forbids, or a verdict on a turn that was not an answer, changes nothing.

result[].score is what actually reached the standings. On a replay the set does not count it is 0, while the verdict still says whether they were right, so you can celebrate a correct answer without the leaderboard disagreeing with you.

Multiple choice and true or false questions are graded in code. When the words do not name an option outright, the host is asked which option they pointed at and the engine compares that pick to the answer key, so a player answering in their own language is not marked wrong by a string comparison.

Errors

StatusCodeMeaning
400invalid_jsonThe request body was not JSON.
400invalid_question_setThe set has a question or a play policy that cannot be played.
401Missing or invalid API key.
403The quiz engine requires a Team plan.
404The session, character or question set does not exist on your team.
422missing_idsNo sessionId, and not both of characterId and questionSetId.
422unknown_participantThe participantId is not in this run.
422unknown_optionThe optionId is not one of this question's options.
422nothing_saidA question is open but the call carried neither say nor optionId.
422ai_contestants_unsupportedA participant of type ai was supplied.
409operation_in_progressAnother turn is already running for this session. Retry after the Retry-After header.
409quiz_finishedThe run is over. The body carries the closing turn again under turn.
502invalid_model_outputThe host's reply could not be read. Nothing was charged.
502model_output_truncatedThe host's reply was cut short. Nothing was charged.

One turn at a time

A session runs one turn at a time. A second request while a turn is in flight gets 409 with a Retry-After header rather than a second, conflicting turn, so a double tap cannot produce two answers to one question.

Request reference

{
  "sessionId": "",           // Continue a run. Omit on the first call
  "characterId": "",         // Required on the first call, the host of the quiz
  "questionSetId": "",       // Required on the first call, the set to play
  "playerId": "",            // Optional, your own id for this player
  "participants": [],        // Optional, up to 10, unique ids. Omit for one player
  "participantId": "",       // Who spoke, once there is more than one player
  "say": "",                 // What the player said. "" is an explicit pass
  "optionId": ""             // The option they tapped
}

On this page