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.x build can skip rows that already match their version — see the Changelog for which release introduced each change.


What changed at a glance

AreaBefore (pre-v2.7.0)Now (3.4.x)
Generate request shapeTop-level num_questions, question_type, difficulty, question_type_counts, allowed_typesSingle required question_config object
reasoning defaultfalse (instant)true (reasoning path); send reasoning: false to keep instant
"mixed" keywordAllowed as a type and as a difficultyRemoved in both places
StreamingPOST /generate-questions/stream and POST /generate-questions/stream-by-questionPOST /generate-questions with "streaming": true
Follow-up request typesPOST /follow-up supported only more-questions (using question_type_counts)POST /follow-up now supports more-questions, inline-explanation, and chat
Vision inputNot supportedOptional image_urls on both endpoints
File attachmentsNot supportedattachments (images and documents) on both endpoints — preferred over image_urls
Continuation handleprevious_response_id (request body field)response_id returned by every turn; pass the same value back as response_id on /follow-up
Question objectsUnified shape with possible null fieldsType-scoped — only the keys relevant to type are present; server adds id and created_at
Unknown JSON keysSilently ignoredRejected (extra="forbid"422)

Endpoint Changes

Removed

  • POST /generate-questions/stream-by-questiondeleted. Use POST /generate-questions with "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; use POST /generate-questions with "streaming": true.

Added

  • POST /follow-up — the endpoint itself existed in the legacy contract (more-questions only), but it now accepts three request types selected via request_type:
    • more-questions — generate additional questions in the same thread (question_config required).
    • inline-explanation — produce a short explanation for a single question (question_config ignored).
    • chat — free-form tutoring chat over the thread; always streamed via SSE.
  • attachments on POST /generate-questions and POST /follow-up — send files (images and documents like PDF/Word/text) as { "url", "mime_type", "name"? } with public HTTPS URLs. Preferred over legacy image_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_questions
  • question_type
  • question_type_counts
  • difficulty (now lives inside question_config)
  • allowed_types

The "mixed" concept is gone:

  • No "mixed" key inside question_config.type_counts
  • No "mixed" value for difficulty (use null or omit)
  • No implicit "open mix" default — question_config must 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 return 422

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 0 values 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

PayloadWhy
question_config omitted or nullThe 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 keyextra="forbid"

Migrating Common Old Patterns

Old patternNew pattern
Omit question_config for open mixSend 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-questionPOST /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 reasoning explicitly? Nothing changes — an explicit true or false is honored exactly as before.
  • Relying on the old implicit false? Add reasoning: false to keep the instant contract (no reasoning tokens, no reasoning_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 type are present. There are no null placeholders for unused fields (e.g. a multiple_choice response no longer carries matchPairs: null or codeBlock: null).
  • id (UUID) and created_at (ISO 8601) are added server-side on every question.
  • reorder, matching, and fill-in-the-blank have no answer field at all — the canonical answer comes from choices order, positional matchPairs, or choices[].label respectively.
  • For multiple_choice questions, server-side enrichment shuffles choices (Fisher-Yates) and re-keys them to c1, c2, … in the emitted order, then remaps answer.value to the new key. Other types' choices (reorder, fill-in-the-blank, true_false) are emitted in canonical order. Clients that previously cached a multiple_choice choice key from a prior turn cannot reuse it across turns.
  • response_id is the continuation handle. Pass it back as response_id on the next POST /follow-up turn.

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 attachments over image_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": true on POST /generate-questions to receive per-question SSE.
  • The same flag applies to POST /follow-up for more-questions and inline-explanation. chat is always streamed regardless of the flag.
  • SSE event taxonomy by stream type:
    • Question streams (/generate-questions, /follow-up more-questions): question (per question), done (terminal — carries response_id, total_questions, and optional validation / agent_metadata), error.
    • /follow-up inline-explanation stream: explanation_text (incremental delta), explanation (full text emitted once at the end), done (carries response_id and explanation), error.
    • /follow-up chat stream (always streamed): text (incremental delta), done (carries response_id and full message), error.
    • All streams with reasoning: true may also emit reasoning_summary_part ({ "index": N } — start of a new part) and reasoning_summary ({ "text": "<delta>", "index": N } — incremental text).
  • The legacy POST /generate-questions/stream-by-question path is gone.
  • The legacy raw-chunk POST /generate-questions/stream has 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-questionsquestion_config is required (replaces legacy question_type_counts)
  • inline-explanationquestion_config is ignored; put the question text in input (added in v2.8.0)
  • chatquestion_config is ignored; always streamed (added in v2.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.