MakeQuestions API by Karson AI
API Reference
Generate educational questions from text (and optional images) with the Karson AI API for question generation. The API supports 8 question types, reasoning mode, follow-up turns, and real-time streaming.
Supported question types
- multiple_choice
- true_false
- short_answer
- exact_answer
- reorder
- code-output
- matching
- fill-in-the-blank
Base URL
https://api.makequestions.aiAuthentication
Authenticate requests by including your API key in the X-API-Key header.
To get an API key, email support@karson.ai.
curl https://api.makequestions.ai/generate-questions \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"context": "Your text here...",
"question_config": {
"type_counts": {
"true_false": 3,
"multiple_choice": 2,
"short_answer": 1,
"fill-in-the-blank": 1,
"reorder": 1
},
"difficulty": "medium"
}
}'Generate Questions
/generate-questionsGenerate questions from input context.
Parameters
contextstringrequiredThe input text to generate questions on (e.g. an article, a book chapter, content instructions, or other learning material).
question_configobjectrequiredControls which question types to generate and (optionally) the difficulty band. It has two fields:
type_countsmaprequired- Each key must be one of the following question types. If a type is not present in the map, the response will not include questions of that type.
multiple_choicetrue_falseshort_answerexact_answerreordercode-outputmatchingfill-in-the-blank
- Each value must be one of:
- A positive integer — fixed number of questions for that type.
null— the model chooses the count; you always receive at least one question of that type.0— exclude that type (same as omitting the key).
- The server drops keys whose value is
0. At least one type must remain with a positive integer ornullcount — an emptytype_countsor only0values yields422.
difficulty"easy" | "medium" | "hard" | nulloptionalOptional. Omit the field or set null for mixed difficulty; otherwise use "easy", "medium", or "hard".
Examples
1. Selected types with exact counts
{
"type_counts": {
"multiple_choice": 3,
"true_false": 2,
"short_answer": 1
},
"difficulty": "medium"
}2. Selected types, model-chosen counts (null per type)
{
"type_counts": {
"multiple_choice": null,
"true_false": null
},
"difficulty": null
}3. Mix of exact counts and null
{
"type_counts": {
"multiple_choice": 3,
"true_false": null,
"short_answer": 1
}
}inputstring | nullOptional steering text, separate from context. Does not replace the source material; it nudges tone, difficulty of wording, or what to emphasize. Whitespace-only values are treated as omitted.
Examples
Focus on exam-style wording; use plausible distractors.Keep language at a high-school reading level; avoid undefined jargon.Prioritize conceptual understanding over rote memorization.Multiple choice with 3 options or 5 options per question.
attachmentsobject[] | nullPreferred way to send files. Each item is { "url", "mime_type", "name"? } with a public HTTPS url and a supported mime_type. Images (image/jpeg, image/png, image/gif, image/webp) are sent as vision input; documents (application/pdf, .doc, .docx, text/plain) are read directly as source material. Up to 1,500 images and 50 documents per request; URLs must be HTTPS and ≤ 8,192 chars.
image_urlsstring[] | nullLegacy HTTPS image URLs (max 1,500, deduplicated, each URL ≤ 8,192 chars). Prefer attachments.
reasoningbooleanReasoning model path — the default. Adds reasoning_summary to the response (and reasoning_summary_part / reasoning_summary SSE events when streaming), and bills reasoning tokens. Set to false for instant mode (no reasoning tokens, no summary). Default: true.
streamingbooleanWhen true, streams questions via SSE from the same endpoint. Default: false (full JSON response).
Request example
{
"context": "Photosynthesis is the process by which plants convert sunlight into energy.",
"question_config": {
"type_counts": {
"multiple_choice": 2,
"true_false": 1
},
"difficulty": "medium"
},
"input": "Use concise, exam-style wording.",
"attachments": [
{
"url": "https://cdn.example.com/photosynthesis-notes.pdf",
"mime_type": "application/pdf",
"name": "photosynthesis-notes.pdf"
}
],
"reasoning": true,
"streaming": false
}Response example
{
"success": true,
"response_id": "resp_abc123",
"total_questions": 3,
"questions": [
{
"type": "multiple_choice",
"difficulty": "medium",
"question": "What do plants convert into energy?",
"choices": [
{ "key": "c1", "label": "Sunlight" },
{ "key": "c2", "label": "Water" },
{ "key": "c3", "label": "Soil" },
{ "key": "c4", "label": "Air" }
],
"answer": { "value": "c1" },
"id": "8b2f0a5c-6e5e-4a1b-9b5e-9a9f1c4e6a7d",
"created_at": "2026-04-22T17:00:00.000Z"
},
{
"type": "multiple_choice",
"difficulty": "medium",
"question": "Which organelle is the main site of photosynthesis?",
"choices": [
{ "key": "c1", "label": "Chloroplast" },
{ "key": "c2", "label": "Mitochondrion" },
{ "key": "c3", "label": "Nucleus" },
{ "key": "c4", "label": "Ribosome" }
],
"answer": { "value": "c1" },
"id": "c4d8e1f2-3a4b-5c6d-7e8f-901234abcdef",
"created_at": "2026-04-22T17:00:01.000Z"
},
{
"type": "true_false",
"difficulty": "medium",
"question": "Photosynthesis releases oxygen as a byproduct.",
"choices": [
{ "key": "true", "label": "True" },
{ "key": "false", "label": "False" }
],
"answer": { "value": "true" },
"id": "d5e9f0a3-4b5c-6d7e-8f90-123456789abc",
"created_at": "2026-04-22T17:00:02.000Z"
}
],
"validation": {
"requested_counts": { "multiple_choice": 2, "true_false": 1 },
"actual_counts": { "multiple_choice": 2, "true_false": 1 },
"is_exact_match": true
}
}response_idis the continuation handle for this model session — pass it back asresponse_idon the nextPOST /follow-up.- Each question is type-scoped: only the keys relevant to its
typeare present (nonullplaceholders). See Response Schema. idandcreated_atare added server-side.validationandagent_metadataare optional diagnostics;reasoning_summaryappears wheneverreasoningis on — which is now the default (sendreasoning: falseto suppress it).
Follow-up
/follow-upContinue a session by sending its response_id in the request body. The server keeps prior context — you usually only add new instructions and (for more-questions) a fresh question_config. Unknown JSON keys are rejected with 422.
Request types
There are three ways to continue a conversation on this endpoint. Pick one by setting request_type:
more-questions
Generate additional quiz questions in the same conversation. Supply a full question_config (types and counts) for this turn, as you would on /generate-questions. Optional input steers tone or focus. Response is a questions array (batch JSON or SSE question events), same shapes as generate.
inline-explanation
Produce a short explanation for one question: put the question wording (and optional identifiers) in input. The model uses the thread's prior context and answers. Response is an explanation string in batch mode, or an explanation_text delta stream followed by a final explanation event when streaming: true.
chat
Free-form tutoring chat about the material in the thread: put the user's message in input. question_config is ignored. There is no batch JSON mode — the response is always SSE (text deltas, then done with the full message).
Parameters
request_typestringrequired"more-questions", "inline-explanation", or "chat" — see Request types above.
response_idstring | nullThe response_id returned by the previous turn (from /generate-questions or a prior /follow-up). Required for inline-explanation and chat. For more-questions, optional: include it to continue the same chain, or omit / null only when starting a fresh chain (still with a valid question_config).
question_configobjectrequired for more-questionsSame contract as /generate-questions question_config. Ignored for inline-explanation and chat.
contextstring | nullOptional extra learning material for this turn; does not replace the stored conversation context.
inputstring | nullmore-questions: optional steering (same idea as /generate-questions input). inline-explanation and chat: required non-empty string — the question text to explain, or the user's chat message, respectively.
image_urlsstring[] | nullLegacy HTTPS image URLs (max 1,500, deduplicated, each URL ≤ 8,192 chars) for this turn. Prefer attachments.
reasoningbooleanReasoning model path — the default. Adds reasoning_summary to the response (and reasoning_summary_part / reasoning_summary SSE events when streaming), and bills reasoning tokens. Set to false for instant mode (no reasoning tokens, no summary). Default: true.
streamingbooleanWhen true, more-questions and inline-explanation return SSE like /generate-questions. chat is always SSE; this flag does not apply. Default: false.
Request example — more-questions
{
"response_id": "resp_abc123",
"request_type": "more-questions",
"question_config": {
"type_counts": { "true_false": 2 },
"difficulty": "easy"
},
"input": "Focus on practical examples.",
"reasoning": true,
"streaming": false
}Response example — more-questions
When streaming is false, the body matches the request type. Optional fields (validation, agent_metadata, reasoning_summary) follow the same rules as /generate-questions where applicable.
{
"success": true,
"response_id": "resp_def456",
"total_questions": 2,
"questions": [
{
"type": "true_false",
"difficulty": "easy",
"question": "Plants need sunlight for photosynthesis.",
"choices": [
{ "key": "true", "label": "True" },
{ "key": "false", "label": "False" }
],
"answer": { "value": "true" },
"id": "11111111-1111-1111-1111-111111111111",
"created_at": "2026-04-22T18:00:00.000Z"
},
{
"type": "true_false",
"difficulty": "easy",
"question": "Chlorophyll absorbs mostly red and blue light.",
"choices": [
{ "key": "true", "label": "True" },
{ "key": "false", "label": "False" }
],
"answer": { "value": "true" },
"id": "22222222-2222-2222-2222-222222222222",
"created_at": "2026-04-22T18:00:01.000Z"
}
],
"validation": {
"requested_counts": { "true_false": 2 },
"actual_counts": { "true_false": 2 },
"is_exact_match": true
}
}Response example — inline-explanation
{
"success": true,
"response_id": "resp_def456",
"explanation": "The correct answer is …"
}chat has no non-streaming JSON body — responses are always SSE (event: text deltas, then event: done with message and response_id). See Streaming.
Streaming
Stream responses in real-time using Server-Sent Events (SSE) by sending streaming: true. chat is always streamed regardless of the flag. The exact event mix depends on the request type.
Common events
reasoning_summary_part— marks the start of a new reasoning summary part. Data: { "index": <0-based> }. Only when reasoning: true.reasoning_summary— incremental text delta for the current reasoning part. Data: { "text": "<delta>", "index": <0-based> }. Only when reasoning: true.error— failure event; connection closes after.Question streams (/generate-questions + /follow-up more-questions)
question— a fully enriched question. Data: { "index": <1-based>, "question": { ... } }.done— terminal event. Data: { success, response_id, total_questions, validation?, agent_metadata? }./follow-up inline-explanation stream
explanation_text— incremental text delta. Data: { "text": "<delta>" }.explanation— full explanation text emitted once after streaming completes. Data: { "explanation": "<full text>" }.done— terminal event. Data: { success, response_id, explanation, reasoning_summary? }./follow-up chat stream (always streamed)
text— incremental message delta. Data: { "text": "<delta>" }.done— terminal event. Data: { success, response_id, message, reasoning_summary? }.Example SSE Stream
/generate-questionsevent: question
data: {"index": 1, "question": {"type": "multiple_choice", ...}}
event: question
data: {"index": 2, "question": {"type": "true_false", ...}}
event: done
data: {"success": true, "response_id": "resp_abc123", "total_questions": 2}See Response Schema for the per-type field layout of streamed question objects.
Response Schema
Each question is a strict, type-scoped object. A variant only includes the fields meaningful to its type — there are no null placeholders for unused fields (e.g. a multiple_choice response has no matchPairs or codeBlock key at all).
Always present
typeOne of the 8 supported question typesdifficulty"easy" | "medium" | "hard"questionThe question text (fill-in-the-blank uses {{key}} placeholders)idUUID — added server-side before emitcreated_atISO 8601 timestamp — added server-sideType-specific fields
Only the fields listed for a given type are emitted; everything else is absent.
multiple_choice— choices: [{ key, label }], answer: { "value": "<choices[].key>" }. Choices are shuffled server-side and re-keyed c1, c2, … in the emitted order; answer.value is remapped to the new key. Choice keys are not stable across turns.true_false— choices with keys pinned to "true"/"false", answer: { "value": "true" | "false" }.short_answer— answer: { "value": string, "rubric": string[] | null }. No choices.exact_answer— answer: { "value": string }. No choices.code-output— codeBlock: { language, script }, answer: { "value": "<stdout>" }. No choices.reorder— choices: [{ key, label }] in canonical (correct) order. No answer field.matching— matchPairs: { left: [{key,label}], right: [{key,label}] }; canonical pairs are positional (left[i] ↔ right[i]). No answer field.fill-in-the-blank— choices hold the canonical answer per blank (choices[].label, keyed to {{key}} in question). No answer field.Examples
{
"type": "multiple_choice",
"difficulty": "easy",
"question": "What is the capital of France?",
"choices": [
{ "key": "c1", "label": "Paris" },
{ "key": "c2", "label": "London" },
{ "key": "c3", "label": "Berlin" },
{ "key": "c4", "label": "Madrid" }
],
"answer": { "value": "c1" },
"id": "8b2f0a5c-6e5e-4a1b-9b5e-9a9f1c4e6a7d",
"created_at": "2026-04-22T17:00:00.000Z"
}Streaming Examples
Minimal client snippets for consuming the SSE response from /generate-questions with streaming: true.
const response = await fetch("https://api.makequestions.ai/generate-questions", {
method: "POST",
headers: {
"X-API-Key": "your-api-key",
"Content-Type": "application/json",
},
body: JSON.stringify({
context: "Your text...",
question_config: {
type_counts: { multiple_choice: 3, true_false: 2 },
difficulty: null
},
streaming: true
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
// Parse SSE events and handle questions
console.log(text);
}Frontend Integration
Drop-in helpers for React and Next.js apps.
// lib/api.ts
// Call this from the server only (a Route Handler or Server Action). Your API
// key must never reach the browser — do NOT put it in a NEXT_PUBLIC_ variable.
const API_URL = "https://api.makequestions.ai";
type ConcreteQuestionType =
| "multiple_choice" | "true_false" | "short_answer" | "exact_answer"
| "reorder" | "code-output" | "matching" | "fill-in-the-blank";
type QuestionConfig = {
// Required, non-empty. Positive int = exact count; null = model decides
// quantity with a guaranteed minimum of 1 of that type.
type_counts: Partial<Record<ConcreteQuestionType, number | null>>;
difficulty?: "easy" | "medium" | "hard" | null;
};
type Attachment = {
url: string; // public HTTPS URL
mime_type: string; // e.g. "application/pdf", "image/png"
name?: string; // optional original filename
};
export async function generateQuestions(
context: string,
questionConfig: QuestionConfig,
options?: {
input?: string | null;
attachments?: Attachment[] | null; // preferred for files (images + documents)
imageUrls?: string[] | null; // legacy; prefer attachments
reasoning?: boolean;
}
) {
const response = await fetch(`${API_URL}/generate-questions`, {
method: "POST",
headers: {
"X-API-Key": process.env.MAKEQUESTIONS_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
context,
question_config: questionConfig,
input: options?.input ?? null,
attachments: options?.attachments ?? null,
image_urls: options?.imageUrls ?? null,
reasoning: options?.reasoning ?? true, // default is reasoning mode; pass false for instant
}),
});
if (!response.ok) {
throw new Error("Failed to generate questions");
}
return response.json();
}Errors
The API returns standard HTTP status codes with a detail field.
200Success400Bad request (model rejected it, or an attachment URL could not be fetched)401Missing/invalid API key413Request or attachments too large422Validation error429Rate limited500Server errorRate Limits
The API implements rate limiting to ensure fair usage.
Implement exponential backoff when receiving 429 errors.