MakeQuestions API by Karson AI
Migration Guide
Move clients to the current Karson AI API contract: required question_config, supported question types, and optional vision input.
This guide covers moving an existing client from the legacy server contract (any 2.x line that predates v2.7.0, the earliest release that already had today's consolidated streaming endpoint) to the current server contract (3.4.x). It collects every breaking change a caller can hit and shows a concrete before/after for each.
Callers already on a more recent
2.xbuild can skip rows that already match their version — see the Changelog for which release introduced each change.
What changed at a glance
| Area | Before (pre-v2.7.0) | Now (3.4.x) |
|---|---|---|
| Generate request shape | Top-level num_questions, question_type, difficulty, question_type_counts, allowed_types | Single required question_config object |
reasoning default | false (instant) | true (reasoning path); send reasoning: false to keep instant |
"mixed" keyword | Allowed as a type and as a difficulty | Removed in both places |
| Streaming | POST /generate-questions/stream and POST /generate-questions/stream-by-question | POST /generate-questions with "streaming": true |
| Follow-up request types | POST /follow-up supported only more-questions (using question_type_counts) | POST /follow-up now supports more-questions, inline-explanation, and chat |
| Vision input | Not supported | Optional image_urls on both endpoints |
| File attachments | Not supported | attachments (images and documents) on both endpoints — preferred over image_urls |
| Continuation handle | previous_response_id (request body field) | response_id returned by every turn; pass the same value back as response_id on /follow-up |
| Question objects | Unified shape with possible null fields | Type-scoped — only the keys relevant to type are present; server adds id and created_at |
| Unknown JSON keys | Silently ignored | Rejected (extra="forbid" → 422) |
Endpoint Changes
Removed
POST /generate-questions/stream-by-question— deleted. UsePOST /generate-questionswith"streaming": true.POST /generate-questions/stream(raw model JSON chunks) — deleted. It was previously mounted only in dev mode and never part of the public contract; usePOST /generate-questionswith"streaming": true.
Added
POST /follow-up— the endpoint itself existed in the legacy contract (more-questionsonly), but it now accepts three request types selected viarequest_type:more-questions— generate additional questions in the same thread (question_configrequired).inline-explanation— produce a short explanation for a single question (question_configignored).chat— free-form tutoring chat over the thread; always streamed via SSE.
attachmentsonPOST /generate-questionsandPOST /follow-up— send files (images and documents like PDF/Word/text) as{ "url", "mime_type", "name"? }with public HTTPS URLs. Preferred over legacyimage_urls; up to 1,500 images and 50 documents per request.
Breaking Payload Changes on POST /generate-questions
These legacy top-level fields are removed from the public contract:
num_questionsquestion_typequestion_type_countsdifficulty(now lives insidequestion_config)allowed_types
The "mixed" concept is gone:
- No
"mixed"key insidequestion_config.type_counts - No
"mixed"value fordifficulty(usenullor omit) - No implicit "open mix" default —
question_configmust always be supplied
POST /follow-up renamed its continuation handle:
- Old request field:
previous_response_id - New request field:
response_id(matches the field name in every response) - Unknown keys are rejected (
extra="forbid"), so old payloads return422
Use question_config instead of the legacy fields:
{
"context": "Your learning text...",
"question_config": {
"type_counts": {
"multiple_choice": 3,
"true_false": 2
},
"difficulty": "easy"
}
}
question_config Rules
{
"type_counts": { [concrete_type]: number | null, ... }, // required, non-empty
"difficulty": "easy" | "medium" | "hard" | null // optional
}
- Required, non-empty. Must contain at least one concrete question type after
0values are stripped. - Keys are concrete types only:
multiple_choice,true_false,short_answer,exact_answer,reorder,code-output,matching,fill-in-the-blank. - Values:
- Positive integer → exact target count for that type
null→ model-decided quantity, with a guaranteed minimum of 1 of that type (agentic fill backstops zero-generation outcomes)0→ treated as "exclude this type"; the key is silently dropped (equivalent to omitting it)
difficulty: null(or omitted) means mixed difficulty. Do not send the string"mixed".
Rejected with 422
| Payload | Why |
|---|---|
question_config omitted or null | The field is required |
type_counts: {} | Must be non-empty |
type_counts: { "multiple_choice": 0, "true_false": 0 } | After stripping 0 values, the map is empty |
type_counts: { "multiple_choice": -1 } | Negative or non-integer counts |
type_counts: { "mixed": 5 } | No "mixed" key — use concrete types |
type_counts: { "unknown_type": 3 } | Unknown key |
| Any unknown top-level key | extra="forbid" |
Migrating Common Old Patterns
| Old pattern | New pattern |
|---|---|
Omit question_config for open mix | Send at least one concrete type, e.g. {"type_counts": {"multiple_choice": null}} |
{"num_questions": 6, "question_type": "multiple_choice"} | {"question_config": {"type_counts": {"multiple_choice": 6}}} |
{"question_type_counts": {"mixed": 10}} | Pick concrete types: {"type_counts": {"multiple_choice": 5, "true_false": 5}} (or use null values to let the model decide each count) |
{"question_type_counts": {"multiple_choice": 5, "true_false": 0}} | Either omit true_false or keep the 0 (it's silently dropped): {"type_counts": {"multiple_choice": 5}} |
Top-level "difficulty": "easy" | question_config.difficulty: "easy" |
Top-level "difficulty": "mixed" | Omit difficulty or set to null |
POST /generate-questions/stream-by-question | POST /generate-questions with "streaming": true |
POST /generate-questions/stream (prod) | POST /generate-questions with "streaming": true |
{ "previous_response_id": "resp_abc123" } on follow-up | { "response_id": "resp_abc123" } |
Reasoning Default (v3.4.0)
reasoning now defaults to true on POST /generate-questions and POST /follow-up. A request that omits the field runs the reasoning path: the response may include a reasoning_summary (plus reasoning_summary_part / reasoning_summary SSE events when streaming), and the call bills reasoning tokens.
- Already send
reasoningexplicitly? Nothing changes — an explicittrueorfalseis honored exactly as before. - Relying on the old implicit
false? Addreasoning: falseto keep the instant contract (no reasoning tokens, noreasoning_summary):
{
"context": "Your learning text...",
"question_config": { "type_counts": { "multiple_choice": 3 } },
"reasoning": false
}
All generation now runs on a single model: reasoning: true uses low reasoning effort, reasoning: false uses zero effort (truly instant — no reasoning tokens). multiple_choice responses are guaranteed at least 2 choices.
Response Shape Changes
Every successful generate or follow-up turn now returns a continuation handle and per-question metadata:
{
"success": true,
"response_id": "resp_abc123",
"total_questions": 1,
"questions": [
{
"type": "multiple_choice",
"difficulty": "medium",
"question": "What do plants convert into energy?",
"choices": [
{ "key": "c1", "label": "Sunlight" },
{ "key": "c2", "label": "Water" }
],
"answer": { "value": "c1" },
"id": "8b2f0a5c-6e5e-4a1b-9b5e-9a9f1c4e6a7d",
"created_at": "2026-04-22T17:00:00.000Z"
}
]
}
- Each question is type-scoped: only the keys meaningful to its
typeare present. There are nonullplaceholders for unused fields (e.g. amultiple_choiceresponse no longer carriesmatchPairs: nullorcodeBlock: null). id(UUID) andcreated_at(ISO 8601) are added server-side on every question.reorder,matching, andfill-in-the-blankhave noanswerfield at all — the canonical answer comes fromchoicesorder, positionalmatchPairs, orchoices[].labelrespectively.- For
multiple_choicequestions, server-side enrichment shuffleschoices(Fisher-Yates) and re-keys them toc1, c2, …in the emitted order, then remapsanswer.valueto the new key. Other types'choices(reorder,fill-in-the-blank,true_false) are emitted in canonical order. Clients that previously cached amultiple_choicechoice key from a prior turn cannot reuse it across turns. response_idis the continuation handle. Pass it back asresponse_idon the nextPOST /follow-upturn.
Vision Input (image_urls)
You can add turn-scoped visual context on either endpoint:
{
"context": "Use both text and image context.",
"question_config": { "type_counts": { "multiple_choice": 3 } },
"image_urls": [
"https://cdn.example.com/diagram-1.png"
]
}
Rules:
- HTTPS only
- max 1,500 URLs
- each URL max length 8,192 chars
- duplicates are removed server-side
For follow-up chains, resend image_urls on any turn that still depends on visuals.
New clients should prefer
attachmentsoverimage_urls— it accepts both images and documents (PDF/Word/text). See the Changelog and API Reference.
Streaming Migration Notes
- The two prod streaming endpoints have collapsed into a single flag. Send
"streaming": trueonPOST /generate-questionsto receive per-question SSE. - The same flag applies to
POST /follow-upformore-questionsandinline-explanation.chatis always streamed regardless of the flag. - SSE event taxonomy by stream type:
- Question streams (
/generate-questions,/follow-upmore-questions):question(per question),done(terminal — carriesresponse_id,total_questions, and optionalvalidation/agent_metadata),error. /follow-upinline-explanationstream:explanation_text(incremental delta),explanation(full text emitted once at the end),done(carriesresponse_idandexplanation),error./follow-upchatstream (always streamed):text(incremental delta),done(carriesresponse_idand fullmessage),error.- All streams with
reasoning: truemay also emitreasoning_summary_part({ "index": N }— start of a new part) andreasoning_summary({ "text": "<delta>", "index": N }— incremental text).
- Question streams (
- The legacy
POST /generate-questions/stream-by-questionpath is gone. - The legacy raw-chunk
POST /generate-questions/streamhas been removed (it was previously dev-only).
Follow-up Migration Notes
POST /follow-up itself is not new — the legacy contract already exposed it for more-questions. What changed is that it now supports three request types (selected via request_type) and uses the renamed response_id continuation handle:
more-questions—question_configis required (replaces legacyquestion_type_counts)inline-explanation—question_configis ignored; put the question text ininput(added inv2.8.0)chat—question_configis ignored; always streamed (added inv2.8.0)
The continuation handle is response_id (renamed in v3.2.0 from the previous request field previous_response_id, which had been live since v2.5.0). Pass the response_id returned by the previous turn:
POST /follow-up
{
- "previous_response_id": "resp_abc123",
+ "response_id": "resp_abc123",
"request_type": "more-questions",
"question_config": { "type_counts": { "true_false": 2 } }
}
Follow-up request models reject unknown fields (extra="forbid"), so legacy or misspelled keys return 422.
Built by Karson AI, Inc.