API Reference
REST API for exam grading requests and result retrieval
REST API for submitting exam responses to receive per-question AI feedback and weakness reports per attempt.
Common
Base URL
https://tap-file-upload-production.coxwave.linkAuthentication
All API requests require the TAP-API-KEY header.
Request Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
| TAP-API-KEY | String | O | Your issued API key |
Question Types
Questions are identified by problem_type. The section (Writing or Speaking) is derived from this value, so you do not send it.
| problem_type | Question | Section | Question no. | QNOTYPE from the lookup API |
|---|---|---|---|---|
| MSOM | Build a Sentence | Writing | 1-10 | MSOM |
| WDS | Write an Email | Writing | 11 | WDS |
| WDS | Write for an Academic Discussion | Writing | 12 | WDS |
| LISTENREPEAT | Listen and Repeat | Speaking | 1-7 | AUDO |
| INTERVIEW | Take an Interview | Speaking | 8-11 | AUDO |
Speaking cannot use QNOTYPE as it comes: the lookup API returns AUDO for every
speaking question. Split it by question number and send either LISTENREPEAT (1-7)
or INTERVIEW (8-11).
A request may only carry questions from one section. Mixing Writing and Speaking questions is rejected with 422.
Question Information
The prompt, task situation and correct answer are not sent with the request. The server fetches them from the question lookup API. A request carries only what identifies the question and the learner's response.
| Field | Role |
|---|---|
| exam_id | The COURSE_ID from the question lookup API, shaped {TEST_ID}_{TESTNO} |
| question_number | SEQ from the question list, not QNOSEQ which restarts per section |
A question is kept once fetched, so the same question is never looked up twice.
Writing is split into sections, so question 11 (Write an Email) carries QNOSEQ 1.
Send SEQ, which counts across the whole exam, as question_number.
Feedback Status
| status | Description |
|---|---|
| PENDING | Accepted, waiting to be graded |
| PROCESSING | Feedback in progress |
| DONE | Graded, result available |
| FAILED | Feedback failed, reason in explanation |
Exam Feedback
Submit one whole section of an attempt.
One job is created per question, and results are retrieved by the returned job_id.
Submit Writing
Request
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| exam_id | String | O | Exam ID. Send the COURSE_ID from the question lookup API as it is (e.g. "I100000760_1") |
| user_id | String | O | Learner ID |
| section_score | Number | O | Section score (1.0-6.0, in steps of 0.5) |
| items | Array of objects | O | Questions, at least one. See items item |
items item
| Parameter | Type | Required | Description |
|---|---|---|---|
| problem_id | String | O | Question ID. Must be unique within a request |
| problem_type | String | O | MSOM or WDS |
| question_number | Number | O | Question number. It decides which rubric a WDS item uses, 11 (Write an Email) or 12 (Academic Discussion). Rejected with 422 when missing |
| score | Number | O | This question's score. Build a Sentence is 0 or 1, the rest are 0-5 |
| user_answer | String | O | The learner's response |
Build a Sentence (MSOM) is graded by exact match against correct_answer. The correct sentence comes from the question lookup, case and punctuation are ignored, and the model only writes the explanation.
Request Example
curl -X POST "https://tap-file-upload-production.coxwave.link/api/v1/courses/exam-gradings/writing" \
-H "Content-Type: application/json" \
-H "TAP-API-KEY: {YOUR_API_KEY}" \
-d '{
"exam_id": "I100000760_1",
"user_id": "u-1001",
"section_score": 4.0,
"items": [
{
"problem_id": "w1",
"question_number": 1,
"problem_type": "MSOM",
"user_answer": "I wonder where did she find the book."
},
{
"problem_id": "w11",
"question_number": 11,
"problem_type": "WDS",
"user_answer": "Dear Professor Kim, thank you for your email about the group project..."
},
{
"problem_id": "w12",
"question_number": 12,
"problem_type": "WDS",
"user_answer": "I agree with Andrew that online shopping has changed local businesses..."
}
]
}'Response
202 Accepted
Response Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
| jobs | Array of objects | O | Feedback jobs created, one per question |
| jobs[].job_id | String | O | Feedback job ID, used to retrieve the result |
| jobs[].problem_id | String | O | Question ID |
| jobs[].status | String | O | Status at submission (PENDING) |
Response Example
{
"jobs": [
{ "job_id": "6c4e8c77-...", "problem_id": "w1", "status": "PENDING" },
{ "job_id": "5e0b1c2f-...", "problem_id": "w11", "status": "PENDING" },
{ "job_id": "a1b2c3d4-...", "problem_id": "w12", "status": "PENDING" }
]
}Submit Speaking
Request
Sent as multipart/form-data.
| Parameter | Type | Required | Description |
|---|---|---|---|
| exam_id | String | O | Exam ID. Send the COURSE_ID from the question lookup API as it is (e.g. "I100000760_1") |
| user_id | String | O | Learner ID |
| section_score | Number | O | Section score (1.0-6.0, in steps of 0.5) |
| items | String | O | Questions as a JSON array, sent as a string. See items item |
| audios | Array of files | O | Answer recordings. Names must match audio_file_name in items |
items item
| Parameter | Type | Required | Description |
|---|---|---|---|
| problem_id | String | O | Question ID. Must be unique within a request |
| problem_type | String | O | LISTENREPEAT or INTERVIEW |
| question_number | Number | O | Question number. Rejected with 422 when missing |
| score | Number | O | This question's score. Build a Sentence is 0 or 1, the rest are 0-5 |
| audio_file_name | String | O | Recording file name for this question. Must exactly match a file uploaded in audios |
Recordings are stored and transcribed before grading. The transcript carries the timing of each utterance, so pace and pauses are reflected in the score.
Send the sentence to repeat for Listen and Repeat, and the full question for Take an Interview, from the question lookup. When the lookup fails, there is no way to know what was asked, so the grading ends as FAILED.
Request Example
curl -X POST "https://tap-file-upload-production.coxwave.link/api/v1/courses/exam-gradings/speaking" \
-H "TAP-API-KEY: {YOUR_API_KEY}" \
-F "exam_id=I100000760_1" \
-F "user_id=u-1001" \
-F "section_score=2.0" \
-F 'items=[
{
"problem_id": "s1",
"question_number": 1,
"problem_type": "LISTENREPEAT",
"score": 5,
"audio_file_name": "q01.mp3"
},
{
"problem_id": "s8",
"question_number": 8,
"problem_type": "INTERVIEW",
"score": 3,
"audio_file_name": "q08.mp3"
}
]' \
-F "audios=@q01.mp3;type=audio/mpeg" \
-F "audios=@q08.mp3;type=audio/mpeg"Response
202 Accepted, in the same shape as the Writing request.
{
"jobs": [
{ "job_id": "7a577745-...", "problem_id": "s1", "status": "PENDING" },
{ "job_id": "b2c3d4e5-...", "problem_id": "s8", "status": "PENDING" }
]
}Retrieve Result
Request
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| job_id | String | O | Feedback job ID from the submission response |
Request Example
curl "https://tap-file-upload-production.coxwave.link/api/v1/courses/exam-gradings/7a577745-8fb2-453d-aa57-b17fc80e0e39" \
-H "TAP-API-KEY: {YOUR_API_KEY}"Response
| Parameter | Type | Required | Description |
|---|---|---|---|
| job_id | String | O | Feedback job ID |
| status | String | O | PENDING / PROCESSING / DONE / FAILED |
| result | Object | - | Feedback result, null until the feedback is written |
| weakness_report | String | O | Whether the weakness diagnosis is ready. PENDING or READY |
result object
| Parameter | Type | Required | Description |
|---|---|---|---|
| exam_id | String | O | Exam ID |
| problem_id | String | O | Question ID |
| question_number | Number | O | Question number. Rejected with 422 when missing |
| problem_type | String | O | Question type |
| grade_type | String | O | WRITING or SPEAKING |
| score | Number | O | This question's score. Build a Sentence is 0 or 1, the rest are 0-5 |
| score_base | Number | O | Maximum score. 1 for Build a Sentence, 5 otherwise |
| section_score | Number | O | Section score (1.0-6.0, in steps of 0.5) |
| explanation | String | O | Feedback and the reasoning behind the score |
| user_answer | String | O | The learner's response. Transcribed text for Speaking |
| correct_answer | String | O | Model answer or correct sentence |
| assessment_results.strength | Array of strings | O | What the learner did well |
| assessment_results.weakness | Array of objects | O | Weaknesses, with tag and description |
| tips | Array of objects | O | Practice tips, with description and example |
| idea | Array of objects | O | Idea suggestions |
Response Example
{
"job_id": "7a577745-8fb2-453d-aa57-b17fc80e0e39",
"status": "DONE",
"result": {
"exam_id": "I100000760_1",
"problem_id": "s1",
"question_number": 1,
"problem_type": "LISTENREPEAT",
"grade_type": "SPEAKING",
"score": 3.0,
"score_base": 5.0,
"section_score": 2.0,
"explanation": "제시된 문장의 핵심 어휘는 전달했으나 중간에 끊김이 있어 3점입니다.",
"user_answer": "The ticket counter is over there.",
"assessment_results": {
"strength": ["핵심 어휘를 정확히 발화했습니다."],
"weakness": [
{ "tag": "답변 마무리", "description": "문장 끝을 흐리지 않고 끝까지 발화하세요." }
]
},
"tips": [
{ "description": "문장을 한 호흡에 말하는 연습을 해보세요.", "example": "The ticket counter / is over there." }
],
"idea": []
},
"weakness_report": "READY"
}Feedback is written in Korean, since it is shown to Korean learners.
Errors
| Code | Condition |
|---|---|
| 401 | Missing or invalid TAP-API-KEY header |
| 404 | Result requested for an unknown job_id |
| 422 | items is empty |
| 422 | Duplicate problem_id in items |
| 422 | Questions from more than one section in a request |
| 422 | Unknown problem_type |
| 422 | A WDS question has no question_number |
| 422 | No uploaded file matches audio_file_name |
| 422 | An uploaded audio file is empty |
