Play Box API v0.3

Play Box API v0.3

กล่อง Play Box → ระบบกลาง (งานวิจัย + แอปหมอไทยใกล้บ้าน) · C26-0040P ฤๅษีดัดตน Exercise Play Box · คณะแพทยศาสตร์ศิริราชพยาบาล

ภาพรวม

กล่อง Play Box 16 เครื่องส่งข้อมูลเข้า API เดียว ของระบบกลาง · ทุกรายการเข้าคิวในกล่องก่อน ส่งซ้ำได้โดยข้อมูลไม่ซ้ำ · ต้องออนไลน์ตอนนั้นมีแค่ ตรวจ QR กับ เชื่อมบัญชีแอป · ชื่อเรียก รูป ภาพ/วิดีโอ จุดโครงกระดูกดิบ PIN ไม่ออกจากกล่อง

ที่อยู่ API

Mock (ใช้ได้ตอนนี้)
https://playbox-api.srv1168222.hstgr.cloud/v1/…
mock ของระบบกลางที่ทีม Play Box ทำ — ลองส่งได้ที่ ทดสอบส่ง
Staging ของทีม
แจ้ง URL ให้ทีม Play Box (path ขึ้นต้น /v1/ เหมือนกัน) · ต้องพร้อมก่อนทดสอบเชื่อมต่อร่วม
Production
แจ้งภายหลัง
คีย์ทดสอบ (mock)
PB-01 … PB-16 · api_key test-key-PB-01 … test-key-PB-16

การยืนยันตัวตน

  1. POST /v1/auth/token {"device_id","api_key"} → access_token (JWT อายุ 1 ชม.)
  2. คำขออื่นทุกตัวใส่ Authorization: Bearer <JWT> (ยกเว้น GET /v1/health)
  3. หมดอายุ/ผิด → 401 token_invalid กล่องขอใหม่ 1 ครั้ง · device_id ในข้อมูลต้องตรงกับ token ไม่งั้น 403 device_mismatch

ระบบกลางต้องทำ

หัวข้องานดูที่
① QRออก QR (app_qr) · POST /v1/pairing/verify · /link · /unlink · ตารางโทเคน + ตารางการเชื่อมกฎ pairing
② ส่งผลเข้าแอปรับ session แล้วสร้างการ์ดผลการฝึก (app_activity) ภายใน 1 นาที · คิดหน้าสะสม (app_summary)ข้อมูลฝั่งแอป · PDF ข้อ 5
③ งานวิจัยรับข้อมูล 7 ชนิด · เก็บตามตารางข้อ 6.2 · ส่งออก CSV · ลบเมื่อถอนความยินยอมPDF ข้อ 6
③ Dashboardเว็บ 7 หน้า สำหรับทีมวิจัยและผู้ดูแลโครงการ ตามนิยามตัวชี้วัดข้อ 7PDF ข้อ 7
พื้นฐานPOST /v1/auth/token · POST /v1/device/heartbeat · GET /v1/health · ทะเบียนเครื่อง 16 เครื่อง + ชื่อสถานที่auth · heartbeat

ไม่ต้องทำ: ค่าตั้งเครื่อง (/v1/device/config) และอัปเดตซอฟต์แวร์ (/v1/firmware/*) — ทีม Play Box โฮสต์เองบนเซิร์ฟเวอร์ของทีม · /v1/batch ยังเป็นทางเลือก

กฎร่วมที่มีผลกับโค้ด

  • HTTPS (TLS 1.2+) · JSON Content-Type: application/json; charset=utf-8
  • เวลา ISO 8601 มีเขตเวลา เช่น 2026-10-07T09:20:05+07:00 · ตัดสินจากเวลาที่เกิดในข้อมูล ไม่ใช่เวลาที่รับ (ข้อมูลมาช้าเป็นชั่วโมงหรือเป็นวันได้)
  • กันซ้ำ: ทุกรายการมี UUID ที่กล่องสร้าง ส่งซ้ำได้ · รหัสที่บันทึกแล้วตอบ 409 duplicate = กล่องถือว่าสำเร็จ · ทั้งสองฝั่งดูที่ error.code
  • null ≠ 0: null = ประเมินไม่ได้ ห้ามแทนด้วย 0
  • ช่องที่ไม่รู้จัก: เก็บหรือข้าม ห้ามตอบ 400 (schema_version รุ่นย่อยใหม่เพิ่มช่องได้)
  • กล่องส่งซ้ำเอง: 400 ไม่ส่งซ้ำ (ย้ายไปกองรอคนตรวจ) · 429 / 5xx / ไม่มีเน็ต ลองใหม่ 1 → 5 → 15 → 60 นาที ไม่จำกัดรอบ ใช้ Retry-After ถ้ามี

ปริมาณ: 16 เครื่อง · session ละประมาณ 5–15 KB · ไม่เกิน 30 รอบต่อเครื่องต่อวัน

ผู้ใช้ 3 แบบ

เชื่อมแอปใช้เฉพาะเครื่องผู้ใช้ทั่วไป
enrollmentappdevice_only—
consent.scopes["research","app"]["research"]—
ส่งเข้างานวิจัย✓✓✗ — เหตุการณ์ความปลอดภัยแบบไม่มีรหัสคน นับใน Dashboard เท่านั้น (ไม่อยู่ในไฟล์ส่งออก)
ส่งต่อไปบัญชีแอป✓ ตามกฎข้อ 5.1✗ — เชื่อมทีหลังได้ที่ SC-21✗
ผลการฝึก / คัดกรอง / ปรับเทียบส่งส่งไม่ส่ง (ไม่บันทึกด้วย)
เหตุการณ์ความปลอดภัยส่งส่งส่ง subject: "guest" ไม่มี participant_id ไม่มี session_id

ตาม flow

ลำดับหน้าจอของกล่อง (Flow+UI 5 ต.ค.) และ API ที่ส่งในแต่ละขั้น · ทุกรายการเข้าคิวในกล่อง ส่งเมื่อออนไลน์ ยกเว้นที่มีป้าย ต้องออนไลน์ · กด ลองส่ง เพื่อเปิดในหน้าทดสอบพร้อมตัวอย่าง

1เปิดเครื่อง

  1. SC-01 เปิดเครื่อง · ตรวจระบบ

    กล่องตรวจกล้อง เน็ต และข้อมูลรอส่ง (ผู้ใช้ไม่ต้องกด) แล้วส่งคิวที่ค้าง

    • POST /v1/auth/token → 200 device_id + api_key → JWT 1 ชม.
    • POST /v1/device/heartbeat → 200
    • GET /v1/health → 200
    • POST /v1/event → 201 เฉพาะไม่พบกล้อง: subject: device · reason: camera_not_found
  2. — อัตโนมัติ

    ทุก 15 นาที

  3. SC-01 เปิดเครื่อง · ตรวจระบบ

    เปิดเครื่องครั้งถัดไปหลังไฟดับกลางรอบ

    • POST /v1/session → 201 status: incomplete · end_reason: power_loss

2ผู้ใช้ใหม่ — เชื่อมแอป

  1. SC-02 เลือกผู้ใช้

    กด [เพิ่มผู้ใช้ใหม่]

    ไม่ส่งข้อมูล

  2. SC-22 เลือกวิธีใช้

    เลือก [แอปหมอไทยใกล้บ้าน] (เลือกได้เมื่อออนไลน์)

    ไม่ส่งข้อมูล

  3. แอป แอปหมอไทยใกล้บ้าน

    เปิดเมนู "เชื่อมกล่องฤๅษีดัดตน" แอปแสดง QR (ขอใหม่เองทุก 5 นาที)

    • POST /mock/app-qr → 200 API ภายในของทีม ชื่อ path ตามสะดวก คืนรูปแบบ app_qr · ใน mock ใช้ /mock/app-qr
  4. SC-18 สแกน QR จากแอป

    ยื่น QR ในแอปให้กล้องของกล่อง

    • POST /v1/pairing/verify → 200 ต้องออนไลน์mode: new_user · ไม่ส่งว่าเป็นใคร · expires_in_sec < 300 กล่องให้ขอ QR ใหม่
  5. SC-03 ประกาศความยินยอม

    อ่านประกาศครบแล้วกด [ยินยอม]

    • POST /v1/pairing/link → 201 ต้องออนไลน์participant_id + consent_id จองล่วงหน้า · link = pending
  6. SC-20 ไม่ยินยอม

    กด [ไม่ยินยอม]

    ไม่ส่งข้อมูล

  7. SC-19 ตั้งชื่อโปรไฟล์

    ตั้งชื่อ กด [เสร็จ]

    • POST /v1/participant → 201 enrollment: app → link เป็น active
    • POST /v1/consent → 201 scopes: [research, app] · context: new_user
  8. SC-19 ตั้งชื่อโปรไฟล์

    กดยกเลิกตอนตั้งชื่อ (หลังเชื่อมแล้ว)

    • POST /v1/pairing/unlink → 201 reason: cancelled_before_profile · ส่งทันที
  9. SC-23 เลือกผู้ตอบคัดกรอง

    เลือกผู้ตอบ แล้วคัดกรองแบบเต็ม (SC-04) จนครบ

    • POST /v1/screening → 201 ครั้งแรก + ทุก 30 วัน
    • POST /v1/event → 201 เฉพาะพบสัญญาณอันตราย: reason: screening_danger_signal → SC-08

3ผู้ใช้ใหม่ — ใช้เฉพาะเครื่อง

  1. SC-22 เลือกวิธีใช้

    เลือก [ใช้เฉพาะเครื่องนี้] (ไม่ต้องมีเน็ต) → SC-03 กด [ยินยอม]

    ไม่ส่งข้อมูล

  2. SC-19 ตั้งชื่อโปรไฟล์

    ตั้งชื่อ กด [เสร็จ]

    • POST /v1/participant → 201 enrollment: device_only
    • POST /v1/consent → 201 scopes: [research] · ไม่มี link
  3. SC-23 เลือกผู้ตอบคัดกรอง

    คัดกรองแบบเต็ม (SC-04) เหมือนผู้ใช้แอป

4ฝึกประจำวัน

  1. SC-02 เลือกผู้ใช้

    เลือกผู้ใช้ → SC-06 เลือกโปรแกรม

    ไม่ส่งข้อมูล

  2. SC-07 คัดกรองสั้นก่อนฝึก

    คัดกรองสั้น ตอบ [ไม่มีอาการ]

    ไม่ส่งข้อมูล

  3. SC-05 ปรับเทียบรายบุคคล

    ปรับเทียบ (ครั้งแรก / ผลคัดกรองเปลี่ยน) ทำครบหรือกดออก

  4. SC-09 ตรวจตำแหน่ง

    ตรวจตำแหน่ง → SC-10 เลือกโหมด

    ไม่ส่งข้อมูล

  5. SC-12 ฝึก · ต่อเนื่อง

    ฝึก แล้วระบบให้พัก (SC-14): เสียสมดุล · เร็วเกิน · ออกนอกกรอบ · ประเมินไม่ได้นาน

    • POST /v1/event → 201 อาจถึงก่อน session ที่อ้าง
  6. SC-12 ฝึก · ต่อเนื่อง

    ไม่เห็นผู้ฝึก > 2 วิ ระหว่างฝึก

    • POST /v1/event → 201 reason: user_absent · severity: info
  7. SC-15 สรุปผล

    จบรอบ: ครบ / กด [จบการฝึกวันนี้] / ไม่กด 3 นาที / หยุดครั้งที่ 2

    • POST /v1/session → 201 ระบบกลางสร้างการ์ดในแอปภายใน 1 นาที (ถ้าผ่านกฎขึ้นแอป)
  8. SC-09 ตรวจตำแหน่ง

    ถูกพาไปจัดตำแหน่งกลางรอบแล้วออก

    • POST /v1/session → 201 status: incomplete · end_reason: left_at_camera_check

5หยุด / ไม่สบาย

  1. SC-07 คัดกรองสั้นก่อนฝึก

    คัดกรองสั้น ตอบ [มีอาการ] → SC-08

    • POST /v1/event → 201 reason: daily_check_symptoms · ไม่มี session ตามมา
  2. SC-12 ฝึก · ต่อเนื่อง

    กดปุ่มแดง ไม่สบาย ระหว่างฝึก/พัก → SC-08

    • POST /v1/event → 201 reason: unwell_button · critical
    • POST /v1/session → 201 status: unwell_stopped
  3. SC-14 หยุดพัก

    ระบบหยุดเพื่อความปลอดภัยครั้งที่ 2 ในรอบ

    • POST /v1/event → 201 2 รายการ: event ของเหตุ + second_safety_stop
    • POST /v1/session → 201 status: safety_stopped

6ผู้ใช้ทั่วไป

  1. SC-02 เลือกผู้ใช้

    กด [ผู้ใช้ทั่วไป] — ไม่ส่ง session / คัดกรอง / ปรับเทียบ

    ไม่ส่งข้อมูล

  2. SC-12 ฝึก · ต่อเนื่อง

    กดปุ่มแดง หรือระบบให้พัก

    • POST /v1/event → 201 subject: guest · participant_id และ session_id = null

7ผู้ดูแล (SC-17 ใส่ PIN → SC-21)

  1. SC-21 ผู้ดูแล · จัดการผู้ใช้

    [เชื่อมแอปทีหลัง] → สแกน QR (SC-18) → กด [ยินยอม] (SC-03)

    • POST /v1/pairing/verify → 200 ต้องออนไลน์mode: link_later
    • POST /v1/pairing/link → 201 ต้องออนไลน์participant มีแล้ว → active ทันที
    • POST /v1/consent → 201 scopes: [app] · context: link_later · ส่งหลัง link 201 เท่านั้น
  2. SC-21 ผู้ดูแล · จัดการผู้ใช้

    [ยกเลิกการเชื่อมแอป]

    • POST /v1/pairing/unlink → 201 reason: admin_unlink · เข้าคิว
  3. SC-21 ผู้ดูแล · จัดการผู้ใช้

    [ถอนความยินยอมและลบผู้ใช้] ยืนยัน 2 ชั้น

    • POST /v1/withdraw → 201 เข้าคิวเป็นรายการแรก · หลังจากนี้รหัสนี้ตอบ 403 participant_withdrawn
  4. SC-21 ผู้ดูแล · จัดการผู้ใช้

    [ปรับเทียบใหม่]

ลำดับในคิวของกล่อง (ระบบกลางต้องรับได้)

  • รายการของผู้เข้าร่วมคนเดียวกันส่งตามเวลาที่สร้าง โดย participant มาก่อนเสมอ · ต่างคนส่งสลับกันได้
  • event อาจมาถึงก่อน session ที่อ้างถึง ระบบกลางต้องรับ session_id ที่ยังไม่รู้จัก
  • ถอนความยินยอม: กล่องลบรายการของคนนั้นที่ยังไม่ได้ส่งทิ้ง แล้วส่ง withdraw เป็นรายการแรก
  • ระบบกลางตอบ 422 participant_unknown: กล่องส่ง participant และ consent ก่อน แล้วลองรายการเดิมใหม่

Endpoints 14

ทุกตารางสร้างจาก JSON Schema (schemas/*.schema.json) · ช่องที่เป็น object/array เปิดดูช่องย่อยได้ · "ข้อ N" ในคำอธิบาย = ข้อใน PDF สเปกเต็ม

pathทำอะไรส่งจากหน้าสำเร็จ
POST/v1/auth/tokenขอ token ของเครื่องSC-01200
POST/v1/device/heartbeatแจ้งสถานะเครื่อง (ใช้ในหน้า Dashboard เครื่อง)SC-01 · ทุก 15 นาที200
POST/v1/pairing/verifyตรวจ QR จากแอปSC-18200
POST/v1/pairing/linkเชื่อมบัญชีแอปกับผู้เข้าร่วมSC-03 [ยินยอม]201
POST/v1/pairing/unlinkยกเลิกการเชื่อมSC-19 ยกเลิก · SC-21201
POST/v1/participantผู้เข้าร่วมใหม่SC-19 [เสร็จ]201
POST/v1/consentความยินยอมSC-19 / SC-21201
POST/v1/screeningผลคัดกรองแบบเต็มSC-04201
POST/v1/calibrationผลปรับเทียบSC-05201
POST/v1/sessionผลการฝึก 1 รอบSC-15 · SC-08 · SC-09 · เปิดเครื่องหลังไฟดับ201
POST/v1/eventเหตุการณ์ความปลอดภัยSC-01 · SC-04 · SC-07 · SC-11/12 · SC-14201
POST/v1/withdrawถอนความยินยอมและลบผู้ใช้SC-21201
POST/v1/batchส่งย้อนหลังทีละหลายรายการ (ทางเลือก)หลังออฟไลน์200
GET/v1/healthเช็คว่าระบบกลางออนไลน์SC-01200
POST/v1/auth/token200

ขอ 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 คีย์

Request body token_request.schema.json

ช่องชนิดต้องมีความหมาย
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

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)

Request body heartbeat_request.schema.json

ช่องชนิดต้องมีความหมาย
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

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

ลำดับเงื่อนไขverifylink
1ไม่พบ SHA-256 ของโทเคน422 qr_not_recognized422 qr_not_recognized
2(link เท่านั้น) บัญชี + กล่องนี้ + participant_id นี้ มี link pending/active อยู่แล้ว = คำขอซ้ำ—201 + link_id และ linked_at เดิม · ไม่แตะโทเคน
3โทเคนใช้แล้ว หรือเลย expires_at410 qr_expired410 qr_expired
4(link เท่านั้น) participant_id นี้ถอนความยินยอมแล้ว—403 participant_withdrawn
5new_user และบัญชีมี link active ในกล่องนี้409 account_has_profile_here409 account_has_profile_here
6link_later และบัญชีมี link active ในกล่องนี้ (verify ไม่รู้ว่าผู้ใช้คนไหนจึงดูทุกคน · link เป็นคนอื่นแน่นอน เพราะคนเดิมเข้าข้อ 2 ไปแล้ว)409 account_linked_elsewhere409 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)

Request body pairing_verify_request.schema.json

ช่องชนิดต้องมีความหมาย
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

POST/v1/participant201

ผู้เข้าร่วมใหม่ — ส่งหลังตั้งชื่อโปรไฟล์เสร็จ (SC-19) · ชื่อเรียกและรูป avatar อยู่ในกล่องเท่านั้น ไม่ส่ง

ส่งจากหน้า
SC-19 [เสร็จ]
token
Authorization: Bearer …
กล่องไม่มีเน็ต
เข้าคิว ส่งเมื่อออนไลน์
ระบบกลางต้องทำ
  • บันทึกแล้วเปลี่ยน link pending ของ participant_id นี้เป็น active (+ แจ้งแอป) · เก็บช่วงอายุ 5 ปี + เพศจากบัญชีแอปครั้งเดียวตอนนี้
  • participant_code ซ้ำกับ participant_id อื่น → 409 participant_code_taken (กล่องเลื่อนเลขแล้วส่งใหม่)

Request body participant.schema.json

ช่องชนิดต้องมีความหมาย
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

POST/v1/screening201

ผลคัดกรองแบบเต็ม

ส่งจากหน้า
SC-04
token
Authorization: Bearer …
กล่องไม่มีเน็ต
เข้าคิว ส่งเมื่อออนไลน์
ระบบกลางต้องทำ
  • งานวิจัยเท่านั้น ไม่ขึ้นแอป · ระบบกลางไม่ต้องรู้ถ้อยคำคำถาม (question_id + questionnaire_version)

Request body screening.schema.json

ช่องชนิดต้องมีความหมาย
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

POST/v1/calibration201

ผลปรับเทียบ

ส่งจากหน้า
SC-05
token
Authorization: Bearer …
กล่องไม่มีเน็ต
เข้าคิว ส่งเมื่อออนไลน์
ระบบกลางต้องทำ
  • งานวิจัยเท่านั้น ไม่ขึ้นแอป · values เป็นสัดส่วนร่างกาย (ชื่อค่าเป็นร่าง)

Request body calibration.schema.json

ช่องชนิดต้องมีความหมาย
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

POST/v1/session201

ผลการฝึก 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

Request body session.schema.json

ช่องชนิดต้องมีความหมาย
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

POST/v1/event201

เหตุการณ์ความปลอดภัย — ส่งแยกจาก 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 แบบ)
reasontypeseverityเกิดเมื่อ
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 ตรวจไม่พบกล้อง

Request body event.schema.json

ช่องชนิดต้องมีความหมาย
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

POST/v1/withdraw201

ถอนความยินยอมและลบผู้ใช้ — กล่องลบโปรไฟล์ ข้อมูลในเครื่อง และรายการของคนนี้ที่ยังไม่ได้ส่งทันที แล้วส่งคำขอนี้เป็นรายการแรกของคิว · ระบบกลางรับได้แม้ไม่เคยเห็น 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)

Request body withdraw.schema.json

ช่องชนิดต้องมีความหมาย
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

POST/v1/batch200

ส่งย้อนหลังทีละหลายรายการ (ทางเลือก) — ประมวลผลตามลำดับ ไม่ใช่ all-or-nothing รายการที่ผิดไม่หยุดรายการถัดไป · ตอบ 200 เสมอเมื่อรับคำขอได้ ผลจริงอยู่รายรายการ

ส่งจากหน้า
หลังออฟไลน์
token
Authorization: Bearer …
กล่องไม่มีเน็ต
เข้าคิว ส่งเมื่อออนไลน์
ระบบกลางต้องทำ
  • ประมวลผลตามลำดับ ไม่ใช่ all-or-nothing · ตอบ 200 เสมอเมื่อรับคำขอได้ ผลจริงอยู่รายรายการ · ไม่ทำ กล่องส่งทีละรายการได้

Request body batch_request.schema.json

ช่องชนิดต้องมีความหมาย
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

GET/v1/health200

เช็คว่าระบบกลางออนไลน์

ส่งจากหน้า
SC-01
token
ไม่ต้องมี
กล่องไม่มีเน็ต
—
ระบบกลางต้องทำ
  • ไม่ต้องมี token · กล่องเรียกตอนเปิดเครื่อง

คำตอบ 200 {"status": "ok"}

ตัวอย่างคำตอบ 3 บรรทัด
{
  "status": "ok"
}

ตอบได้ทุก endpoint: 429 · 500 · 503

ข้อมูลฝั่งแอป ภายในระบบของทีม — กล่องไม่เรียก · ชื่อช่องเป็นคำแนะนำ

กฎว่า 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_codepose_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 ≥ 7
streak_30ต่อเนื่อง 30 วันbest_streak_days ≥ 30
stars_50ดาว 50 ดวงดาวสะสม ≥ 50
sessions_20ฝึกครบ 20 รอบการ์ด completed ≥ 20

หน้าจอในแอป สูตรความต่อเนื่อง/ดาว/ตราแบบเต็ม: PDF ข้อ 5

ข้อผิดพลาด

ตอบเป็น {"error": {…}} เสมอ · กล่องตัดสินใจจาก error.code ไม่ใช่ HTTP status อย่างเดียว

รูปแบบ error error_qr_expired.json 8 บรรทัด
{
  "error": {
    "code": "qr_expired",
    "message": "QR หมดอายุหรือใช้ไปแล้ว",
    "field": null,
    "request_id": "req-7f3a91"
  }
}
codeHTTPความหมายกล่องทำอะไร
invalid_field400ข้อมูลผิดรูปแบบ · field บอกชื่อช่องไม่ส่งซ้ำ ย้ายไปกองรอคนตรวจ · ถ้าเป็น participant/consent รายการที่ตามมาของคนนั้นรอไว้ (ไม่ทิ้ง)
token_invalid401token หมดอายุ/ไม่ถูกต้องขอ token ใหม่ 1 ครั้งแล้วส่งอีกที · ยังไม่ได้ หยุดคิวไว้ ลองใหม่ตามรอบ
participant_withdrawn403ผู้เข้าร่วมถอนความยินยอมแล้วทิ้งรายการนี้
device_mismatch403device_id ในข้อมูลไม่ตรงกับ tokenไม่ส่งซ้ำ ย้ายไปกองรอคนตรวจ
duplicate409รหัสนี้บันทึกแล้ว (ส่งซ้ำ)ถือว่าส่งสำเร็จ ลบจากคิว · ระบบกลางไม่ต้องเทียบเนื้อหา (กล่องไม่ใช้รหัสซ้ำ) แต่ควร log ถ้าต่าง
participant_unknown422ยังไม่มี participant นี้ในระบบส่ง participant + consent ก่อน แล้วลองรายการนี้ใหม่ · ครบ 3 รอบยังไม่ได้ ย้ายไปกองรอคนตรวจ
qr_not_recognized422รูปแบบถูกแต่ไม่รู้จักโทเคนนี้ (ไม่ใช่ QR ที่ระบบกลางออก)ขึ้น "ใช้ QR จากแอปหมอไทยใกล้บ้านเท่านั้น" แล้วสแกนต่อ · QR ที่ไม่ขึ้นต้น PBLINK:1: กล่องขึ้นข้อความนี้เองโดยไม่เรียก
qr_expired410QR หมดอายุหรือใช้ไปแล้วขึ้น "ขอ QR ใหม่ในแอป" แล้วสแกนต่อ · verify ได้ expires_in_sec < 300 กล่องขึ้นข้อความเดียวกันเอง (ไม่พอเวลาอ่านประกาศ)
account_has_profile_here409บัญชีแอปนี้มีโปรไฟล์ที่เชื่อมอยู่ (link active) ในกล่องนี้แล้ว (new_user)ขึ้น "บัญชีนี้มีชื่อในกล่องนี้แล้ว" แล้วกลับ SC-02 ให้เลือกชื่อเดิม · ไม่สร้างโปรไฟล์ใหม่
account_linked_elsewhere409บัญชีแอปนี้เชื่อมอยู่ (link active) กับผู้ใช้อื่นในกล่องนี้แล้ว (link_later)ขึ้น "ใช้ QR ของผู้ใช้คนนี้"
participant_code_taken409participant_code นี้มี participant_id อื่นใช้แล้วเลื่อนตัวนับในกล่องไปเลขถัดไปที่ว่าง แล้วส่ง participant เดิม (participant_id เดิม) ด้วยรหัสใหม่ · แสดงรหัสใหม่ในเมนู SC-21 · เกิดได้เมื่อติดตั้งกล่องใหม่แล้วตัวนับเริ่มใหม่ (กล่องเก็บตัวนับไว้ข้ามการติดตั้ง)
rate_limited429ส่งถี่เกินลองใหม่ตาม Retry-After หรือ 1 → 5 → 15 → 60 นาที ไม่จำกัดรอบ
server_error500ระบบกลางผิดพลาดลองใหม่ 1 → 5 → 15 → 60 นาที ไม่จำกัดรอบ (ไม่ย้ายไปกองรอคนตรวจ)
unavailable503ปิดปรับปรุงเหมือน 500 · ตรวจ QR / เชื่อมบัญชี: ลองอัตโนมัติทุก 5 วิ ขณะผู้ใช้รอหน้าจอ

กองรอคนตรวจ (กล่องไม่ส่งซ้ำ): 400 · 403 device_mismatch · 422 ครบ 3 รอบ

ทดสอบส่ง

ชุดทดสอบตาม flow

กำลังโหลด…

ส่งเอง

ค่าที่ใช้เติมตัวอย่าง participant_id · consent_id · qr_payload · link_id
link สำเร็จแล้วเก็บ link_id ให้เอง

Inbox ของ mock

สิ่งที่ mock ได้รับจากเครื่องนี้ (ใหม่สุดก่อน) และการ์ดที่ระบบกลางจำลองสร้างให้แอป

บัญชี = ช่อง account ด้านบน (ว่าง = บัญชีของชุดทดสอบที่รันล่าสุด)

ดาวน์โหลด

  • openapi.yaml 103 KB
    OpenAPI 3.1 — import เข้า Postman / Swagger / ตัวสร้างโค้ดได้
  • Postman collection 87 KB
    สร้างจากตัวอย่างตามลำดับ flow · ตั้ง baseUrl device_id api_key ใน Variables · ขั้น /mock/* ใช้กับ mock เท่านั้น (staging ใส่ qr_payload เอง)
  • ตัวรันชุดทดสอบ (CLI) 68 KB
    ชุดทดสอบเดียวกับหน้านี้ ไม่ติด CORS · Python 3.9+ ไม่ต้องติดตั้งอะไร · แตก zip แล้ว python playbox-test/mock/run_scenarios.py --base https://staging.ของทีม --key <api_key> --qr <QR จากแอป>
  • schemas.zip 29 KB
    JSON Schema 2020-12 ทั้ง 23 ไฟล์ (รวมฝั่งแอป app_qr · app_activity · app_summary)
  • examples.zip 15 KB
    ตัวอย่าง 25 ไฟล์ เรื่องเดียวต่อเนื่องตาม flow · ผ่าน schema แล้ว
  • API-SPEC-v0.3.pdf 1.1 MB
    สเปกเต็ม (ภาษาไทย) — เหตุผล งานวิจัย Dashboard นิยามตัวชี้วัด
  • ตัวอย่างไฟล์ส่งออกงานวิจัย (CSV) 8 KB
    รายละเอียดงานวิจัยดูใน PDF ข้อ 6
  • ตัวอย่างหน้าตา Dashboard 58 KB
    รายละเอียด Dashboard ดูใน PDF ข้อ 7