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.ai

Authentication

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

POST/generate-questions

Generate questions from input context.

Parameters

contextstringrequired

The input text to generate questions on (e.g. an article, a book chapter, content instructions, or other learning material).

question_configobjectrequired

Controls 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_choice
    • true_false
    • short_answer
    • exact_answer
    • reorder
    • code-output
    • matching
    • fill-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 or null count — an empty type_counts or only 0 values yields 422.
difficulty"easy" | "medium" | "hard" | nulloptional

Optional. 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 | null

Optional 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[] | null

Preferred 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[] | null

Legacy HTTPS image URLs (max 1,500, deduplicated, each URL ≤ 8,192 chars). Prefer attachments.

reasoningboolean

Reasoning 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.

streamingboolean

When 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_id is the continuation handle for this model session — pass it back as response_id on the next POST /follow-up.
  • Each question is type-scoped: only the keys relevant to its type are present (no null placeholders). See Response Schema.
  • id and created_at are added server-side. validation and agent_metadata are optional diagnostics; reasoning_summary appears whenever reasoning is on — which is now the default (send reasoning: false to suppress it).

Follow-up

POST/follow-up

Continue 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 | null

The 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-questions

Same contract as /generate-questions question_config. Ignored for inline-explanation and chat.

contextstring | null

Optional extra learning material for this turn; does not replace the stored conversation context.

inputstring | null

more-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[] | null

Legacy HTTPS image URLs (max 1,500, deduplicated, each URL ≤ 8,192 chars) for this turn. Prefer attachments.

reasoningboolean

Reasoning 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.

streamingboolean

When 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

POST/generate-questions
event: 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 types
difficulty"easy" | "medium" | "hard"
questionThe question text (fill-in-the-blank uses {{key}} placeholders)
idUUID — added server-side before emit
created_atISO 8601 timestamp — added server-side

Type-specific fields

Only the fields listed for a given type are emitted; everything else is absent.

multiple_choicechoices: [{ 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_falsechoices with keys pinned to "true"/"false", answer: { "value": "true" | "false" }.
short_answeranswer: { "value": string, "rubric": string[] | null }. No choices.
exact_answeranswer: { "value": string }. No choices.
code-outputcodeBlock: { language, script }, answer: { "value": "<stdout>" }. No choices.
reorderchoices: [{ key, label }] in canonical (correct) order. No answer field.
matchingmatchPairs: { left: [{key,label}], right: [{key,label}] }; canonical pairs are positional (left[i]right[i]). No answer field.
fill-in-the-blankchoices 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.

200Success
400Bad request (model rejected it, or an attachment URL could not be fetched)
401Missing/invalid API key
413Request or attachments too large
422Validation error
429Rate limited
500Server error

Rate Limits

The API implements rate limiting to ensure fair usage.

Requests1,000 per minute (per API key)
Max context50,000 characters
Max request size1 MB
Max image inputs1,500
Max document attachments50
Max file URL length8,192 chars

Implement exponential backoff when receiving 429 errors.