提交與作答 Submission & Response
提交必須包含的作答範圍、冪等鍵、批次提交,以及提交的生命週期
提交(Submission)是一位學生對一份考試中,所有題目的作答(Response)的完整集合。一份提交中,必須包含該考試的所有應回答題目的對應作答,必須一次建立。提交與作答建立後不可修改也不可刪除,並且會在建立後立即排程,每筆作答同時進行非同步批閱。
一份提交的請求內容如下。responses 中每一筆對應考試裡的一道應回答題目,answer 的形態依題型而定(填充題為 fill_in,其餘為 text):
{
"exam_id": "exam_kQXzTR",
"student_ref": "student_0042",
"roster_id": "ros_7hK2Qn",
"custom_id": "midterm-0042",
"responses": [
{
"question_ref": "q_mN4pXe:1",
"answer": {
"type": "fill_in",
"entries": [
{
"blank_id": "mockBlank1leeu4igarik",
"doc": {
"type": "doc",
"content": [
{ "type": "paragraph", "content": [{ "type": "text", "text": "類囊體膜" }] }
]
}
}
]
}
},
{
"question_ref": "q_7g2NkP:2",
"answer": {
"type": "text",
"doc": {
"type": "doc",
"content": [
{ "type": "paragraph", "content": [{ "type": "text", "text": "光反應在類囊體膜產生 ATP 與 NADPH。" }] }
]
}
}
}
]
}建立提交的請求另需帶 Idempotency-Key 標頭,詳見冪等鍵。
提交的內容
一份提交中,其 responses 欄位必須完整包含該考試中,所有應回答的題目。每道應回答題目均應存在恰好一筆作答,同一道應答題目不得存在重複的作答。
應回答題目將包含:
- 一般題目:即題目本身。
- 題組:應回答題目為其末端子題。因此建立提交時,其
question_ref應指向該些末端子題的 ref(包含_sub_區段的版本化參照,例如q_sci_sub_mockSubQ1gk3vphrn2ecx_sub_mockSubQ2wd8j5tramx6f:1)。題組本身不是可作答對象,因此對其建立作答將會被視為無效參照。
應回答題目的數量,可查看讀取考試端點的 gradeable_count 參數。請注意 gradeable_count 與 question_count 參數不同,後者計算的是最上層的題目總數,因此題組將僅計為一題,兩者用途不同。
學生未作答的題目,仍需要送出一筆空白作答,不可省略。其寫法請參考作答與題幹的內容格式。
若送出了在考試中不存在的題目參照,或存在重複的參照,或遺漏應回答題目,都會使整個請求返回 400 ERR_VALIDATION_FAILED 錯誤,且回應的 details 會指出存在問題的參照。
學生識別
student_ref 與 roster_id 提供學生識別碼與學生名冊的參照。兩個欄位都是選填,且都不影響批閱。
student_ref 是客戶端系統中的學生識別碼,必要時將可引用此參照作為學生重複作答的查詢基礎。roster_id 指定一份學生名冊,可於未來提供 IDP 服務時,作為學生手寫姓名辨識名冊對照使用。
冪等鍵
使用 POST /submissions 建立提交時,必須在請求標頭中傳送自訂字串的 Idempotency-Key 冪等鍵,以確保重複的相同請求,得以被正確識別及去重。
冪等鍵的有效時間為 24 小時。在有效時間內,對於同鍵、同內容的請求,將重放相同的回應,不會建立第二筆提交;但若為同鍵、不同內容的請求,則會得到 409 ERR_DUPLICATE_TXID 錯誤。
ERR_DUPLICATE_TXID 錯誤不應視為請求成功。
若該請求意圖確實是新的操作,應產生新的冪等鍵;若該請求意圖是重送某個請求,則表示客戶端的兩次嘗試之間可能存在問題,
導致 payload 有所異動,請勿將 409 錯誤視為「已處理完成」而略過。
冪等鍵沒有格式的限制,任何能作為唯一性識別的字串皆可使用。但我們建議,比起使用常見的 UUID,使用結構化的字串可以更便於理解及除錯排查。例如:
sub-{student_ref}-{exam_id}-{yyyymmdd}送出後的回應
正常的提交請求回應應為 201,且內容中會顯示狀態為 pending,並將內嵌所有收到的作答。例如:
請求回應範例
{
"data": {
"id": "sub_9Xw2Lp",
"exam_id": "exam_kQXzTR",
"student_ref": "student_0042",
"custom_id": "midterm-0042",
"source": "direct",
"status": "pending",
"response_count": 2,
"responses": [
{
"id": "resp_2Fa8Qz",
"question_ref": "q_mN4pXe:1",
"status": "pending",
"eta": 1,
"…": "…"
},
{
"id": "resp_5Kc3Yv",
"question_ref": "q_7g2NkP:2",
"status": "pending",
"eta": 3,
"…": "…"
}
],
"eta": 3,
"created_at": "2026-03-02T09:30:00Z"
}
}回應中,eta 欄位為該份考卷提交(或該筆題目作答)的預估批閱剩餘分鐘數。該數值由當下系統負載、題目類型等資訊即時估算,且欄位只在批閱未完成時出現。該時間僅供估算參考,並非批閱完成的時間保證,也不盡然將會隨時間絕對遞減。
其中,整個考卷提交層級的 eta (data.eta)為未完成作答中(data.responses[].eta)的最大值。
提交的生命週期與錯誤處理
一份提交,將在其下的每一筆作答都到達終態時(completed 或 failed)標記其狀態為 completed。
個別題目作答批閱失敗時,考卷提交並不會標示為失敗。 舉例來說,若一筆作答批閱逾時,提交仍會 completed,但提交的 summary.failed 將為 1。因此判斷是否有作答批閱任務出錯時,應查看 summary.failed,而非提交的 status。
有個別題目作答批閱失敗時,整份考卷提交的 summary.total_score 將為 null ,表示「總分尚不可用」,不應使用已經評分的部分作答先行加總計算。請於 Studio 將失敗的作答重新排程進行重試。
若整份考卷提交遇到整體性的失敗,例如批閱從未派送成功,或提交本身的紀錄無法解析出可批閱的內容時,其提交狀態將轉換為 failed。在此狀況下,請重新建立一份新的提交。
讀取批閱進度
請使用 GET /submissions/{id} 以讀取 responses 的個別進度或狀態。每筆個別題目作答的 response 均會各自帶有獨立的 status 與 eta,批閱完成者另帶最新的批閱結果 evaluation。
當所有題目均達到終態,且整份提交的狀態為 completed 時,將整份提交讀取時另外會有 summary 的統計資料物件,以及 completed_at 的完成時間戳。
請注意 GET /submissions 為列表端點,回傳物件將僅有摘要,不含 responses。
正式環境中,請訂閱 submission.completed Webhook 而非使用輪詢。
若確需輪詢,亦請採用指數退避,避免使用固定的短間隔。
且批閱時間依題型而異,例如申論題或作文所需時間將遠長於填充題。應參考 eta 提供的時間設定指數退避的週期。
批次提交
若需要大量建立提交時,可以使用 POST /submissions/batch 端點。
請求本體為一個 submissions 陣列,每一筆的內容與單筆建立相同,另各自帶有自己的 idempotency_key:
{
"submissions": [
{
"idempotency_key": "midterm-0042-v1",
"exam_id": "exam_kQXzTR",
"student_ref": "student_0042",
"responses": [
{
"question_ref": "q_mN4pXe:1",
"answer": {
"type": "fill_in",
"entries": [
{
"blank_id": "mockBlank1leeu4igarik",
"doc": {
"type": "doc",
"content": [
{ "type": "paragraph", "content": [{ "type": "text", "text": "類囊體膜" }] }
]
}
}
]
}
},
{
"question_ref": "q_7g2NkP:2",
"answer": {
"type": "text",
"doc": {
"type": "doc",
"content": [
{ "type": "paragraph", "content": [{ "type": "text", "text": "光反應在類囊體膜產生 ATP 與 NADPH。" }] }
]
}
}
}
]
},
{
"idempotency_key": "midterm-0043-v1",
"exam_id": "exam_kQXzTR",
"student_ref": "student_0043",
"responses": ["…"]
}
]
}每則批次提交請求最多包含 100 筆提交,若超過將會得到 400 ERR_BATCH_TOO_LARGE 錯誤。
使用批次提交時,大部分的請求與回應均與單筆建立相同,但有兩處不同:
- 冪等鍵改置於個別提交中:批次提交的冪等鍵應置於每筆提交的
idempotency_key欄位,而非使用整個請求的Idempotency-Key標頭。當重送時,批次中每一筆提交的回放行為,則將與單筆建立時相同。 - 提交可以部分成功:就算批次中的提交可能有項目驗證或建立失敗,但批次請求本身仍將回應
200。驗證成功的提交項目將照常建立並排入批閱,因個別提交內容錯誤而導致拒絕的情況(例如:格式錯誤的作答、過長的作答文字、缺少應作答的題目)則將標示於請求回應中:
請求回應範例
{
"data": {
"batch_id": "batch_7Td3Mf",
"total": 2,
"succeeded": 1,
"failed": 1,
"results": [
{
"index": 0,
"idempotency_key": "midterm-0042-v1",
"status": "created",
"submission_id": "sub_9Xw2Lp"
},
{
"index": 1,
"idempotency_key": "midterm-0043-v1",
"status": "failed",
"error": {
"code": "ERR_VALIDATION_FAILED",
"message": "responses do not cover every exam item: missing 'q_7g2NkP:2'"
}
}
]
}
}其中,results 維持請求的順序,index 為該項目在請求陣列中的位置。
在批次提交中,回應 200 不代表全部成功。請務必逐筆檢查 results[].status。
整批遭到拒絕的情況僅可能有:
- 提交超過 100 筆
- 請求超過大小上限
- 請求結構本身無法逐筆處理(
submissions缺少或不是陣列,或某筆缺少idempotency_key)。
可重試的批次迴圈
以下的程式範例,為一個簡單的自動重試函式,將批次建立提交,並自動重試直至全部成功建立(或重試次數用盡)。
async function submitBatch(items: SubmissionInput[]) {
const pending = new Map(items.map((i) => [i.idempotency_key, i]));
for (let attempt = 0; attempt < 3 && pending.size > 0; attempt++) {
const res = await fetch(`${API}/submissions/batch`, {
method: 'POST',
headers: {
Authorization: `Bearer ${KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ submissions: [...pending.values()] }),
});
const { data } = await res.json();
for (const r of data.results) {
if (r.status === 'created') {
pending.delete(r.idempotency_key);
} else if (!isRetryable(r.error.code)) {
// 不可重試的錯誤:記錄下來交由人工處理
report(r);
pending.delete(r.idempotency_key);
}
}
if (pending.size > 0) await sleep(2 ** attempt * 1000);
}
return [...pending.values()]; // 重試次數用盡仍未成功的項目
}由於冪等鍵是逐筆的,整批重送對已成功的項目是安全的,它們會被重播而不會重複建立。
相關
- API 參考 — 提交
- 作答與題幹的內容格式 —
answer的寫法 - 取得批閱結果
- 錯誤處理策略