KEATH.AIKEATH.AI
  • Home
  • KEATH Public API v1
  • Examples and Test Snippets
  • Migration from the Legacy API
  • 主页
  • KEATH Public API v1
  • 示例与测试代码
  • 从旧接口迁移
  • Accueil
  • API publique KEATH v1
  • Exemples et tests rapides
  • Migration depuis l'ancienne API
  • Inicio
  • API pública KEATH v1
  • Ejemplos y pruebas
  • Migración desde la API anterior
  • الصفحة الرئيسية
  • KEATH Public API v1
  • أمثلة واختبارات سريعة
  • الانتقال من الواجهة القديمة
  • Home
  • KEATH Public API v1
  • Examples and Test Snippets
  • Migration from the Legacy API
  • 主页
  • KEATH Public API v1
  • 示例与测试代码
  • 从旧接口迁移
  • Accueil
  • API publique KEATH v1
  • Exemples et tests rapides
  • Migration depuis l'ancienne API
  • Inicio
  • API pública KEATH v1
  • Ejemplos y pruebas
  • Migración desde la API anterior
  • الصفحة الرئيسية
  • KEATH Public API v1
  • أمثلة واختبارات سريعة
  • الانتقال من الواجهة القديمة
  • KEATH Public API v1
  • Examples and Test Snippets
  • Migration from the Legacy API

Examples and Test Snippets

Quick Smoke Test

Use this order for a basic integration check:

  1. Call GET /credits
  2. Call GET /models
  3. Create a student assignment with POST /assignments, or use POST /evaluations/one-pass if you do not want to create an assignment
  4. Call POST /evaluations with the returned assignment_id
  5. Poll GET /evaluations/:taskId/status
  6. Fetch the final payload from GET /evaluations/:taskId
  7. Call POST /feedback-rewrite
  8. If you need to build a new marking model, then test questions-ingest-preview, rubrics-ingest-preview, and POST /models

If steps 1 and 2 work but evaluation fails, the most common reason is that the client is using a model_id where an assignment_id is required, the selected model is not ready, or the request still references the legacy X-TOKEN-KEY flow.

The public API key in these examples is a KEATH key that starts with kct_. Provider keys used by KEATH internally, such as NewAPI or Gemini keys, should stay on the server and should never be sent from your client.

Test Input Text for specification

Use these as safe test prompts when a source file contains multiple questions or extra pages.

Question ingest:

Only parse Question 2. Ignore cover pages, sample answers, and any teacher notes. If the PDF contains multiple tasks, focus on the situational writing task only.

Rubric ingest:

Use only the rubric table for the target writing task. Ignore sample answers, explanatory notes, and any extra worksheets.

Feedback rewrite:

Make the feedback shorter, clearer, and more student-friendly. Keep the judgment the same.

Test Essay Text

Use this as a safe grading payload when you want to verify POST /evaluations without using production student work.

Dear Mrs Tan,

I am writing to request permission for our class to organise a recycling drive next Friday after school. Many students have noticed that large amounts of paper bottles and food packaging are thrown away every day. We believe a short class project would help students build better habits and understand why recycling matters.

If the school approves the activity our class can prepare labelled collection boxes and take turns to explain the instructions to other students. We can also make a short announcement during assembly so that everyone knows what items can be collected.

I hope you will consider this proposal. Thank you for your time and support.

Yours sincerely,
Jamie Lee

curl Examples

Credits

curl "https://keath.ai/api/keath/public/v1/credits" \
  -H "X-API-Key: kct_your_api_key"

Question ingest with direct PDF upload

curl -X POST "https://keath.ai/api/keath/public/v1/questions-ingest-preview" \
  -H "X-API-Key: kct_your_api_key" \
  -F "file=@./question-pack.pdf;type=application/pdf" \
  -F "specification=Only parse Question 2. Ignore the sample answer pages."

If your HTTP client cannot set type=application/pdf, KEATH can infer common PDF/image types from the file extension. Sending the MIME type explicitly is still recommended because it is clearer and works across more proxies.

Question ingest with image upload plus existing URL

curl -X POST "https://keath.ai/api/keath/public/v1/questions-ingest-preview" \
  -H "X-API-Key: kct_your_api_key" \
  -F "files=@./scan-1.jpg;type=image/jpeg" \
  -F 'assets=[{"url":"https://cdn.example.com/supporting-page.pdf","mime_type":"application/pdf","name":"supporting-page.pdf"}]' \
  -F "specification=Use the uploaded image as the target question."

Rubric ingest with direct upload

curl -X POST "https://keath.ai/api/keath/public/v1/rubrics-ingest-preview" \
  -H "X-API-Key: kct_your_api_key" \
  -F "file=@./rubric.pdf;type=application/pdf" \
  -F "total_score=30" \
  -F "cum_method=sum" \
  -F "specification=Use the rubric table on page 1 only."

Create an assignment from a model

curl -X POST "https://keath.ai/api/keath/public/v1/assignments" \
  -H "X-API-Key: kct_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "model_id": 930,
    "assignment_name": "Situation Writing Test",
    "assignment_desc": "Student-facing writing assignment",
    "project_subject": "English",
    "deadline_time": "2026-06-01T00:00:00.000Z",
    "expected_number": 30,
    "file_type": "pdf"
  }'

Submit an evaluation

curl -X POST "https://keath.ai/api/keath/public/v1/evaluations" \
  -H "X-API-Key: kct_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "assignment_id": 1620,
    "paper_content": "Dear Mrs Tan,\n\nI am writing to request permission for our class to organise a recycling drive next Friday after school...",
    "style": "Bullet"
  }'

Submit an evaluation with an answer file

curl -X POST "https://keath.ai/api/keath/public/v1/evaluations" \
  -H "X-API-Key: kct_your_api_key" \
  -F "assignment_id=1620" \
  -F "answer_file=@./student-answer.pdf;type=application/pdf" \
  -F "student_id=anon-student-001" \
  -F "style=Bullet"

One-pass evaluation

curl -X POST "https://keath.ai/api/keath/public/v1/evaluations/one-pass" \
  -H "X-API-Key: kct_your_api_key" \
  -H "Idempotency-Key: eval-student-001-attempt-1" \
  -F "model_id=930" \
  -F "question_file=@./question.pdf;type=application/pdf" \
  -F "rubric_file=@./rubric.docx;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document" \
  -F "answer_file=@./student-answer.txt;type=text/plain" \
  -F "student_id=anon-student-001" \
  -F "specification=Use Question 2 only." \
  -F "style=Bullet"

The response is HTTP 202. Save its public task_id; do not wait for the grading result in this request.

Batch one-pass evaluation

curl -X POST "https://keath.ai/api/keath/public/v1/evaluations/batch" \
  -H "X-API-Key: kct_your_api_key" \
  -H "Idempotency-Key: class-5a-writing-2026-07-15" \
  -H "Content-Type: application/json" \
  -d '{
    "evaluations": [
      {"model_id":930,"question_text":"Write a formal letter.","paper_content":"Dear Principal, ...","student_id":"anon-001"},
      {"model_id":930,"question_text":"Write a formal letter.","paper_content":"Dear Principal, I propose ...","student_id":"anon-002"}
    ],
    "callback_url":"https://integration.example.com/keath/results"
  }'

Poll evaluation status

curl "https://keath.ai/api/keath/public/v1/evaluations/task_123/status" \
  -H "X-API-Key: kct_your_api_key"

Fetch evaluation result

curl "https://keath.ai/api/keath/public/v1/evaluations/task_123" \
  -H "X-API-Key: kct_your_api_key"

Cancel evaluation

curl -X POST "https://keath.ai/api/keath/public/v1/evaluations/task_123/cancel" \
  -H "X-API-Key: kct_your_api_key"

Feedback rewrite

curl -X POST "https://keath.ai/api/keath/public/v1/feedback-rewrite" \
  -H "X-API-Key: kct_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "style": "bullet_points",
    "instruction": "Make this shorter and more student-friendly.",
    "sections": [
      {
        "item": "Content",
        "comment": "Your answer contains relevant ideas but the explanation remains underdeveloped.",
        "score": 7,
        "max_score": 10
      }
    ]
  }'

JavaScript Example

const apiKey = process.env.KEATH_API_KEY
const baseUrl = 'https://keath.ai/api/keath/public/v1'

async function getCredits() {
  const response = await fetch(`${baseUrl}/credits`, {
    headers: {
      'X-API-Key': apiKey,
    },
  })
  return response.json()
}

async function previewQuestionsWithPdf(file) {
  const formData = new FormData()
  formData.append('file', file, file.name)
  formData.append(
    'specification',
    'Only parse Question 2. Ignore the sample answer pages.',
  )

  const response = await fetch(`${baseUrl}/questions-ingest-preview`, {
    method: 'POST',
    headers: {
      'X-API-Key': apiKey,
    },
    body: formData,
  })

  return response.json()
}

async function createAssignment(modelId) {
  const response = await fetch(`${baseUrl}/assignments`, {
    method: 'POST',
    headers: {
      'X-API-Key': apiKey,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model_id: modelId,
      assignment_name: 'Situation Writing Test',
      assignment_desc: 'Student-facing writing assignment',
      project_subject: 'English',
      deadline_time: '2026-06-01T00:00:00.000Z',
      expected_number: 30,
      file_type: 'pdf',
    }),
  })

  return response.json()
}

async function submitEvaluation(assignmentId, paperContent) {
  const response = await fetch(`${baseUrl}/evaluations`, {
    method: 'POST',
    headers: {
      'X-API-Key': apiKey,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      assignment_id: assignmentId,
      paper_content: paperContent,
      style: 'Bullet',
    }),
  })

  return response.json()
}

async function submitEvaluationWithFile(assignmentId, answerFile) {
  const formData = new FormData()
  formData.append('assignment_id', String(assignmentId))
  formData.append('answer_file', answerFile, answerFile.name)
  formData.append('student_id', 'anon-student-001')
  formData.append('style', 'Bullet')

  const response = await fetch(`${baseUrl}/evaluations`, {
    method: 'POST',
    headers: {
      'X-API-Key': apiKey,
    },
    body: formData,
  })

  return response.json()
}

async function submitOnePassEvaluation({ modelId, questionFile, rubricFile, answerFile, requestId }) {
  const formData = new FormData()
  formData.append('model_id', String(modelId))
  formData.append('question_file', questionFile, questionFile.name)
  formData.append('rubric_file', rubricFile, rubricFile.name)
  formData.append('answer_file', answerFile, answerFile.name)
  formData.append('student_id', 'anon-student-001')
  formData.append('specification', 'Use Question 2 only.')
  formData.append('style', 'Bullet')

  const response = await fetch(`${baseUrl}/evaluations/one-pass`, {
    method: 'POST',
    headers: {
      'X-API-Key': apiKey,
      'Idempotency-Key': requestId,
    },
    body: formData,
  })

  return response.json()
}

async function pollEvaluationUntilDone(taskId, options = {}) {
  const maxAttempts = options.maxAttempts ?? 20
  const initialDelayMs = options.initialDelayMs ?? 3000
  const maxDelayMs = options.maxDelayMs ?? 15000

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const response = await fetch(`${baseUrl}/evaluations/${taskId}/status`, {
      headers: {
        'X-API-Key': apiKey,
      },
    })
    const payload = await response.json()
    const status = payload.data?.status || payload.status

    if (status === 'SUCCESS' || status === 'FAILURE' || status === 'CANCEL') {
      return payload
    }

    const delayMs = Math.min(initialDelayMs * attempt, maxDelayMs)
    await new Promise(resolve => setTimeout(resolve, delayMs))
  }

  throw new Error(`Evaluation ${taskId} did not finish after ${maxAttempts} polling attempts`)
}

Python Example

import json
import time
import requests

API_KEY = "kct_your_api_key"
BASE_URL = "https://keath.ai/api/keath/public/v1"


def get_credits():
    response = requests.get(
        f"{BASE_URL}/credits",
        headers={"X-API-Key": API_KEY},
        timeout=60,
    )
    response.raise_for_status()
    return response.json()


def preview_questions_with_pdf(path: str):
    with open(path, "rb") as f:
        response = requests.post(
            f"{BASE_URL}/questions-ingest-preview",
            headers={"X-API-Key": API_KEY},
            files={"file": ("question-pack.pdf", f, "application/pdf")},
            data={
                "specification": "Only parse Question 2. Ignore the sample answer pages."
            },
            timeout=300,
        )
    response.raise_for_status()
    return response.json()


def preview_questions_mixed(path: str):
    with open(path, "rb") as f:
        response = requests.post(
            f"{BASE_URL}/questions-ingest-preview",
            headers={"X-API-Key": API_KEY},
            files={"file": ("scan-1.jpg", f, "image/jpeg")},
            data={
                "assets": json.dumps(
                    [
                        {
                            "url": "https://cdn.example.com/supporting-page.pdf",
                            "mime_type": "application/pdf",
                            "name": "supporting-page.pdf",
                        }
                    ]
                ),
                "specification": "Use the uploaded image as the target question.",
            },
            timeout=300,
        )
    response.raise_for_status()
    return response.json()


def submit_evaluation(assignment_id: int, paper_content: str):
    response = requests.post(
        f"{BASE_URL}/evaluations",
        headers={
            "X-API-Key": API_KEY,
            "Content-Type": "application/json",
        },
        json={
            "assignment_id": assignment_id,
            "paper_content": paper_content,
            "style": "Bullet",
        },
        timeout=300,
    )
    response.raise_for_status()
    return response.json()


def submit_evaluation_with_file(assignment_id: int, answer_path: str):
    with open(answer_path, "rb") as f:
        response = requests.post(
            f"{BASE_URL}/evaluations",
            headers={"X-API-Key": API_KEY},
            data={
                "assignment_id": str(assignment_id),
                "student_id": "anon-student-001",
                "style": "Bullet",
            },
            files={"answer_file": ("student-answer.pdf", f, "application/pdf")},
            timeout=300,
        )
    response.raise_for_status()
    return response.json()


def submit_one_pass_evaluation(
    model_id: int,
    question_path: str,
    rubric_path: str,
    answer_path: str,
    request_id: str,
):
    with open(question_path, "rb") as question, open(rubric_path, "rb") as rubric, open(answer_path, "rb") as answer:
        response = requests.post(
            f"{BASE_URL}/evaluations/one-pass",
            headers={
                "X-API-Key": API_KEY,
                "Idempotency-Key": request_id,
            },
            data={
                "model_id": str(model_id),
                "student_id": "anon-student-001",
                "specification": "Use Question 2 only.",
                "style": "Bullet",
            },
            files={
                "question_file": ("question.pdf", question, "application/pdf"),
                "rubric_file": (
                    "rubric.docx",
                    rubric,
                    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                ),
                "answer_file": ("student-answer.txt", answer, "text/plain"),
            },
            timeout=300,
        )
    response.raise_for_status()
    return response.json()


def wait_for_evaluation(task_id: str):
    max_attempts = 20
    initial_delay_seconds = 3
    max_delay_seconds = 15

    for attempt in range(1, max_attempts + 1):
        response = requests.get(
            f"{BASE_URL}/evaluations/{task_id}/status",
            headers={"X-API-Key": API_KEY},
            timeout=60,
        )
        response.raise_for_status()
        payload = response.json()
        data = payload.get("data", payload)
        status = data["status"]
        if status in {"SUCCESS", "FAILURE", "CANCEL"}:
            return payload
        delay_seconds = min(initial_delay_seconds * attempt, max_delay_seconds)
        time.sleep(delay_seconds)

    raise TimeoutError(
        f"Evaluation {task_id} did not finish after {max_attempts} polling attempts"
    )

Debug Checklist

  • Confirm the request is sent to /api/keath/public/v1/...
  • Confirm the header is X-API-Key, not X-TOKEN-KEY
  • Confirm you are using a KEATH public API key (kct_...), not a provider key such as a NewAPI sk-... key
  • Confirm model_id comes from GET /models
  • Confirm assignment_id comes from POST /assignments or the product assignment detail URL, not from GET /models
  • Confirm the selected model status is ready before creating assignments or one-pass evaluations
  • For evaluation file uploads, confirm the student answer uses an accepted answer field such as answer_file
  • For one-pass requests, confirm model_id is returned by GET /models
  • For multipart requests with assets, confirm assets is a JSON array string
  • For multipart evaluation requests with rubrics or current_feedbacks, confirm they are JSON array strings
  • For uploaded files, prefer an explicit supported MIME type; if unavailable, make sure the filename has a supported extension
  • For large PDFs, confirm each file is under 20 MB
  • For Google Docs, export the document as DOCX, PDF, or plain text before upload; direct Google Docs OAuth import is not part of public v1 yet
Last Updated: 7/15/26, 8:35 AM
Contributors: PJ
Prev
KEATH Public API v1
Next
Migration from the Legacy API