Endpoints 14
ทุกตารางสร้างจาก JSON Schema (schemas/*.schema.json) · ช่องที่เป็น object/array เปิดดูช่องย่อยได้ ·
"ข้อ N" ในคำอธิบาย = ข้อใน PDF สเปกเต็ม
ขอ token ของเครื่อง
ส่งจากหน้า SC-01
token ไม่ต้องมี
กล่องไม่มีเน็ต ใช้ token เดิมต่อ ขอใหม่เมื่อออนไลน์
ระบบกลางต้องทำ JWT อายุ 1 ชม. (expires_in = 3600) · ผูก device_id ไว้ใน token เพื่อเทียบกับ device_id ในข้อมูล (ไม่ตรง → 403 device_mismatch) ทะเบียนเครื่อง PB-01 … PB-16 + ชื่อสถานที่ · api_key สุ่ม 32 ไบต์ base64url ต่อเครื่อง เก็บแค่ hash · ระหว่างหมุนคีย์ 1 เครื่องใช้ได้ 2 คีย์
ช่อง ชนิด ต้องมี ความหมาย device_idstring ^[A-Z0-9-]{3,32}$ ✓ รหัสกล่อง PB-01 … PB-16 (ระบบกลางลงทะเบียนพร้อมชื่อสถานที่ตั้ง ข้อ 8) · ต้องตรงกับ token api_keystring ✓ กุญแจของเครื่อง (เก็บใน key store ที่เข้ารหัส)
คำตอบ 200 ได้ token
ช่อง ชนิด ต้องมี ความหมาย access_tokenstring ✓ JWT token_type"Bearer"✓ ชนิด expires_ininteger ✓ อายุ (วินาที)
ตัวอย่างคำตอบ 5 บรรทัด คัดลอก
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.…",
"token_type": "Bearer",
"expires_in": 3600
}
ข้อผิดพลาดเฉพาะ endpoint 401 device_id/api_key ไม่ถูกต้อง
ตอบได้ทุก endpoint: 429 · 500 · 503
ลองส่ง endpoint นี้
POST /v1/device/heartbeat200
แจ้งสถานะเครื่อง (ใช้ในหน้า Dashboard เครื่อง) — ต้องมี: ข้อมูลหน้า Dashboard เครื่อง (ข้อ 7) · ส่งตอนเปิดเครื่องและทุก 15 นาที
ส่งจากหน้า SC-01 · ทุก 15 นาที
token Authorization: Bearer …
กล่องไม่มีเน็ต ข้าม
ระบบกลางต้องทำ ตอบ server_time — กล่องใช้เช็คนาฬิกาเพี้ยน · ข้อมูลใช้ในหน้า Dashboard เครื่อง (PDF ข้อ 7) devices.last_seen_at = เวลาที่ระบบกลางรับคำขอล่าสุดที่ใช้ token ของเครื่อง (ไม่ใช่ heartbeat.at)
ช่อง ชนิด ต้องมี ความหมาย device_idstring ^[A-Z0-9-]{3,32}$ ✓ รหัสกล่อง PB-01 … PB-16 (ระบบกลางลงทะเบียนพร้อมชื่อสถานที่ตั้ง ข้อ 8) · ต้องตรงกับ token atเวลา ISO 8601 ✓ เวลาที่กล่องส่ง app_versionstring ✓ รุ่นซอฟต์แวร์ pose_configstring ✓ รุ่นเกณฑ์ท่าที่ใช้อยู่ = session.versions.pose_config (Dashboard หน้า ⑥ คอลัมน์ "รุ่นเกณฑ์") config_versionstring รุ่นไฟล์ค่าตั้งเครื่อง (ดึงจากเซิร์ฟเวอร์ทีม Play Box) — ไม่ใช่รุ่นเกณฑ์ cameraok · not_found✓ สถานะกล้อง pendingobject ✓ ข้อมูลรอส่งแยกตามประเภท เช่น {"session": 2, "event": 1} heldinteger รายการในกองรอคนตรวจ (ส่งไม่ผ่านเพราะข้อมูลผิด) last_sync_atเวลา ISO 8601 หรือ null ส่งสำเร็จครั้งล่าสุด profilesinteger จำนวนโปรไฟล์ในเครื่อง disk_free_gbnumber พื้นที่ว่าง
ตัวอย่าง — สถานะเครื่อง heartbeat_request.json 16 บรรทัด ลองส่ง คัดลอก
{
"device_id": "PB-07",
"at": "2026-10-07T09:00:05+07:00",
"app_version": "0.3.0",
"pose_config": "2026.10.9",
"config_version": "device-2026.10.9-1",
"camera": "ok",
"pending": {
"session": 0,
"event": 0
},
"held": 0,
"last_sync_at": "2026-10-06T17:42:10+07:00",
"profiles": 3,
"disk_free_gb": 182.4
}
คำตอบ 200 รับแล้ว
ช่อง ชนิด ต้องมี ความหมาย server_timeเวลา ISO 8601 ✓ เวลาของระบบกลาง (กล่องใช้เช็คนาฬิกาเพี้ยน)
ตัวอย่างคำตอบ 3 บรรทัด คัดลอก
{
"server_time": "2026-10-07T09:00:06+07:00"
}
ตอบได้ทุก endpointที่มี token: 400 invalid_field · 401 token_invalid · 429 rate_limited · 500 server_error · 503 unavailable
ลองส่ง endpoint นี้
Pairing (QR) — สิ่งที่ระบบกลางต้องตัดสิน
QR จากแอป PBLINK:1: + โทเคน 22 ตัว [A-Za-z0-9_-] = สุ่ม 16 ไบต์ (CSPRNG) base64url ไม่มี padding · regex ^PBLINK:1:[A-Za-z0-9_-]{22}$ · โทเคนไม่มีความหมาย ไม่ต้องเซ็นเก็บแค่ SHA-256 ของโทเคน · อายุ 15 นาที · บัญชีละ ≤ 3 ตัวที่ยังไม่หมดอายุ (ออกตัวที่ 4 ยกเลิกตัวเก่าสุด) · ออกผ่าน API ภายในของแอป รูปแบบ app_qr ใช้ได้ครั้งเดียว: หมดเมื่อ link สำเร็จ · verify ไม่ทำให้หมด
สถานะ link link_id = lk_ + สุ่ม 16 ไบต์ base64url (22 ตัว) — กล่องเก็บแค่ค่านี้ตอน link: participant_id มีในระบบแล้ว (เชื่อมทีหลัง) → active ทันที · ยังไม่มี (ผู้ใช้ใหม่) → pending แล้วเป็น active เมื่อ participant มาถึง pending ไม่มี participant ใน 24 ชม. → expired (no_participant_24h) · participant มาช้าแต่ created_at < linked_at + 24 ชม. → กลับเป็น active (เว้นแต่บัญชีมี link pending/active อื่นในกล่องนี้แล้ว)จบด้วย end_reason: cancelled_before_profile · admin_unlink · app_user · withdraw · no_participant_24h · superseded · account_deleted
กฎที่ต้องจริงเสมอ participant_id หนึ่งมี link pending/active ได้ไม่เกิน 1บัญชีหนึ่งมี link active ต่อกล่องได้ไม่เกิน 1 (กล่องเดียวกัน 1 บัญชี : 1 ผู้เข้าร่วม) บัญชีหนึ่งมี link pending ต่อกล่องได้ไม่เกิน 1 — link ใหม่แทนตัวเก่า (superseded) จึงไม่มี link ค้างมาขวางการสแกนใหม่
ตารางตัดสิน verify / link ตรวจตามลำดับ หยุดที่ข้อแรกที่เข้า · "บัญชี" = เจ้าของโทเคน · "กล่องนี้" = device_id ใน token
ลำดับ เงื่อนไข verify link 1 ไม่พบ SHA-256 ของโทเคน 422 qr_not_recognized 422 qr_not_recognized 2 (link เท่านั้น) บัญชี + กล่องนี้ + participant_id นี้ มี link pending/active อยู่แล้ว = คำขอซ้ำ — 201 + link_id และ linked_at เดิม · ไม่แตะโทเคน 3 โทเคนใช้แล้ว หรือเลย expires_at 410 qr_expired 410 qr_expired 4 (link เท่านั้น) participant_id นี้ถอนความยินยอมแล้ว — 403 participant_withdrawn 5 new_user และบัญชีมี link active ในกล่องนี้409 account_has_profile_here 409 account_has_profile_here 6 link_later และบัญชีมี link active ในกล่องนี้ (verify ไม่รู้ว่าผู้ใช้คนไหนจึงดูทุกคน · link เป็นคนอื่นแน่นอน เพราะคนเดิมเข้าข้อ 2 ไปแล้ว)409 account_linked_elsewhere 409 account_linked_elsewhere 7 ผ่านทั้งหมด 200 + expires_at + expires_in_sec (ไม่บันทึกอะไร) 201: ① จบ link pending ของบัญชีนี้ในกล่องนี้ (superseded) ② จบ link pending/active ของ participant_id นี้ที่เป็นของบัญชีอื่น (superseded) ③ สร้าง link ใหม่ (active ถ้ามี participant แล้ว ไม่งั้น pending) ④ ใส่ used_at ให้โทเคน
link pending ไม่ขวาง การเชื่อม (ข้อ 5–6 ดูเฉพาะ active): คำตอบ link หายแล้วผู้ใช้สแกนใหม่ จึงไม่ติด 409 ค้าง 24 ชม. verify ห้าม ทำให้โทเคนใช้แล้ว และห้ามเก็บความเชื่อมโยงบัญชี↔เครื่อง (ผู้ใช้ยังไม่ยินยอม) จำกัดความถี่ต่อเครื่อง: verify ≤ 30 ครั้ง/นาที · link ≤ 10 ครั้ง/นาที (เกิน → 429)
กรณีที่ต้องทดสอบก่อนต่อจริง QR ปกติ · QR หมดอายุ · QR ใช้แล้ว · QR ไม่ใช่ของแอป · link ซ้ำ (คำตอบหาย) · คำตอบหายแล้วสแกน QR ใหม่ (บัญชีเดิม → link เดิม · บัญชีอื่น → ตัวเก่า superseded) · บัญชีมีโปรไฟล์ในกล่องนี้แล้ว · เชื่อมทีหลังกับบัญชีที่เชื่อมคนอื่นอยู่ · ไม่มี participant ใน 24 ชม. · participant มาช้าเพราะกล่องออฟไลน์ (กลับเป็น active) · ยกเลิกจากแอปแล้วกล่องส่ง unlink · unlink link_id ที่ไม่รู้จัก · link ของคนที่ถอนความยินยอมแล้ว
POST /v1/pairing/verify200 ต้องออนไลน์
ตรวจ QR จากแอป — เรียกทันทีที่กล้องอ่าน QR ได้ (SC-18) · แค่ถามว่า QR ใช้ได้ไหม: ห้ามทำให้ QR ใช้ไปแล้ว และห้ามเก็บความเชื่อมโยงบัญชี↔เครื่องจากขั้นนี้ (ผู้ใช้ยังไม่ยินยอม)
ส่งจากหน้า SC-18
token Authorization: Bearer …
กล่องไม่มีเน็ต ใช้ไม่ได้ — ต้องออนไลน์ตอนนั้น
ระบบกลางต้องทำ ตัดสินตามตาราง pairing คอลัมน์ verify · ห้ามทำให้ QR ใช้แล้ว และห้ามบันทึกความเชื่อมโยงบัญชี↔เครื่อง จำกัด ≤ 30 ครั้ง/นาที/เครื่อง (เกิน → 429)
ช่อง ชนิด ต้องมี ความหมาย qr_payloadstring ✓ ข้อความทั้งหมดที่อ่านได้จาก QR ในแอป (ไม่ตัด ไม่แปลง) · รูปแบบ PBLINK:1:<โทเคน 22 ตัว> (ข้อ 4.2) — ไม่ตรงรูปแบบ กล่องไม่เรียก verify modenew_user · link_later✓ new_user = เพิ่มผู้ใช้ใหม่ · link_later = เชื่อมผู้ใช้เดิมจาก SC-21 (ไม่ส่งว่าเป็นผู้ใช้คนไหน)
ตัวอย่าง — ตรวจ QR — คำขอ pairing_verify_request.json 4 บรรทัด ลองส่ง คัดลอก
{
"qr_payload": "PBLINK:1:q7Xc2v9Lq0Rk7mT1sPa4Zw",
"mode": "new_user"
}
คำตอบ 200 QR ใช้ได้
ช่อง ชนิด ต้องมี ความหมาย status"valid"✓ QR ใช้ได้ expires_atเวลา ISO 8601 ✓ QR หมดอายุเมื่อไร (เวลาระบบกลาง) expires_in_secinteger ✓ อีกกี่วินาทีหมดอายุ — กล่องใช้ค่านี้ เพราะนาฬิกากล่องที่ออฟไลน์นานอาจเพี้ยน
ตัวอย่างคำตอบ pairing_verify_response.json 5 บรรทัด คัดลอก
{
"status": "valid",
"expires_at": "2026-10-07T09:25:00+07:00",
"expires_in_sec": 820
}
ข้อผิดพลาดเฉพาะ endpoint
ตอบได้ทุก endpointที่มี token: 400 invalid_field · 401 token_invalid · 429 rate_limited · 500 server_error · 503 unavailable
ลองส่ง endpoint นี้
POST /v1/pairing/link201 ต้องออนไลน์
เชื่อมบัญชีแอปกับผู้เข้าร่วม — เรียกตอนกด [ยินยอม] (SC-03) · ส่งซ้ำด้วย participant_id เดิม (QR เดิมหรือ QR ใหม่ของบัญชีเดิม) ต้องได้ 201 และ link_id เดิม (คำตอบหายระหว่างทาง) · ระบบกลางรับได้แม้ยังไม่เคยเห็น participant นี้ (link เป็น pending จนกว่า participant มาถึง) · pending ที่ไม่มี participant ตามมาภายใน 24 ชม. ให้ระบบกลางยกเลิกเอง (ข้อ 4.4)
ส่งจากหน้า SC-03 [ยินยอม]
token Authorization: Bearer …
กล่องไม่มีเน็ต ใช้ไม่ได้ — ต้องออนไลน์ตอนนั้น
ระบบกลางต้องทำ ตัดสินตามตาราง pairing คอลัมน์ link · ส่งซ้ำด้วย participant_id เดิม + บัญชีเดิม → 201 และ link_id เดิม participant_id / consent_id เป็นรหัสจองล่วงหน้า — participant / consent ที่ตามมาด้วยรหัสนี้ตอบ 201 ตามปกติจำกัด ≤ 10 ครั้ง/นาที/เครื่อง (เกิน → 429)
ช่อง ชนิด ต้องมี ความหมาย qr_payloadstring ✓ QR ตัวเดิมที่ผ่าน verify modenew_user · link_later✓ เหมือนตอน verify participant_idUUID ✓ รหัสผู้เข้าร่วม (UUID v4) กล่องสุ่มตอนกด [ยินยอม] (SC-03) · ไม่มีชื่อหรือข้อมูลที่ระบุตัวบุคคล consent_idUUID ✓ รายการความยินยอมที่มี scope app (ส่งตามมาใน /v1/consent) · participant_id และ consent_id เป็นรหัสล่วงหน้า: participant / consent ที่ส่งตามมาด้วยรหัสนี้ บันทึกตามปกติ (201) ไม่ใช่ 409
ตัวอย่าง — เชื่อมบัญชี — คำขอ pairing_link_request.json 6 บรรทัด ลองส่ง คัดลอก
{
"qr_payload": "PBLINK:1:q7Xc2v9Lq0Rk7mT1sPa4Zw",
"mode": "new_user",
"participant_id": "8e80639a-270d-49da-ad3c-88c4cf8491d8",
"consent_id": "e48f2be7-c617-4583-87e7-9626be62c4c2"
}
คำตอบ 201 เชื่อมแล้ว · ส่งซ้ำด้วย participant เดิม (บัญชีเดิม) ได้ link_id เดิม
ช่อง ชนิด ต้องมี ความหมาย link_idstring ^lk_[A-Za-z0-9_-]{22}$ ✓ รหัสเชื่อมโยงแบบสุ่ม lk_ + 22 ตัว — กล่องเก็บแค่ค่านี้ ไม่เก็บชื่อหรือข้อมูลใดจากแอป linked_atเวลา ISO 8601 ✓ เวลาที่ระบบกลางสร้าง link (เวลาระบบกลาง · แสดงในแอป) — ช่วงที่ส่งผลเข้าแอปเริ่มจาก given_at ของ consent ไม่ใช่ค่านี้ (ข้อ 5.1)
ตัวอย่างคำตอบ pairing_link_response.json 4 บรรทัด คัดลอก
{
"link_id": "lk_9Qe2v7XcR4mT1sPa0bNw3K",
"linked_at": "2026-10-07T09:12:41+07:00"
}
ข้อผิดพลาดเฉพาะ endpoint
ตอบได้ทุก endpointที่มี token: 400 invalid_field · 401 token_invalid · 429 rate_limited · 500 server_error · 503 unavailable
ลองส่ง endpoint นี้
POST /v1/pairing/unlink201
ยกเลิกการเชื่อม — ข้อมูลที่ส่งไปแอปแล้วยังอยู่ในแอป · บัญชีแอปนั้นกลับมาเชื่อมใหม่ได้ · งานวิจัยยังส่งตาม scope research · admin_unlink จบทุก link pending/active ของ (กล่องนี้, participant_id) ไม่ใช่แค่ link_id ที่ส่งมา · กรณีพิเศษดูข้อ 10.3
ส่งจากหน้า SC-19 ยกเลิก · SC-21
token Authorization: Bearer …
กล่องไม่มีเน็ต ยกเลิกตอนตั้งชื่อ: ส่งทันที ไม่ได้ค่อยเข้าคิว · ผู้ดูแล: เข้าคิว
ระบบกลางต้องทำ จบทุก link pending/active ของ (กล่องนี้, participant_id) — ไม่ใช่แค่ link_id ที่ส่งมา · until_at = at link_id ไม่รู้จัก → 201 ไม่ทำอะไร (บันทึก log) · link เป็นของกล่องอื่น → 403 device_mismatch · participant_id ไม่ตรงกับ link → 400 invalid_field (field = participant_id)link จบไปแล้ว (เช่นผู้ใช้ยกเลิกจากแอป) → 201 · คง end_reason แรกไว้ · until_at = ค่าที่เร็วกว่า participant ถอนความยินยอมแล้ว → 201 (ไม่มีอะไรต้องทำ)
ช่อง ชนิด ต้องมี ความหมาย schema_versionstring ^0\.\d+$ ✓ รุ่นของโครงข้อมูล ตอนนี้ "0.3" · รุ่นย่อยที่ใหม่กว่าอาจมีช่องเพิ่ม ระบบกลางให้ข้ามช่องที่ไม่รู้จัก request_idUUID ✓ รหัสคำขอ (UUID) สร้างที่กล่อง กันส่งซ้ำ link_idstring ^lk_[A-Za-z0-9_-]{22}$ ✓ รหัสเชื่อมโยงที่ได้ตอน link participant_idUUID ✓ รหัสผู้เข้าร่วม (UUID v4) กล่องสุ่มตอนกด [ยินยอม] (SC-03) · ไม่มีชื่อหรือข้อมูลที่ระบุตัวบุคคล device_idstring ^[A-Z0-9-]{3,32}$ ✓ รหัสกล่อง PB-01 … PB-16 (ระบบกลางลงทะเบียนพร้อมชื่อสถานที่ตั้ง ข้อ 8) · ต้องตรงกับ token reasoncancelled_before_profile · admin_unlink✓ cancelled_before_profile = กดยกเลิกตอนตั้งชื่อ (SC-19) จึงไม่สร้างโปรไฟล์ · admin_unlink = ผู้ดูแลกดยกเลิกการเชื่อมแอป (SC-21) atเวลา ISO 8601 ✓ เวลาที่กด — ข้อมูลที่เกิดหลังเวลานี้ไม่ส่งต่อไปแอป
ตัวอย่าง — ยกเลิกการเชื่อมแอป pairing_unlink.json 9 บรรทัด ลองส่ง คัดลอก
{
"schema_version": "0.3",
"request_id": "5b04f2b7-8773-4974-9b36-22959361ac47",
"link_id": "lk_3Hn8wQ2pZt6Kd0Vbr5Ty1M",
"participant_id": "1c7e4b2a-93d5-4f60-8a1e-5b9c0d2e7f34",
"device_id": "PB-07",
"reason": "admin_unlink",
"at": "2026-11-02T14:05:10+07:00"
}
คำตอบ 201 บันทึกแล้ว · link_id ที่ไม่รู้จัก / link ที่จบแล้ว / participant ที่ถอนแล้ว ก็ตอบ 201 (ข้อ 10.3)
รูปแบบ created: id = รหัสที่ส่งมา · received_at = เวลาที่ระบบกลางรับ
ตัวอย่างคำตอบ 4 บรรทัด คัดลอก
{
"id": "5b04f2b7-8773-4974-9b36-22959361ac47",
"received_at": "2026-11-02T14:05:14+07:00"
}
ข้อผิดพลาดเฉพาะ endpoint
ตอบได้ทุก endpointที่มี token: 400 invalid_field · 401 token_invalid · 429 rate_limited · 500 server_error · 503 unavailable
ลองส่ง endpoint นี้
ผู้เข้าร่วมใหม่ — ส่งหลังตั้งชื่อโปรไฟล์เสร็จ (SC-19) · ชื่อเรียกและรูป avatar อยู่ในกล่องเท่านั้น ไม่ส่ง
ส่งจากหน้า SC-19 [เสร็จ]
token Authorization: Bearer …
กล่องไม่มีเน็ต เข้าคิว ส่งเมื่อออนไลน์
ระบบกลางต้องทำ บันทึกแล้วเปลี่ยน link pending ของ participant_id นี้เป็น active (+ แจ้งแอป) · เก็บช่วงอายุ 5 ปี + เพศจากบัญชีแอปครั้งเดียวตอนนี้ participant_code ซ้ำกับ participant_id อื่น → 409 participant_code_taken (กล่องเลื่อนเลขแล้วส่งใหม่)
ช่อง ชนิด ต้องมี ความหมาย schema_versionstring ^0\.\d+$ ✓ รุ่นของโครงข้อมูล ตอนนี้ "0.3" · รุ่นย่อยที่ใหม่กว่าอาจมีช่องเพิ่ม ระบบกลางให้ข้ามช่องที่ไม่รู้จัก participant_idUUID ✓ รหัสผู้เข้าร่วม (UUID v4) กล่องสุ่มตอนกด [ยินยอม] (SC-03) · ไม่มีชื่อหรือข้อมูลที่ระบุตัวบุคคล device_idstring ^[A-Z0-9-]{3,32}$ ✓ รหัสกล่อง PB-01 … PB-16 (ระบบกลางลงทะเบียนพร้อมชื่อสถานที่ตั้ง ข้อ 8) · ต้องตรงกับ token participant_codestring ^[A-Z0-9]{2,10}-\d{4}$ ✓ รหัสสั้นสำหรับคนอ่าน (ทีมวิจัย) = รหัสกล่องไม่มีขีด + ลำดับในกล่อง เช่น PB07-0012 · กล่องออกเองตอนออฟไลน์ได้ ไม่ชนข้ามเครื่อง · ผู้ดูแลเห็นในเมนู SC-21 · ไม่ใช้แทน participant_id ในการเชื่อมข้อมูล · ระบบกลางห้ามรับรหัสที่ participant_id อื่นใช้แล้ว (409 participant_code_taken ) enrollmentapp · device_only✓ app = เข้าผ่านแอปหมอไทยใกล้บ้าน (ส่งงานวิจัย + บัญชีแอป) · device_only = ใช้เฉพาะเครื่องนี้ (ส่งงานวิจัยเท่านั้น) created_atเวลา ISO 8601 ✓ เวลาสร้างโปรไฟล์ (กด [เสร็จ] ที่ SC-19) app_versionstring รุ่นซอฟต์แวร์ในกล่อง
ตัวอย่าง — ผู้เข้าร่วมใหม่ participant.json 9 บรรทัด ลองส่ง คัดลอก
{
"schema_version": "0.3",
"participant_id": "8e80639a-270d-49da-ad3c-88c4cf8491d8",
"participant_code": "PB07-0012",
"device_id": "PB-07",
"enrollment": "app",
"created_at": "2026-10-07T09:14:10+07:00",
"app_version": "0.3.0"
}
คำตอบ 201 บันทึกแล้ว
รูปแบบ created: id = รหัสที่ส่งมา · received_at = เวลาที่ระบบกลางรับ
ตัวอย่างคำตอบ 4 บรรทัด คัดลอก
{
"id": "8e80639a-270d-49da-ad3c-88c4cf8491d8",
"received_at": "2026-10-07T09:14:14+07:00"
}
ข้อผิดพลาดเฉพาะ endpoint
ตอบได้ทุก endpointที่มี token: 400 invalid_field · 401 token_invalid · 429 rate_limited · 500 server_error · 503 unavailable
ลองส่ง endpoint นี้
ความยินยอม — ผู้ใช้ใหม่: ส่งต่อจาก participant · เชื่อมแอปทีหลัง: เข้าคิวหลัง link ได้ 201 เท่านั้น · กด [ไม่ยินยอม] = ไม่ส่งอะไรเลย
ส่งจากหน้า SC-19 / SC-21
token Authorization: Bearer …
กล่องไม่มีเน็ต เข้าคิว ส่งเมื่อออนไลน์
ระบบกลางต้องทำ given_at ของ consent ที่ผูกกับ link = from_at จุดเริ่มส่งผลเข้าแอป (นาฬิกากล่อง)เชื่อมทีหลัง: consent scopes: ["app"] + context: "link_later" มาหลัง link ได้ 201 เท่านั้น
ช่อง ชนิด ต้องมี ความหมาย schema_versionstring ^0\.\d+$ ✓ รุ่นของโครงข้อมูล ตอนนี้ "0.3" · รุ่นย่อยที่ใหม่กว่าอาจมีช่องเพิ่ม ระบบกลางให้ข้ามช่องที่ไม่รู้จัก consent_idUUID ✓ รหัสรายการ (UUID) สร้างที่กล่อง participant_idUUID ✓ รหัสผู้เข้าร่วม (UUID v4) กล่องสุ่มตอนกด [ยินยอม] (SC-03) · ไม่มีชื่อหรือข้อมูลที่ระบุตัวบุคคล device_idstring ^[A-Z0-9-]{3,32}$ ✓ รหัสกล่อง PB-01 … PB-16 (ระบบกลางลงทะเบียนพร้อมชื่อสถานที่ตั้ง ข้อ 8) · ต้องตรงกับ token scopesarray ของ research · app ✓ ขอบเขตที่ยินยอม · research = ส่งเข้าระบบงานวิจัย · app = ส่งผลไปบัญชีแอปด้วย (ดูตาราง "ส่งอะไรไปแอป") notice_versionstring ✓ รุ่นของประกาศความยินยอมที่ผู้ใช้อ่าน (รหัสรุ่นตามเอกสาร IRB) contextnew_user · link_later✓ new_user = ตอนเพิ่มผู้ใช้ใหม่ · link_later = เชื่อมแอปทีหลังจากเมนูผู้ดูแล (SC-21) ประกาศเฉพาะส่วนแอป given_atเวลา ISO 8601 ✓ เวลากด [ยินยอม] (หลังอ่านครบทุกหน้า)
ตัวอย่าง — ความยินยอม (ผู้ใช้ใหม่ เชื่อมแอป) consent.json 13 บรรทัด ลองส่ง คัดลอก
{
"schema_version": "0.3",
"consent_id": "e48f2be7-c617-4583-87e7-9626be62c4c2",
"participant_id": "8e80639a-270d-49da-ad3c-88c4cf8491d8",
"device_id": "PB-07",
"scopes": [
"research",
"app"
],
"notice_version": "PB-CONSENT-20261001",
"context": "new_user",
"given_at": "2026-10-07T09:12:40+07:00"
}
ตัวอย่าง — ความยินยอม (เชื่อมแอปทีหลัง) consent_link_later.json 12 บรรทัด ลองส่ง คัดลอก
{
"schema_version": "0.3",
"consent_id": "4d2a9e71-0b6c-4f8e-9a35-c71e6b0d2f58",
"participant_id": "1c7e4b2a-93d5-4f60-8a1e-5b9c0d2e7f34",
"device_id": "PB-07",
"scopes": [
"app"
],
"notice_version": "PB-CONSENT-APP-20261001",
"context": "link_later",
"given_at": "2026-10-20T10:02:15+07:00"
}
คำตอบ 201 บันทึกแล้ว
รูปแบบ created: id = รหัสที่ส่งมา · received_at = เวลาที่ระบบกลางรับ
ตัวอย่างคำตอบ 4 บรรทัด คัดลอก
{
"id": "e48f2be7-c617-4583-87e7-9626be62c4c2",
"received_at": "2026-10-07T09:12:44+07:00"
}
ข้อผิดพลาดเฉพาะ endpoint
ตอบได้ทุก endpointที่มี token: 400 invalid_field · 401 token_invalid · 429 rate_limited · 500 server_error · 503 unavailable
ลองส่ง endpoint นี้
ผลคัดกรองแบบเต็ม
ส่งจากหน้า SC-04
token Authorization: Bearer …
กล่องไม่มีเน็ต เข้าคิว ส่งเมื่อออนไลน์
ระบบกลางต้องทำ งานวิจัยเท่านั้น ไม่ขึ้นแอป · ระบบกลางไม่ต้องรู้ถ้อยคำคำถาม (question_id + questionnaire_version)
ช่อง ชนิด ต้องมี ความหมาย schema_versionstring ^0\.\d+$ ✓ รุ่นของโครงข้อมูล ตอนนี้ "0.3" · รุ่นย่อยที่ใหม่กว่าอาจมีช่องเพิ่ม ระบบกลางให้ข้ามช่องที่ไม่รู้จัก screening_idUUID ✓ รหัสรายการ (UUID) participant_idUUID ✓ รหัสผู้เข้าร่วม (UUID v4) กล่องสุ่มตอนกด [ยินยอม] (SC-03) · ไม่มีชื่อหรือข้อมูลที่ระบุตัวบุคคล device_idstring ^[A-Z0-9-]{3,32}$ ✓ รหัสกล่อง PB-01 … PB-16 (ระบบกลางลงทะเบียนพร้อมชื่อสถานที่ตั้ง ข้อ 8) · ต้องตรงกับ token kind"full"✓ คัดกรองสุขภาพแบบเต็ม (SC-04) · คัดกรองสั้นก่อนฝึกอยู่ใน session.daily_check roundinitial · review✓ initial = ครั้งแรก · review = รอบทบทวนทุก 30 วัน answered_byself · caregiver✓ ผู้ตอบ (เลือกที่ SC-23) self = ผู้ใช้ตอบเอง · caregiver = ผู้ดูแลตอบให้ questionnaire_versionstring ✓ รุ่นชุดคำถาม (คำถามและข้อสัญญาณอันตรายยังรอผู้เชี่ยวชาญ) started_atเวลา ISO 8601 เวลาเริ่มข้อแรก completed_atเวลา ISO 8601 ✓ เวลากด [ถูกต้อง] ในหน้าทบทวนคำตอบ answersarray ของ object ✓ คำตอบทุกข้อ (บังคับตอบ ไม่มีข้าม) ช่องใน answers[] · 2 ช่อง ช่อง ชนิด ต้องมี ความหมาย question_idstring ✓ รหัสคำถามในชุดคำถาม answeryes · no · unsure✓ มี / ไม่มี / ไม่แน่ใจ (ไม่แน่ใจ นับเป็นความเสี่ยง)
outcomepass · restricted · danger_stop✓ pass = ฝึกได้ทุกโปรแกรม · restricted = ตัดท่า/โปรแกรมบางส่วน · danger_stop = ตอบ มี/ไม่แน่ใจ ในข้อสัญญาณอันตราย → SC-08 (ส่ง event ด้วย) excluded_posesarray ของ string ^P\d-\d$ ท่าที่ถูกตัดตามตารางผู้เชี่ยวชาญ excluded_programsarray ของ string ^PG-\d+$ โปรแกรมที่ถูกซ่อน recalibration_requiredboolean รอบทบทวนแล้วข้อจำกัดเปลี่ยน → ต้องปรับเทียบใหม่ครั้งถัดไป
ตัวอย่าง — คัดกรองแบบเต็ม screening.json 57 บรรทัด ลองส่ง คัดลอก
{
"schema_version": "0.3",
"screening_id": "6e731ce7-512a-4d91-af0f-6d0825f3618b",
"participant_id": "8e80639a-270d-49da-ad3c-88c4cf8491d8",
"device_id": "PB-07",
"kind": "full",
"round": "initial",
"answered_by": "caregiver",
"questionnaire_version": "SCR-FULL-0.1",
"started_at": "2026-10-07T09:14:30+07:00",
"completed_at": "2026-10-07T09:17:50+07:00",
"answers": [
{
"question_id": "Q01",
"answer": "no"
},
{
"question_id": "Q02",
"answer": "no"
},
{
"question_id": "Q03",
"answer": "yes"
},
{
"question_id": "Q04",
"answer": "no"
},
{
"question_id": "Q05",
"answer": "unsure"
},
{
"question_id": "Q06",
"answer": "no"
},
{
"question_id": "Q07",
"answer": "no"
},
{
"question_id": "Q08",
"answer": "no"
}
],
"outcome": "restricted",
"excluded_poses": [
"P3-1",
"P3-2",
"P3-3"
],
"excluded_programs": [
"PG-3",
"PG-4"
],
"recalibration_required": false
}
คำตอบ 201 บันทึกแล้ว
รูปแบบ created: id = รหัสที่ส่งมา · received_at = เวลาที่ระบบกลางรับ
ตัวอย่างคำตอบ 4 บรรทัด คัดลอก
{
"id": "6e731ce7-512a-4d91-af0f-6d0825f3618b",
"received_at": "2026-10-07T09:17:54+07:00"
}
ข้อผิดพลาดเฉพาะ endpoint
ตอบได้ทุก endpointที่มี token: 400 invalid_field · 401 token_invalid · 429 rate_limited · 500 server_error · 503 unavailable
ลองส่ง endpoint นี้
ผลปรับเทียบ
ส่งจากหน้า SC-05
token Authorization: Bearer …
กล่องไม่มีเน็ต เข้าคิว ส่งเมื่อออนไลน์
ระบบกลางต้องทำ งานวิจัยเท่านั้น ไม่ขึ้นแอป · values เป็นสัดส่วนร่างกาย (ชื่อค่าเป็นร่าง)
ช่อง ชนิด ต้องมี ความหมาย schema_versionstring ^0\.\d+$ ✓ รุ่นของโครงข้อมูล ตอนนี้ "0.3" · รุ่นย่อยที่ใหม่กว่าอาจมีช่องเพิ่ม ระบบกลางให้ข้ามช่องที่ไม่รู้จัก calibration_idUUID ✓ รหัสรายการ (UUID) participant_idUUID ✓ รหัสผู้เข้าร่วม (UUID v4) กล่องสุ่มตอนกด [ยินยอม] (SC-03) · ไม่มีชื่อหรือข้อมูลที่ระบุตัวบุคคล device_idstring ^[A-Z0-9-]{3,32}$ ✓ รหัสกล่อง PB-01 … PB-16 (ระบบกลางลงทะเบียนพร้อมชื่อสถานที่ตั้ง ข้อ 8) · ต้องตรงกับ token triggerfirst_training · screening_changed · admin_menu✓ first_training = ฝึกครั้งแรก · screening_changed = ผลคัดกรองรอบทบทวนเปลี่ยน · admin_menu = [ปรับเทียบใหม่] ที่ SC-21 statussuccess · cancelled · default_values✓ success = ครบทุกขั้น · cancelled = กดย้อนกลับออกจากขั้นแรก · default_values = ผู้ดูแลเลือกใช้ค่ากลางที่ SC-21 (รอยืนยันใน flow) · ไม่สำเร็จ 3 ครั้ง กล่องเริ่มขั้นนั้นใหม่เอง ไม่ใช่สถานะจบ started_atเวลา ISO 8601 เวลาเริ่ม completed_atเวลา ISO 8601 ✓ เวลาจบ stepsarray ของ object ✓ ผลรายขั้น ช่องใน steps[] · 3 ช่อง ช่อง ชนิด ต้องมี ความหมาย stepstand_front · stand_start · sit_start✓ ① ยืนตรงหันหน้าเข้ากล้อง ② ท่าตั้งต้นยืน ③ ท่าตั้งต้นนั่ง resultok · skipped_by_screening · not_reached✓ ok = บันทึกแล้ว · skipped_by_screening = ข้ามเพราะผลคัดกรองบอกว่าทำไม่ได้ · not_reached = ออกก่อนถึงขั้นนี้ attemptsinteger ✓ จำนวนครั้งที่ลอง (รวมรอบที่เริ่มขั้นใหม่หลังไม่สำเร็จ 3 ครั้ง)
valuesobject ค่าปรับเทียบ 5 ค่าที่เก็บกับโปรไฟล์ (ชื่อค่าเป็นร่าง: สัดส่วนร่างกายเป็นเมตรโดยประมาณ) · เป็นข้อมูลสัดส่วนร่างกาย ต้องอยู่ในประกาศ IRB cameraobject สภาพภาพตอนปรับเทียบ ช่องใน camera · 3 ช่อง ช่อง ชนิด ต้องมี ความหมาย distance_mnumber / null ระยะผู้ฝึกถึงกล้องโดยประมาณ (เมตร) lightok · lowแสงพอไหม full_bodyboolean เห็นเต็มตัวไหม
ตัวอย่าง — ปรับเทียบ calibration.json 39 บรรทัด ลองส่ง คัดลอก
{
"schema_version": "0.3",
"calibration_id": "e8e7569d-e35c-4940-8411-e83db712881b",
"participant_id": "8e80639a-270d-49da-ad3c-88c4cf8491d8",
"device_id": "PB-07",
"trigger": "first_training",
"status": "success",
"started_at": "2026-10-07T09:18:30+07:00",
"completed_at": "2026-10-07T09:20:00+07:00",
"steps": [
{
"step": "stand_front",
"result": "ok",
"attempts": 1
},
{
"step": "stand_start",
"result": "ok",
"attempts": 2
},
{
"step": "sit_start",
"result": "ok",
"attempts": 1
}
],
"values": {
"shoulder_width": 0.37,
"torso_len": 0.51,
"hip_width": 0.3,
"upper_arm_len": 0.29,
"thigh_len": 0.43
},
"camera": {
"distance_m": 2.4,
"light": "ok",
"full_body": true
}
}
คำตอบ 201 บันทึกแล้ว
รูปแบบ created: id = รหัสที่ส่งมา · received_at = เวลาที่ระบบกลางรับ
ตัวอย่างคำตอบ 4 บรรทัด คัดลอก
{
"id": "e8e7569d-e35c-4940-8411-e83db712881b",
"received_at": "2026-10-07T09:20:04+07:00"
}
ข้อผิดพลาดเฉพาะ endpoint
ตอบได้ทุก endpointที่มี token: 400 invalid_field · 401 token_invalid · 429 rate_limited · 500 server_error · 503 unavailable
ลองส่ง endpoint นี้
ผลการฝึก 1 รอบ — ส่งเมื่อรอบจบ: SC-15 (ครบหรือไม่ครบ) · SC-08 (กดปุ่มแดง) · ออกจากหน้าจัดตำแหน่งกลางรอบ · เปิดเครื่องหลังไฟดับ · ผู้ใช้ทั่วไปไม่ส่ง
ส่งจากหน้า SC-15 · SC-08 · SC-09 · เปิดเครื่องหลังไฟดับ
token Authorization: Bearer …
กล่องไม่มีเน็ต เข้าคิว ส่งเมื่อออนไลน์
ระบบกลางต้องทำ ผ่านกฎขึ้นแอป → สร้างการ์ด app_activity ภายใน 1 นาที แล้วคิด app_summary ใหม่ · 409 duplicate ไม่สร้างและไม่แก้การ์ด ตัดสินจาก started_at (เวลาที่เกิด) — ข้อมูลมาช้าเป็นวันหรือไม่เรียงได้ · ดาวคิดที่กล่อง ระบบกลางไม่ต้องคิด criteria · reps (คะแนนรายเกณฑ์ ค่าองศา) งานวิจัยเท่านั้น — ตารางส่งออกดู PDF ข้อ 6
ช่อง ชนิด ต้องมี ความหมาย schema_versionstring ^0\.\d+$ ✓ รุ่นของโครงข้อมูล ตอนนี้ "0.3" · รุ่นย่อยที่ใหม่กว่าอาจมีช่องเพิ่ม ระบบกลางให้ข้ามช่องที่ไม่รู้จัก session_idUUID ✓ รหัสรอบ (UUID) สร้างที่กล่อง ใช้กันข้อมูลซ้ำ participant_idUUID ✓ รหัสผู้เข้าร่วม (v0.1 ชื่อ user_id) · บังคับมี เพราะผู้ใช้ทั่วไปไม่ส่ง session device_idstring ^[A-Z0-9-]{3,32}$ ✓ รหัสกล่อง PB-01 … PB-16 (ระบบกลางลงทะเบียนพร้อมชื่อสถานที่ตั้ง ข้อ 8) · ต้องตรงกับ token program_idstring ^PG-\d+$ ✓ โปรแกรมที่เลือก (SC-06) program_namestring ✓ ชื่อโปรแกรม program_of_the_dayboolean เลือกการ์ด "โปรแกรมวันนี้" (หมุน PG-1 → 2 → 3) หรือเลือกเอง modecontinuous · step_by_step✓ โหมดที่เลือกตอนเริ่มรอบ (SC-10) · โหมดรายท่าอยู่ใน exercises[].mode started_atเวลา ISO 8601 ✓ เวลาเริ่มท่าแรก ended_atเวลา ISO 8601 ✓ เวลาที่รอบจบ (ขึ้นหน้าสรุป SC-15 หรือ SC-08) total_duration_secnumber ✓ ended_at − started_at (รวมช่วงพักและช่วงเปลี่ยนท่า) active_duration_secnumber ✓ ผลรวม duration_sec ของทุกท่า (เวลาที่ฝึกจริง) · ใช้คิดนาทีในการ์ดแอป statuscompleted · incomplete · safety_stopped · unwell_stopped✓ completed = ครบทุกท่า · incomplete = ฝึกไม่ครบ · safety_stopped = ระบบหยุดเพื่อความปลอดภัยครั้งที่ 2 · unwell_stopped = กดปุ่มแดง ไม่สบาย end_reasonfinished · user_ended · idle_timeout · second_safety_stop · unwell_button · left_at_camera_check · power_loss✓ finished = ครบ · user_ended = กด [จบการฝึกวันนี้] · idle_timeout = ไม่มีการกด 3 นาทีที่ SC-14 · second_safety_stop · unwell_button · left_at_camera_check = ถูกส่งไปจัดตำแหน่งกลางรอบ (SC-09) แล้วออก · power_loss = ไฟดับ/เครื่องดับกลางรอบ ส่งตอนเปิดเครื่องครั้งถัดไป daily_checkobject ✓ ผลคัดกรองสั้นก่อนฝึก ช่องใน daily_check · 4 ช่อง ช่อง ชนิด ต้องมี ความหมาย has_symptomsfalse✓ คัดกรองสั้นก่อนฝึก (SC-07) ตอบ [ไม่มี] เสมอ · ถ้ามีอาการจะไม่มี session ส่ง event แทน answered_byself · caregiver · nullผู้ตอบ · null = ไม่ได้ถาม (flow ยังรอยืนยันว่าจะถามไหม) questionnaire_versionstring รุ่นชุด 5 อาการ checked_atเวลา ISO 8601 ✓ เวลาที่ตอบ
screening_idUUID หรือ null ผลคัดกรองแบบเต็มที่ใช้อยู่ตอนฝึก (ใช้กรองโปรแกรม) calibration_idUUID หรือ null ค่าปรับเทียบที่ใช้ overall_scorenumber / null ✓ คะแนนรวม 0–100 = ค่าเฉลี่ย accuracy_score ของท่าที่ประเมินได้ · null = ประเมินไม่ได้ทั้งรอบ starsinteger ✓ ดาว 0–3 (กล่องคิด ระบบกลางไม่ต้องคิด): 0 = ทำครบไม่ถึงครึ่งของท่าย่อย · 1 = ครบครึ่งขึ้นไปแต่ไม่ครบ · 2 = ครบทุกท่าแต่มีเหตุการณ์ความปลอดภัย (safety_event_ids ไม่ว่าง) · 3 = ครบทุกท่าและไม่มีเหตุการณ์ความปลอดภัย · กฎดวงที่ 3 รอลูกค้ายืนยัน summaryobject ✓ สิ่งที่หน้าสรุปผล SC-15 แสดง ช่องใน summary · 4 ช่อง ช่อง ชนิด ต้องมี ความหมาย poses_totalinteger ✓ จำนวนท่าย่อยในโปรแกรม = จำนวน exercises[] (เช่น PG-1 = 6 · ตารางข้อ 5.7) poses_completedinteger ✓ ท่าย่อยที่ทำครบ (จำนวน ไม่ใช่ %) good_posesarray ของ string ^P\d_\d(_\d)?$ ท่าที่ทำได้ดี (item_code) แสดงในสรุป SC-15 improve_posesarray ของ object ท่าที่ควรปรับปรุง สูงสุด 2 ท่า · ฝึกไม่ครบจะว่างเสมอ ช่องใน summary.improve_poses[] · 2 ช่อง ช่อง ชนิด ต้องมี ความหมาย item_codestring ^P\d_\d(_\d)?$ ✓ รหัสท่า tipstring ✓ คำแนะนำแก้ 1 ประโยค
pausesarray ของ object การหยุดพักทุกครั้ง (SC-14) ช่องใน pauses[] · 7 ช่อง ช่อง ชนิด ต้องมี ความหมาย atเวลา ISO 8601 ✓ เวลาที่หยุด resumed_atเวลา ISO 8601 หรือ null เวลากลับมาฝึก · null = ไม่ได้กลับ byuser · system✓ user = ผู้ใช้กดปุ่มรีโมต · system = ระบบให้พัก reasonbutton · balance_lost · too_fast · out_of_frame · not_assessable_timeout · user_absent_timeout✓ button = ผู้ใช้กด · balance_lost / too_fast (ครั้งที่ 3) / out_of_frame (> 2 วิ) = หยุดเพื่อความปลอดภัย · not_assessable_timeout / user_absent_timeout = ประเมินไม่ได้ / ไม่เห็นผู้ฝึก นาน 2 นาที safety_stopboolean ✓ true = หยุดเพื่อความปลอดภัย (เสียสมดุล เร็วเกิน ออกนอกกรอบ) · ครั้งที่ 2 ในรอบ = จบการฝึก outcomeresumed · switched_mode · ended · unwell_button✓ resumed = ฝึกต่อ · switched_mode = เปลี่ยนโหมด (เฉพาะผู้ใช้กดหยุด) · ended = จบการฝึก · unwell_button = กดปุ่มแดงระหว่างพัก → SC-08 event_idUUID หรือ null เหตุการณ์ความปลอดภัยที่คู่กัน (ระบบเป็นผู้หยุด)
repositionsinteger ถูกพาไปจัดตำแหน่งกล้องใหม่กลางรอบกี่ครั้ง (ระยะต่างจากค่าปรับเทียบ > 25% หรือประเมินไม่ได้ครั้งที่ 3) exercisesarray ของ object ✓ ผลรายท่าย่อยตามลำดับ ครบทุกท่าย่อยในโปรแกรม (ท่าที่ไม่ได้เริ่ม = not_done) · ลำดับตามตารางข้อ 5.7 ช่องใน exercises[] · 21 ช่อง ช่อง ชนิด ต้องมี ความหมาย pose_nointeger ✓ ลำดับท่าในรอบ (v0.1 / ร่างงวด 1 ข้อ 4.1) set_nointeger ✓ ชุดท่า 1–3 (P1-x = 1) (v0.1 / ร่างงวด 1 ข้อ 4.1) pose_codestring ^P\d-\d$ ✓ รหัสท่าหลัก P1-1 … P3-3 (+ P2-4 ท่าเป็ด) · ตารางชื่อท่าข้อ 5.7 item_codestring ^P\d_\d(_\d)?$ ✓ รหัสท่าย่อย เช่น P1_1_3 (ยืดบน) · P1_2 · ใช้อ้างท่าในสรุปผล · ตารางชื่อท่าข้อ 5.7 pose_namestring ✓ ชื่อท่า modecontinuous · step_by_step✓ โหมดตอนเริ่มท่านี้ mode_switchesinteger เปลี่ยนโหมดกลางท่ากี่ครั้ง (ปุ่มที่ 3 ใน SC-14) started_atเวลา ISO 8601 เวลาเริ่มท่า duration_secnumber ✓ เวลาที่ใช้ในท่านี้ ไม่นับช่วงหยุดพัก reps_targetinteger ✓ จำนวนครั้งเป้าหมาย (โหมดสอนทีละขั้น: ขั้นสุดท้ายคือท่าเต็ม นับเป็น 1 ครั้ง) reps_doneinteger ✓ ครั้งที่นับ — นับเฉพาะครั้งที่ประเมินได้ (flow SC-12) · ท่าครบเมื่อ reps_done = reps_target attempts_not_assessedinteger ✓ ครั้งที่ทำแต่ไม่นับ เพราะประเมินไม่ได้ (หลายคนในกรอบ แสงไม่พอ ใกล้/ไกลเกิน) hold_sec_targetnumber / null ค้างท่ากี่วินาที (เป้าหมาย) assessedboolean มีอย่างน้อย 1 ครั้งที่ประเมินได้ (v0.1) resultgood · needs_improvement · partial · not_assessed · not_done✓ good = ท่าที่ทำได้ดี · needs_improvement = ท่าที่ควรปรับปรุง · partial = เริ่มแล้วแต่ไม่ครบ · not_assessed = ทำแต่ประเมินไม่ได้เลย · not_done = ไม่ได้เริ่ม accuracy_scorenumber / null ✓ ความถูกต้อง 0–100 = เกณฑ์ท่าที่ผ่าน ÷ (เกณฑ์ท่าที่ประเมินได้ + ข้อเตือนที่เตือน) ทุกครั้งรวมกัน (ข้อ 6.4) · null = ประเมินไม่ได้ (ห้ามแทนด้วย 0) tipstring / null คำแนะนำ 1 ประโยคที่แสดงในสรุป (ท่าที่ควรปรับปรุง) stepsobject หรือ null โหมดสอนทีละขั้นตอนเท่านั้น · โหมดต่อเนื่อง = null ช่องใน exercises[].steps · 3 ช่อง ช่อง ชนิด ต้องมี ความหมาย totalinteger ✓ จำนวนขั้น doneinteger ✓ ทำได้ skippedinteger ✓ ข้ามขั้น (ยังไม่ได้ใน 60 วิ แล้วเลือกข้าม)
warningsobject คำเตือนระหว่างท่า ช่องใน exercises[].warnings · 3 ช่อง ช่อง ชนิด ต้องมี ความหมาย too_fastinteger ✓ เตือนเร็วเกินกี่ครั้ง out_of_frameinteger ✓ ออกนอกกรอบกี่ครั้ง not_assessable_secnumber ✓ รวมเวลาที่ประเมินไม่ได้
criteriaarray ของ object ✓ เกณฑ์ทุกข้อของท่าย่อยนี้ตาม pose_config (ตรงกับแถว Jt-n ในฟอร์มลูกค้า) พร้อมผลนับรวม · ส่งครบทุกข้อเสมอแม้ reps_done = 0 (นับ 0/0/0) · โหมดสอนทีละขั้นใช้เกณฑ์ของท่าเต็ม (ขั้นสุดท้าย) · ท่า not_done ส่ง [] ช่องใน exercises[].criteria[] · 16 ช่อง ช่อง ชนิด ต้องมี ความหมาย jtstring ^Jt-\d+$ ✓ ลำดับจุดตามฟอร์มลูกค้า (Jt-1, Jt-2, …) kindpose · warning✓ pose = เกณฑ์ของท่า (ผ่าน/ไม่ผ่าน นับเข้าคะแนนทั้งคู่) · warning = ข้อเตือน เช่น ยกไหล่สูงเกิน / ห่อไหล่ (มีทุกท่า) — เตือน (failed) = นับเป็นไม่ผ่าน · ไม่เตือน (passed) = นับจำนวนไว้ แต่ไม่เข้าคะแนน (ข้อ 6.4) relative_tostart_position · nullstart_position = ค่าที่วัดเป็นส่วนต่างจาก "ฐาน" ของผู้ฝึกคนนั้น: ค่ากลางของภาพช่วงรอเริ่มครั้งแรก (ยืนท่าเตรียม) ≥ 1 วินาที ของท่าย่อยแรกในท่าหลักนั้น · ท่าย่อยถัดไปของท่าหลักเดียวกันใช้ฐานเดิม · ยังไม่มีฐาน = ประเมินไม่ได้ (not_assessed) · null = ค่าตรง keystring ✓ รหัสเกณฑ์ในระบบ เช่น arm_lift_l jointstring ชื่อจุดแบบ Kinect 32 จุด เช่น SHOULDER_LEFT · ระยะ 2 จุดใช้ WRIST_LEFT~WRIST_RIGHT namestring ✓ ชื่อที่ผู้เชี่ยวชาญเห็น เช่น Shoulder Lt measurestring สิ่งที่วัด เช่น shoulder_flexion, elbow_flexion, trunk_tilt, distance_ratio axisX · Y · Z · 3D · 2D✓ ระนาบตามเปเปอร์: X = มองข้าง (sagittal) · Y = มองหน้า (frontal) · Z = มองบน (transverse) · 3D = มุมจริงในอวกาศ · 2D = มุมบนภาพ unitdeg · ratio · torso_pct · ratio_pct✓ deg = องศา · ratio = ระยะ ÷ ความกว้างไหล่ · torso_pct = % ของความยาวลำตัว · ratio_pct = อัตราส่วน × 100 (ห่อไหล่: ไหล่กว้าง ÷ สะโพกกว้าง) normalnumber / null ค่าปกติ (Range Normal) ที่ผู้เชี่ยวชาญตั้ง tolerancenumber / null ยอมให้คลาด ± (+/-) rulewithin · at_least · at_most✓ within = อยู่ใน normal ± tolerance · at_least = ไม่ต่ำกว่า normal − tolerance (เช่น ศอก ≥ ไหล่) · at_most = ไม่เกิน normal + tolerance refnumber / null ค่าอ้างอิงจากเอกสาร/เปเปอร์ (Ref) ถ้ามี passedinteger ✓ จำนวนครั้งที่ผ่านเกณฑ์นี้ (ข้อเตือน: ครั้งที่ไม่เตือน — ไม่เข้าคะแนน) failedinteger ✓ จำนวนครั้งที่ไม่ผ่าน (ข้อเตือน: ครั้งที่เตือน) not_assessedinteger ✓ จำนวนครั้งที่นับแล้วแต่จุดนี้ประเมินไม่ได้ (เช่น มือบังกัน · ข้อเตือน: ยังไม่มีฐานท่าเตรียม หรือมองไม่เห็นไหล่) — ไม่นับเป็นไม่ผ่าน · passed + failed + not_assessed = reps_done
repsarray ของ object ✓ ค่าที่วัดได้ทีละครั้ง เฉพาะครั้งที่นับ (จำนวน = reps_done) ช่องใน exercises[].reps[] · 5 ช่อง ช่อง ชนิด ต้องมี ความหมาย repinteger ✓ ครั้งที่ hold_secnumber ค้างท่าได้กี่วินาที scorenumber / null ✓ คะแนนครั้งนี้ 0–100 = เกณฑ์ท่าที่ผ่าน ÷ (เกณฑ์ท่าที่ประเมินได้ + ข้อเตือนที่เตือน) · null = ประเมินไม่ได้ทุกข้อ resultpass · fail · null✓ pass = score ≥ 70 (เกณฑ์ผ่านของกล่อง · TOR ≥ 70%) · fail = ต่ำกว่า · null = score เป็น null valuesobject ✓ ค่าที่วัดได้ตอนค้างท่า (ค่ากลาง) แยกตาม jt เช่น {"Jt-1": 151.2} · null = จุดนั้นประเมินไม่ได้ในครั้งนี้
safety_event_idsarray ของ UUID ✓ เหตุการณ์ระดับ warning / critical ในรอบนี้ (ส่งแยกที่ /v1/event) · ระดับ info ไม่อยู่ในนี้ app_versionstring รุ่นซอฟต์แวร์ (v0.1) versionsobject ✓ รุ่นของทุกอย่างที่มีผลกับคะแนน (ให้ทีมวิจัยแยกข้อมูลก่อน/หลังปรับเกณฑ์ได้) ช่องใน versions · 4 ช่อง ช่อง ชนิด ต้องมี ความหมาย appstring ✓ รุ่นซอฟต์แวร์ pose_configstring ✓ รุ่นเกณฑ์ท่า (ค่าที่ผู้เชี่ยวชาญตั้ง) scoringstring รุ่นวิธีวัด modelstring โมเดลจับท่า
ตัวอย่าง — ผลการฝึก — PG-1 ครบ 6 ท่าย่อย session.json 1255 บรรทัด
ตัวอย่าง — ผลการฝึก — PG-5 ฝึกไม่ครบ (สอนทีละขั้น) session_incomplete.json 519 บรรทัด
ตัวอย่าง — ผลการฝึก — หยุดเพราะไม่สบาย session_unwell.json 509 บรรทัด
คำตอบ 201 บันทึกแล้ว
รูปแบบ created: id = รหัสที่ส่งมา · received_at = เวลาที่ระบบกลางรับ
ตัวอย่างคำตอบ 4 บรรทัด คัดลอก
{
"id": "da0dd4ca-7815-4c8e-b93d-c5b162ba0bbd",
"received_at": "2026-10-07T09:31:01+07:00"
}
ข้อผิดพลาดเฉพาะ endpoint
ตอบได้ทุก endpointที่มี token: 400 invalid_field · 401 token_invalid · 429 rate_limited · 500 server_error · 503 unavailable
ลองส่ง endpoint นี้
เหตุการณ์ความปลอดภัย — ส่งแยกจาก session · ไม่ใช่การแจ้งเตือนเรียลไทม์ (ส่งเมื่อออนไลน์) · กล่องไม่มีระบบแจ้งเหตุฉุกเฉิน/ตรวจจับการล้ม
ส่งจากหน้า SC-01 · SC-04 · SC-07 · SC-11/12 · SC-14
token Authorization: Bearer …
กล่องไม่มีเน็ต เข้าคิว ส่งเมื่อออนไลน์
ระบบกลางต้องทำ อาจถึงก่อน session ที่อ้าง — ต้องรับ session_id ที่ยังไม่รู้จัก subject: "guest" = ผู้ใช้ทั่วไป ไม่มี participant_id / session_id · นับใน Dashboard เท่านั้น ไม่อยู่ในไฟล์ส่งออกcontext บังคับตาม reason: เหตุระหว่างฝึกต้องมี item_code + rep · เหตุที่ทำให้พักต้องมี pause_no · daily_check_symptoms ต้องมี program_id
reason → type · severity (11 แบบ) reason type severity เกิดเมื่อ daily_check_symptomssafety_warningwarningSC-07 ตอบ [มีอาการ] → SC-08 (ไม่มี session ตามมา)screening_danger_signalsafety_warningwarningSC-04 ตอบ มี/ไม่แน่ใจ ในข้อสัญญาณอันตราย → SC-08 unwell_buttonsafety_warningcriticalกดปุ่มแดง ไม่สบาย ระหว่างฝึก/พัก → SC-08 หยุดทันที balance_lostbalance_lostwarningเสียสมดุล → ระบบให้พัก (SC-14 ) · นับเป็นหยุดเพื่อความปลอดภัย too_faststop_rule_triggeredwarningเร็วเกินครั้งที่ 3 ติดกัน → ระบบให้พัก · นับเป็นหยุดเพื่อความปลอดภัย out_of_frameuser_out_of_framewarningออกนอกกรอบ > 2 วิ → ระบบให้พัก · นับเป็นหยุดเพื่อความปลอดภัย second_safety_stopstop_rule_triggeredwarningหยุดเพื่อความปลอดภัยครั้งที่ 2 ในรอบเดียว → จบการฝึก user_absentuser_out_of_frameinfoไม่เห็นผู้ฝึกเลย > 2 วิ ระหว่างฝึก (SC-12 หยุดรอในหน้าเดิม) บันทึกให้ทีมวิจัย ไม่ใช่การแจ้งเตือนฉุกเฉิน user_absent_timeoutuser_out_of_frameinfoไม่เห็นผู้ฝึกนาน 2 นาที → พัก (SC-14 ) · ไม่นับเป็นหยุดเพื่อความปลอดภัย not_assessable_timeoutdetection_failinfoประเมินไม่ได้ต่อเนื่อง 2 นาที → พัก (SC-14 ) · ไม่นับเป็นหยุดเพื่อความปลอดภัย camera_not_founddevice_errorinfoSC-01 ตรวจไม่พบกล้อง
ช่อง ชนิด ต้องมี ความหมาย schema_versionstring ^0\.\d+$ ✓ รุ่นของโครงข้อมูล ตอนนี้ "0.3" · รุ่นย่อยที่ใหม่กว่าอาจมีช่องเพิ่ม ระบบกลางให้ข้ามช่องที่ไม่รู้จัก event_idUUID ✓ รหัสเหตุการณ์ (UUID) device_idstring ^[A-Z0-9-]{3,32}$ ✓ รหัสกล่อง PB-01 … PB-16 (ระบบกลางลงทะเบียนพร้อมชื่อสถานที่ตั้ง ข้อ 8) · ต้องตรงกับ token subjectparticipant · guest · device✓ participant = ผู้เข้าร่วม (มี participant_id) · guest = ผู้ใช้ทั่วไป ไม่มีรหัสคนเลย (ส่ง — ข้อ 6.1) · device = เหตุการณ์ของเครื่อง participant_idUUID หรือ null ผู้เข้าร่วม (v0.1 ชื่อ user_id) · null เมื่อ subject เป็น guest หรือ device session_idUUID หรือ null รอบที่เกิด · null = เกิดนอกรอบฝึก · session อาจมาถึงทีหลัง event (ระบบกลางต้องรับ session_id ที่ยังไม่รู้จัก) typebalance_lost · detection_fail · device_error · safety_warning · stop_rule_triggered · user_out_of_frame✓ ประเภทตามร่าง API งวด 1 ข้อ 4.2 (6 แบบเดิม) reasondaily_check_symptoms · screening_danger_signal · unwell_button · balance_lost · too_fast · out_of_frame · second_safety_stop · user_absent · user_absent_timeout · not_assessable_timeout · camera_not_found✓ สาเหตุละเอียด (ดูตารางเหตุการณ์) severityinfo · warning · critical✓ ระดับ occurred_atเวลา ISO 8601 ✓ เวลาที่เกิด screenstring ^SC-\d{2}$ หน้าจอที่เกิด contextobject รายละเอียดที่ระบบกำหนดเท่านั้น (คีย์ตามตารางนี้) เช่น {"item_code": "P1_2", "rep": 2, "pause_no": 1} · ห้ามมีข้อมูลส่วนบุคคลหรือข้อความอิสระ · บังคับ: เหตุระหว่างฝึก (unwell_button · balance_lost · too_fast · out_of_frame · second_safety_stop · user_absent · user_absent_timeout · not_assessable_timeout) ต้องมี item_code + rep รวมผู้ใช้ทั่วไป · เหตุที่ทำให้พักต้องมี pause_no · daily_check_symptoms ต้องมี program_id ช่องใน context · 5 ช่อง ช่อง ชนิด ต้องมี ความหมาย item_codestring ^P\d_\d(_\d)?$ ท่าย่อยที่กำลังฝึก (ชื่อตามข้อ 5.7) pose_codestring ^P\d-\d$ ท่าหลัก (ไม่บังคับ · ถ้าส่งต้องตรงกับ item_code) repinteger ครั้งที่กำลังทำ = ครั้งที่นับแล้ว + 1 pause_nointeger ลำดับการพักในรอบ = ลำดับใน session.pauses[] (เริ่ม 1) · เฉพาะเหตุที่ทำให้พัก program_idstring ^PG-\d+$ โปรแกรมที่กำลังจะฝึก (daily_check_symptoms)
ตัวอย่าง — เหตุการณ์ — ระบบให้พัก (เร็วเกิน) event_system_pause.json 19 บรรทัด ลองส่ง คัดลอก
{
"schema_version": "0.3",
"device_id": "PB-07",
"subject": "participant",
"participant_id": "8e80639a-270d-49da-ad3c-88c4cf8491d8",
"event_id": "ec8cf3c7-2b0a-4ebc-bd6c-6d2694317f6f",
"session_id": "da0dd4ca-7815-4c8e-b93d-c5b162ba0bbd",
"type": "stop_rule_triggered",
"reason": "too_fast",
"severity": "warning",
"occurred_at": "2026-10-07T09:25:58+07:00",
"screen": "SC-12",
"context": {
"item_code": "P1_2",
"pose_code": "P1-2",
"rep": 2,
"pause_no": 1
}
}
ตัวอย่าง — เหตุการณ์ — กดปุ่มแดง ไม่สบาย event_unwell.json 17 บรรทัด ลองส่ง คัดลอก
{
"schema_version": "0.3",
"device_id": "PB-07",
"subject": "participant",
"participant_id": "8e80639a-270d-49da-ad3c-88c4cf8491d8",
"event_id": "8f962aac-d7e2-4bfb-a677-4e03952e8975",
"session_id": "a5f0c3d8-6e21-4b9a-8d47-0e3f9b1c2a6d",
"type": "safety_warning",
"reason": "unwell_button",
"severity": "critical",
"occurred_at": "2026-10-09T08:39:46+07:00",
"screen": "SC-12",
"context": {
"item_code": "P1_1_1",
"rep": 1
}
}
ตัวอย่าง — เหตุการณ์ — คัดกรองก่อนฝึก มีอาการ event_daily_check.json 16 บรรทัด ลองส่ง คัดลอก
{
"schema_version": "0.3",
"device_id": "PB-07",
"subject": "participant",
"participant_id": "8e80639a-270d-49da-ad3c-88c4cf8491d8",
"event_id": "cb0843aa-1362-4f5f-add2-6837e9f7b7a0",
"session_id": null,
"type": "safety_warning",
"reason": "daily_check_symptoms",
"severity": "warning",
"occurred_at": "2026-10-10T09:02:44+07:00",
"screen": "SC-07",
"context": {
"program_id": "PG-1"
}
}
ตัวอย่าง — เหตุการณ์ — ผู้ใช้ทั่วไป (ไม่มีรหัสคน) event_guest.json 17 บรรทัด ลองส่ง คัดลอก
{
"schema_version": "0.3",
"event_id": "f3b8d2c1-4a6e-4f09-b7d5-2c8e1a9f0b36",
"device_id": "PB-07",
"subject": "guest",
"participant_id": null,
"session_id": null,
"type": "safety_warning",
"reason": "unwell_button",
"severity": "critical",
"occurred_at": "2026-10-09T15:12:03+07:00",
"screen": "SC-12",
"context": {
"item_code": "P1_2",
"rep": 1
}
}
คำตอบ 201 บันทึกแล้ว
รูปแบบ created: id = รหัสที่ส่งมา · received_at = เวลาที่ระบบกลางรับ
ตัวอย่างคำตอบ 4 บรรทัด คัดลอก
{
"id": "ec8cf3c7-2b0a-4ebc-bd6c-6d2694317f6f",
"received_at": "2026-10-07T09:26:02+07:00"
}
ข้อผิดพลาดเฉพาะ endpoint
ตอบได้ทุก endpointที่มี token: 400 invalid_field · 401 token_invalid · 429 rate_limited · 500 server_error · 503 unavailable
ลองส่ง endpoint นี้
ถอนความยินยอมและลบผู้ใช้ — กล่องลบโปรไฟล์ ข้อมูลในเครื่อง และรายการของคนนี้ที่ยังไม่ได้ส่งทันที แล้วส่งคำขอนี้เป็นรายการแรกของคิว · ระบบกลางรับได้แม้ไม่เคยเห็น participant นี้ · ยกเลิกการเชื่อมแอปของคนนี้ด้วย
ส่งจากหน้า SC-21
token Authorization: Bearer …
กล่องไม่มีเน็ต เข้าคิวเป็นรายการแรก
ระบบกลางต้องทำ กล่องส่งเป็นรายการแรกของคิว (ลบรายการที่ยังไม่ส่งของคนนั้นทิ้งก่อน) ระบบกลาง: รับได้แม้ไม่เคยเห็น participant นี้ (201) · ตั้งสถานะถอนแล้ว · จบ link ทุกตัว (withdraw) · ลบตามข้อ 6.1 และการ์ด/ตราตามข้อ 5.6 หลังจากนี้ participant · consent · screening · calibration · session · event · link ของรหัสนี้ตอบ 403 participant_withdrawn — ยกเว้น unlink และ withdraw ซ้ำ ตอบ 201 (request_id เดิม = 409 duplicate )
ช่อง ชนิด ต้องมี ความหมาย schema_versionstring ^0\.\d+$ ✓ รุ่นของโครงข้อมูล ตอนนี้ "0.3" · รุ่นย่อยที่ใหม่กว่าอาจมีช่องเพิ่ม ระบบกลางให้ข้ามช่องที่ไม่รู้จัก request_idUUID ✓ รหัสคำขอ (UUID) สร้างที่กล่อง กันส่งซ้ำ participant_idUUID ✓ รหัสผู้เข้าร่วม (UUID v4) กล่องสุ่มตอนกด [ยินยอม] (SC-03) · ไม่มีชื่อหรือข้อมูลที่ระบุตัวบุคคล device_idstring ^[A-Z0-9-]{3,32}$ ✓ รหัสกล่อง PB-01 … PB-16 (ระบบกลางลงทะเบียนพร้อมชื่อสถานที่ตั้ง ข้อ 8) · ต้องตรงกับ token withdrawn_atเวลา ISO 8601 ✓ เวลากดยืนยันชั้นที่ 2 requested_from"device_admin"✓ ผู้ดูแลกดในกล่อง (SC-21 ถอนความยินยอมและลบผู้ใช้)
ตัวอย่าง — ถอนความยินยอมและลบผู้ใช้ withdraw.json 8 บรรทัด ลองส่ง คัดลอก
{
"schema_version": "0.3",
"request_id": "8e32a370-1ea3-4f73-a455-d6ff80574f76",
"participant_id": "1c7e4b2a-93d5-4f60-8a1e-5b9c0d2e7f34",
"device_id": "PB-07",
"withdrawn_at": "2026-11-15T11:20:00+07:00",
"requested_from": "device_admin"
}
คำตอบ 201 บันทึกแล้ว · participant ที่ไม่รู้จัก / ถอนไปแล้ว ก็ตอบ 201
รูปแบบ created: id = รหัสที่ส่งมา · received_at = เวลาที่ระบบกลางรับ
ตัวอย่างคำตอบ 4 บรรทัด คัดลอก
{
"id": "8e32a370-1ea3-4f73-a455-d6ff80574f76",
"received_at": "2026-11-15T11:20:04+07:00"
}
ข้อผิดพลาดเฉพาะ endpoint
ตอบได้ทุก endpointที่มี token: 400 invalid_field · 401 token_invalid · 429 rate_limited · 500 server_error · 503 unavailable
ลองส่ง endpoint นี้
ส่งย้อนหลังทีละหลายรายการ (ทางเลือก) — ประมวลผลตามลำดับ ไม่ใช่ all-or-nothing รายการที่ผิดไม่หยุดรายการถัดไป · ตอบ 200 เสมอเมื่อรับคำขอได้ ผลจริงอยู่รายรายการ
ส่งจากหน้า หลังออฟไลน์
token Authorization: Bearer …
กล่องไม่มีเน็ต เข้าคิว ส่งเมื่อออนไลน์
ระบบกลางต้องทำ ประมวลผลตามลำดับ ไม่ใช่ all-or-nothing · ตอบ 200 เสมอเมื่อรับคำขอได้ ผลจริงอยู่รายรายการ · ไม่ทำ กล่องส่งทีละรายการได้
ช่อง ชนิด ต้องมี ความหมาย itemsarray ของ object ✓ ≤ 20 รายการ เรียงตามลำดับคิว ช่องใน items[] · 2 ช่อง ช่อง ชนิด ต้องมี ความหมาย typewithdraw · participant · consent · pairing_unlink · screening · calibration · session · event✓ ชนิด (เหมือน endpoint เดี่ยว) bodyobject ✓ ข้อมูลเหมือนส่งเดี่ยว
ตัวอย่าง — participant + consent ในคำขอเดียว 32 บรรทัด คัดลอก
{
"items": [
{
"type": "participant",
"body": {
"schema_version": "0.3",
"participant_id": "8e80639a-270d-49da-ad3c-88c4cf8491d8",
"participant_code": "PB07-0012",
"device_id": "PB-07",
"enrollment": "app",
"created_at": "2026-10-07T09:14:10+07:00",
"app_version": "0.3.0"
}
},
{
"type": "consent",
"body": {
"schema_version": "0.3",
"consent_id": "e48f2be7-c617-4583-87e7-9626be62c4c2",
"participant_id": "8e80639a-270d-49da-ad3c-88c4cf8491d8",
"device_id": "PB-07",
"scopes": [
"research",
"app"
],
"notice_version": "PB-CONSENT-20261001",
"context": "new_user",
"given_at": "2026-10-07T09:12:40+07:00"
}
}
]
}
คำตอบ 200 ผลรายรายการ
ช่อง ชนิด ต้องมี ความหมาย resultsarray ของ object ✓ ผลรายรายการ ครบทุกรายการ ช่องใน results[] · 3 ช่อง ช่อง ชนิด ต้องมี ความหมาย indexinteger ✓ ลำดับในคำขอ statusinteger ✓ รหัสสถานะเหมือนส่งเดี่ยว (201 / 409 / 400 / 403 / 422 …) errorobject / null ถ้าไม่สำเร็จ รูปแบบเดียวกับ Error
ตัวอย่างคำตอบ 19 บรรทัด คัดลอก
{
"results": [
{
"index": 0,
"status": 201,
"error": null
},
{
"index": 1,
"status": 409,
"error": {
"code": "duplicate",
"message": "บันทึกแล้ว",
"field": null,
"request_id": "req-5d21c0"
}
}
]
}
ตอบได้ทุก endpointที่มี token: 400 invalid_field · 401 token_invalid · 429 rate_limited · 500 server_error · 503 unavailable
ลองส่ง endpoint นี้
เช็คว่าระบบกลางออนไลน์
ส่งจากหน้า SC-01
token ไม่ต้องมี
กล่องไม่มีเน็ต —
ระบบกลางต้องทำ ไม่ต้องมี token · กล่องเรียกตอนเปิดเครื่อง
คำตอบ 200 {"status": "ok"}
ตัวอย่างคำตอบ 3 บรรทัด
ตอบได้ทุก endpoint: 429 · 500 · 503
ลองส่ง endpoint นี้
ข้อมูลฝั่งแอป ภายในระบบของทีม — กล่องไม่เรียก · ชื่อช่องเป็นคำแนะนำ
กฎว่า session ไหนขึ้นแอป session ขึ้นแอป เมื่อ participant มี link ที่เคยเป็น active และ from_at ≤ session.started_at < until_at (link ที่ยังไม่จบ ถือว่า until_at = ไม่มีกำหนด) — link ที่ค้าง pending แล้ว expired ไม่ส่งอะไร from_at = given_at ของ consent ที่ผูกกับ link · until_at = at ของ unlink / withdrawn_at — เป็นนาฬิกากล่อง เหมือน started_at จึงไม่เพี้ยนแม้นาฬิกากล่องคลาด (อย่าเทียบกับ linked_at ซึ่งเป็นเวลาระบบกลาง) · ยกเลิกจากแอปใช้เวลาระบบกลาง (คลาดได้ไม่กี่นาที ยอมรับได้)ตัดสินจาก เวลาที่เกิด (started_at) — ข้อมูลอาจมาถึงช้าหรือไม่เรียง · ผลที่มาถึงหลังยกเลิกการเชื่อมแต่ฝึกก่อนยกเลิก ยังขึ้น ไม่ส่งย้อนหลัง: session ก่อน from_at ไม่ขึ้นแอป (ผู้ใช้ยินยอมส่งเข้าแอปตอนเชื่อม ข้อมูลก่อนนั้นอยู่ในงานวิจัยอย่างเดียว)ขึ้นแอปเฉพาะ session · คัดกรอง ปรับเทียบ เหตุการณ์ คะแนนรายเกณฑ์ ค่าองศา ไม่ขึ้นแอป (งานวิจัยเท่านั้น) session ส่งซ้ำ (409 duplicate) ไม่สร้างและไม่แก้การ์ด · สร้างการ์ดภายใน 1 นาทีหลังรับ
ยกเลิกการเชื่อม / ถอนความยินยอม เหตุ การ์ดที่ขึ้นแอปแล้ว ตรา ผลใหม่ ยกเลิกการเชื่อม (กล่อง หรือแอป) คงไว้ (เป็นข้อมูลของผู้ใช้)คงไว้ ไม่ขึ้นแอป (เว้นแต่ฝึกก่อนเวลายกเลิก) ถอนความยินยอม (withdraw) ลบทุกใบ ของ participant นั้น (รวมที่ฝึกก่อน withdrawn_at) แล้วคิดหน้าสะสมใหม่ลบตราที่ source_activity_id ถูกลบ แล้วคิดใหม่ (ได้คืนถ้าการ์ดจากกล่องอื่นยังเข้าเงื่อนไข — awarded_at ใหม่) ระบบกลางตอบ 403 participant_withdrawn ลบบัญชีแอป หายไปกับบัญชี หายไปกับบัญชี link จบ (account_deleted)
app_qr — QR สำหรับเชื่อมกล่อง (แอป → ระบบกลาง ภายในของทีมแอป) mock: POST /mock/app-qr แอปเรียกตอนเปิดเมนู "เชื่อมกล่องฤๅษีดัดตน" และทุก 5 นาทีระหว่างเปิดหน้านั้น · บัญชีหนึ่งมี QR ที่ยังไม่หมดอายุได้ ≤ 3 ตัว
ช่อง ชนิด ต้องมี ความหมาย qr_textstring ^PBLINK:1:[A-Za-z0-9_-]{22}$ ✓ ข้อความที่แอปแสดงเป็น QR = PBLINK:1: + โทเคนสุ่ม 16 ไบต์ base64url (22 ตัว) · ระบบกลางเก็บแค่ SHA-256 ของโทเคน issued_atเวลา ISO 8601 ✓ เวลาออก expires_atเวลา ISO 8601 ✓ หมดอายุ = issued_at + 15 นาที refresh_after_sec300✓ แอปขอ QR ใหม่เองทุก 5 นาที (ตัวเก่ายังใช้ได้จนหมดอายุ) — QR บนจอจึงเหลืออายุ ≥ 10 นาทีเสมอ
ฝั่งแอป — QR สำหรับเชื่อมกล่อง app_qr.json 6 บรรทัด คัดลอก
{
"qr_text": "PBLINK:1:q7Xc2v9Lq0Rk7mT1sPa4Zw",
"issued_at": "2026-10-07T09:10:00+07:00",
"expires_at": "2026-10-07T09:25:00+07:00",
"refresh_after_sec": 300
}app_activity — การ์ดผลการฝึก 1 รอบในแอป (ระบบกลาง → แอป ภายในของทีมแอป) mock: GET /mock/app/{account}/activity สร้างจาก session ที่ผ่านกฎข้อ 5.1 ภายใน 1 นาทีหลังรับ · ไม่มีคะแนนรายเกณฑ์/ค่าองศา/คัดกรอง/เหตุการณ์ (งานวิจัยเท่านั้น)
ช่อง ชนิด ต้องมี ความหมาย activity_idUUID ✓ = session.session_id (กันซ้ำ: session ส่งซ้ำได้ 409 duplicate — ไม่สร้างและไม่แก้การ์ด) device_idstring ^[A-Z0-9-]{3,32}$ ✓ รหัสกล่อง PB-01 … PB-16 (ระบบกลางลงทะเบียนพร้อมชื่อสถานที่ตั้ง ข้อ 8) · ต้องตรงกับ token site_namestring ✓ ชื่อสถานที่ของกล่อง จากทะเบียนเครื่องของระบบกลาง (ข้อ 8) occurred_atเวลา ISO 8601 ✓ = session.started_at (แสดงวันเวลาที่ฝึกจริง ไม่ใช่เวลาที่ข้อมูลมาถึง) ended_atเวลา ISO 8601 ✓ = session.ended_at program_idstring ^PG-\d+$ ✓ = session.program_id program_namestring ✓ = session.program_name statuscompleted · incomplete · safety_stopped · unwell_stopped✓ = session.status status_textฝึกครบ · ฝึกไม่ครบ · หยุดเพื่อความปลอดภัย · หยุดพักเพราะไม่สบาย✓ ข้อความแสดงตาม status (ข้อ 5.3) active_minutesinteger ✓ = session.active_duration_sec ÷ 60 ปัดครึ่งขึ้น = floor(x / 60 + 0.5) poses_completedinteger ✓ = session.summary.poses_completed poses_totalinteger ✓ = session.summary.poses_total starsinteger ✓ = session.stars good_posesarray ของ object ✓ = session.summary.good_poses แปลงเป็นชื่อ ช่องใน good_poses[] · 2 ช่อง ช่อง ชนิด ต้องมี ความหมาย item_codestring ^P\d_\d(_\d)?$ ✓ รหัสท่าย่อย namestring ✓ ชื่อท่าที่แสดง (ตารางข้อ 5.7)
improve_posesarray ของ object ✓ = session.summary.improve_poses ช่องใน improve_poses[] · 3 ช่อง ช่อง ชนิด ต้องมี ความหมาย item_codestring ^P\d_\d(_\d)?$ ✓ รหัสท่าย่อย namestring ✓ ชื่อท่า tipstring ✓ = session.summary.improve_poses[].tip (คำแนะนำ 1 ประโยค แสดงตามที่กล่องส่ง)
ฝั่งแอป — การ์ดผลการฝึก (ฝึกครบ) app_activity.json 44 บรรทัด คัดลอก
{
"activity_id": "da0dd4ca-7815-4c8e-b93d-c5b162ba0bbd",
"device_id": "PB-07",
"site_name": "หน่วยบริการตัวอย่าง ก",
"occurred_at": "2026-10-07T09:20:40+07:00",
"ended_at": "2026-10-07T09:30:57+07:00",
"program_id": "PG-1",
"program_name": "ผ่อนคลายคอ บ่า ไหล่",
"status": "completed",
"status_text": "ฝึกครบ",
"active_minutes": 8,
"poses_completed": 6,
"poses_total": 6,
"stars": 2,
"good_poses": [
{
"item_code": "P1_1_0",
"name": "ท่าแก้เกียจ · ยืดหน้า"
},
{
"item_code": "P1_1_1",
"name": "ท่าแก้เกียจ · ยืดขวา"
},
{
"item_code": "P1_1_2",
"name": "ท่าแก้เกียจ · ยืดซ้าย"
},
{
"item_code": "P1_1_3",
"name": "ท่าแก้เกียจ · ยืดบน"
},
{
"item_code": "P1_3",
"name": "ท่าแก้ปวดท้อง แก้ข้อเท้า"
}
],
"improve_poses": [
{
"item_code": "P1_2",
"name": "ท่าแก้ปวดท้อง แก้สะบักจม",
"tip": "ยกข้อศอกขึ้นให้เสมอไหล่ หรือสูงกว่าเล็กน้อย"
}
]
}ชื่อท่าที่แสดงในแอป (item_code → ชื่อ) item_code pose_code ชุด ชื่อที่แสดงในแอป P1_1_0P1-11 ท่าแก้เกียจ · ยืดหน้า P1_1_1P1-11 ท่าแก้เกียจ · ยืดขวา P1_1_2P1-11 ท่าแก้เกียจ · ยืดซ้าย P1_1_3P1-11 ท่าแก้เกียจ · ยืดบน P1_2P1-21 ท่าแก้ปวดท้อง แก้สะบักจม P1_3P1-31 ท่าแก้ปวดท้อง แก้ข้อเท้า P2_1_1P2-12 ท่าแก้ลมในอก ในเอว · ก้าวเท้าขวา P2_1_2P2-12 ท่าแก้ลมในอก ในเอว · ก้าวเท้าซ้าย P2_2_2P2-22 ท่าแก้ลมอก · หันขวา P2_2_1P2-22 ท่าแก้ลมอก · หันซ้าย P2_3P2-32 ท่าแก้กล่อนปัตคาดแก้ลมในเอว P2_4P2-42 ท่าเป็ด P3_1_1P3-13 ท่าแก้เข่า แก้ขา · เท้าขวาหน้า P3_1_2P3-13 ท่าแก้เข่า แก้ขา · เท้าซ้ายหน้า P3_2P3-23 ท่าดำรงกายอายุยืน P3_3P3-33 แก้ตะโพกสลักเพชร
ฝั่งแอป — การ์ดผลการฝึก (ฝึกไม่ครบ) app_activity_incomplete.json 22 บรรทัด คัดลอก
{
"activity_id": "3b9d2f61-7c4e-4a0b-9e15-2f8a6c1d4e70",
"device_id": "PB-07",
"site_name": "หน่วยบริการตัวอย่าง ก",
"occurred_at": "2026-10-08T16:05:12+07:00",
"ended_at": "2026-10-08T16:08:52+07:00",
"program_id": "PG-5",
"program_name": "ชุดท่าง่าย ความเสี่ยงต่ำ",
"status": "incomplete",
"status_text": "ฝึกไม่ครบ",
"active_minutes": 3,
"poses_completed": 1,
"poses_total": 7,
"stars": 0,
"good_poses": [
{
"item_code": "P1_1_0",
"name": "ท่าแก้เกียจ · ยืดหน้า"
}
],
"improve_poses": []
}ฝั่งแอป — การ์ดผลการฝึก (หยุดเพราะไม่สบาย) app_activity_unwell.json 22 บรรทัด คัดลอก
{
"activity_id": "a5f0c3d8-6e21-4b9a-8d47-0e3f9b1c2a6d",
"device_id": "PB-07",
"site_name": "หน่วยบริการตัวอย่าง ก",
"occurred_at": "2026-10-09T08:38:05+07:00",
"ended_at": "2026-10-09T08:39:50+07:00",
"program_id": "PG-1",
"program_name": "ผ่อนคลายคอ บ่า ไหล่",
"status": "unwell_stopped",
"status_text": "หยุดพักเพราะไม่สบาย",
"active_minutes": 2,
"poses_completed": 1,
"poses_total": 6,
"stars": 0,
"good_poses": [
{
"item_code": "P1_1_0",
"name": "ท่าแก้เกียจ · ยืดหน้า"
}
],
"improve_poses": []
}app_summary — หน้าผลสะสมในแอป (SC-16 ฝั่งแอป) mock: GET /mock/app/{account}/summary total_stars · sessions · completed_sessions · best_streak_days · ตรา คิดใหม่ทุกครั้งที่มีการ์ดเข้า (ข้อมูลมาช้า/ไม่เรียงได้ — คิดจาก occurred_at) · streak_days คิดตอนอ่าน (ข้อ 5.4)
ช่อง ชนิด ต้องมี ความหมาย as_ofเวลา ISO 8601 ✓ เวลาที่คำนวณหน้านี้ — streak_days คิด ณ เวลานี้ (คิดตอนอ่านทุกครั้ง เพราะขึ้นกับวันนี้) streak_daysinteger ✓ วันติดกันที่มีการ์ด completed ≥ 1 ใบ นับย้อนจากวันของ as_of (วันนั้นยังไม่ฝึก นับจากเมื่อวาน · เมื่อวานก็ไม่มี = 0) · วันตามเวลาไทย — ข้อ 5.4 best_streak_daysinteger ✓ ต่อเนื่องนานที่สุดที่เคยทำได้ total_starsinteger ✓ ผลรวม stars ทุก activity sessionsinteger ✓ จำนวน activity ทั้งหมด completed_sessionsinteger ✓ จำนวน activity ที่ status completed last_activity_atเวลา ISO 8601 หรือ null ✓ occurred_at ล่าสุด badgesarray ของ object ✓ ตราที่ได้แล้ว เรียงตามเวลาที่ได้ ช่องใน badges[] · 3 ช่อง ช่อง ชนิด ต้องมี ความหมาย badge_idfirst_session · all_programs · streak_7 · streak_30 · stars_50 · sessions_20✓ รหัสตรา (ตารางข้อ 5.4) namestring ✓ ชื่อตราที่แสดง awarded_atเวลา ISO 8601 ✓ = ended_at ของการ์ดที่ทำให้ได้ตราครั้งแรก · เก็บถาวรในตาราง app_badge ไม่เปลี่ยนเมื่อคิดใหม่ · ถูกลบได้กรณีเดียวคือถอนความยินยอม (ข้อ 5.6)
ฝั่งแอป — หน้าผลสะสม app_summary.json 16 บรรทัด คัดลอก
{
"as_of": "2026-10-09T20:00:00+07:00",
"streak_days": 0,
"best_streak_days": 1,
"total_stars": 2,
"sessions": 3,
"completed_sessions": 1,
"last_activity_at": "2026-10-09T08:38:05+07:00",
"badges": [
{
"badge_id": "first_session",
"name": "ก้าวแรก",
"awarded_at": "2026-10-07T09:30:57+07:00"
}
]
}ตรา 6 แบบ badge_id ชื่อ ได้เมื่อ first_sessionก้าวแรก การ์ด completed ใบแรก all_programsลองครบ 3 โปรแกรม การ์ด completed ของ program_id ต่างกันอย่างน้อย 3 โปรแกรม (โปรแกรมใดก็ได้ — ระบบกลางไม่ต้องรู้ผลคัดกรอง · ผู้ที่ผลคัดกรองเหลือโปรแกรมน้อยกว่า 3 จะไม่ได้ตรานี้) streak_7ต่อเนื่อง 7 วัน best_streak_days ≥ 7streak_30ต่อเนื่อง 30 วัน best_streak_days ≥ 30stars_50ดาว 50 ดวง ดาวสะสม ≥ 50 sessions_20ฝึกครบ 20 รอบ การ์ด completed ≥ 20
หน้าจอในแอป สูตรความต่อเนื่อง/ดาว/ตราแบบเต็ม: PDF ข้อ 5