Examples and Test Snippets
Quick Smoke Test
Use this order for a basic integration check:
- Call
GET /credits - Call
GET /models - Create a student assignment with
POST /assignments, or usePOST /evaluations/one-passif you do not want to create an assignment - Call
POST /evaluationswith the returnedassignment_id - Poll
GET /evaluations/:taskId/status - Fetch the final payload from
GET /evaluations/:taskId - Call
POST /feedback-rewrite - If you need to build a new marking model, then test
questions-ingest-preview,rubrics-ingest-preview, andPOST /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 Leecurl 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, notX-TOKEN-KEY - Confirm you are using a KEATH public API key (
kct_...), not a provider key such as a NewAPIsk-...key - Confirm
model_idcomes fromGET /models - Confirm
assignment_idcomes fromPOST /assignmentsor the product assignment detail URL, not fromGET /models - Confirm the selected model status is
readybefore 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_idis returned byGET /models - For multipart requests with
assets, confirmassetsis a JSON array string - For multipart evaluation requests with
rubricsorcurrent_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