lowpoly จะยุติการให้บริการในวันที่ 30 ต.ค. 2569 โปรดเปลี่ยนโมเดลก่อนวันที่เหล่านี้เพื่อหลีกเลี่ยงข้อผิดพลาดของคำขอ ดูการเลิกใช้งานทั้งหมด

Creative Lab Keycap API

เปลี่ยนภาพถ่ายให้เป็นปุ่มคีย์แคป (keycap) แบบกำหนดเองสีเต็มรูปแบบสำหรับคีย์บอร์ดแบบกลไกได้ในสองขั้นตอน งานสร้างต้นแบบจะสร้างภาพของคีย์แคปที่เสร็จสมบูรณ์จากภาพถ่าย จากนั้นงานสร้างโมเดลจะเปลี่ยนภาพนั้นให้เป็นโมเดลคีย์แคป 3 มิติที่มี texture โดยใช้ ID ของงานสร้างต้นแบบเป็น input_task_id และ ID ของตัวเลือกภาพ (candidate) เป็น candidate_id

  • POST /openapi/creative-lab/keycap/v1/prototype
  • POST /openapi/creative-lab/keycap/v1/build

endpoint แบบ POST ทั้งสองแบบต้องใช้แผนแบบชำระเงิน คำขอจากบัญชีแผนฟรีจะได้รับการตอบกลับเป็น 402


POST/openapi/creative-lab/keycap/v1/prototype

สร้างงาน Keycap prototype

สร้างภาพหนึ่งภาพของคีย์แคปที่เสร็จสมบูรณ์จากรูปถ่าย ฟิลด์ image_urls ของงานจะเก็บรูปภาพ และ candidate_ids จะเก็บ ID ของรูปนั้น โดยแต่ละฟิลด์มีหนึ่งรายการ หากคุณไม่ชอบดีไซน์ที่ได้ ให้สร้างงาน prototype ใหม่อีกครั้ง Meshy จะคิดค่าใช้จ่ายแยกต่างหากสำหรับแต่ละงาน prototype

หากต้องการสร้างคีย์แคป 3D ให้ส่ง ID ของงานนี้ไปยัง เอนด์พอยต์ build เป็น input_task_id พร้อมกับรายการ candidate_ids เป็น candidate_id สำหรับฟิลด์ต่าง ๆ ของงาน โปรดดู อ็อบเจ็กต์งาน Keycap prototype

Request

POST
/openapi/creative-lab/keycap/v1/prototype
# Stage 1: generate a finished-keycap design render
curl https://api.meshy.ai/openapi/creative-lab/keycap/v1/prototype \
  -X POST \
  -H "Authorization: Bearer ${YOUR_API_KEY}" \
  -H 'Content-Type: application/json' \
  -d '{
    "image_url": "<your publicly accessible image url or base64-encoded data URI>"
  }'

Response

{
  "result": "019c9a4e-2b31-7f6a-8c44-5d2a87b3c1ef"
}

พารามิเตอร์

  • Name
    image_url
    Type
    string
    จำเป็น
    Description

    รูปถ่ายที่จะแปลงเป็นดีไซน์คีย์แคป โดยเป็น URL สาธารณะหรือ data URI (data:image/jpeg;base64,<ข้อมูลรูปภาพที่เข้ารหัส base64 ของคุณ>)

    รูปภาพต้องเป็นไฟล์ JPG, JPEG, PNG หรือ WebP ที่มีขนาดไม่เกิน 20 MB (20,000,000 ไบต์) แต่ละด้านต้องมีความยาวอย่างน้อย 32 พิกเซล และรูปภาพต้องมีจำนวนพิกเซลรวมไม่เกิน 178,956,970 พิกเซล รูปภาพที่เกินขีดจำกัดเหล่านี้จะคืนค่า 400

    Meshy ตรวจจับรูปแบบไฟล์จากข้อมูลของรูปภาพ ไม่ใช่จากนามสกุลไฟล์ใน URL ดังนั้น URL ที่ไม่มีนามสกุลก็ใช้งานได้ และ Meshy จะติดตาม HTTP redirects ด้วย Meshy จะใช้ค่าการหมุนภาพตามข้อมูล EXIF ดังนั้นรูปถ่ายจากโทรศัพท์ที่ถูกหมุนจะแสดงผลออกมาตามที่ควรจะเป็น

    data URI ต้องเข้ารหัสแบบ base64 พร้อมระบุชนิดของรูปภาพ เช่น data:image/png;base64,... และรูปภาพที่ถอดรหัสแล้วต้องเป็น JPEG, PNG หรือ WebP ขีดจำกัด 20 MB จะใช้กับรูปภาพที่ถอดรหัสแล้ว ดังนั้นข้อความ base64 อาจมีขนาดใหญ่กว่าประมาณหนึ่งในสาม

  • Name
    name
    Type
    string
    Description

    ชื่อสำหรับงาน ความยาวไม่เกิน 100 ตัวอักษร ชื่อนี้จะถูกส่งกลับมาในฟิลด์ name ของงาน หากชื่อยาวเกินไปจะคืนค่า 400

  • Name
    remove_background
    Type
    boolean
    ค่าเริ่มต้น false
    Description

    กำหนดว่าจะลบพื้นหลังออกจากรูปภาพ prototype หรือไม่ หากตั้งเป็น true จะได้ PNG แบบ RGBA ที่มีพื้นหลังโปร่งใส ทำให้คุณสามารถวางคีย์แคปบนพื้นหลังใดก็ได้ หาก Meshy ไม่สามารถลบพื้นหลังได้ ก็จะคืนค่ารูปภาพพร้อมพื้นหลังเดิม และงานยังคงสำเร็จ

    ดีไซน์หัวคีย์แคปที่ขั้นตอน build จะแปลงเป็น 3D จะยังคงพื้นหลังไว้ไม่ว่าจะเลือกตัวเลือกใดก็ตาม ขั้นตอน build จะวาดฐานคีย์แคปจากรูปภาพใน image_urls โดยไม่มีพื้นหลังหากคุณส่งค่า true ขั้นตอน build จะคืนค่ารูปภาพนั้นเป็น process_image_urls.composite

ค่าที่ส่งกลับ

ฟิลด์ result จะเก็บ ID ของงาน prototype ใหม่ ดึงข้อมูลงาน หรือ สตรีมงานนั้น จนกว่า status จะเป็น SUCCEEDED จากนั้นส่ง ID ไปยัง เอนด์พอยต์ build เป็น input_task_id พร้อมกับรายการจาก candidate_ids เป็น candidate_id

รูปแบบความล้มเหลว

  • Name
    400 - Bad Request
    Description

    คำขอนี้ไม่สามารถดำเนินการได้ ตัวอย่างเช่น:

    • image_url ขาดหายไป
    • รูปภาพไม่ใช่ไฟล์ JPG, JPEG, PNG หรือ WebP
    • รูปภาพมีขนาดเล็กเกินไป ใหญ่เกิน 20 MB หรือเกินจำนวนพิกเซลสูงสุด
    • Meshy ไม่สามารถดาวน์โหลดรูปภาพได้ เช่น เพราะ URL คืนค่า 404 หรือหมดเวลา
    • data URI ไม่ใช่ base64 ที่ถูกต้อง
    • รูปภาพถูกตั้งค่าสถานะว่าเป็น NSFW

    ข้อความแสดงข้อผิดพลาดจะระบุปัญหาที่พบ แก้ไขคำขอแล้วส่งอีกครั้ง

  • Name
    401 - Unauthorized
    Description

    API คีย์ขาดหายไปหรือไม่ถูกต้อง ตรวจสอบเฮดเดอร์ Authorization

  • Name
    402 - Payment Required
    Description

    เงื่อนไขข้อใดข้อหนึ่งต่อไปนี้เป็นจริง:

    • บัญชีมีเครดิตไม่เพียงพอ
    • API คีย์ พื้นที่ทำงาน หรือสมาชิกถึงขีดจำกัดเครดิตแล้ว
    • บัญชีอยู่ในแพ็กเกจฟรี

    ตรวจสอบเครดิตและแพ็กเกจของคุณ แล้วลองใหม่อีกครั้ง

  • Name
    403 - Forbidden
    Description

    ทีมของคุณเปิดใช้งาน IP Filtering และ Meshy ตั้งค่าสถานะรูปภาพนี้ว่าเกี่ยวข้องกับทรัพย์สินทางปัญญาของบุคคลที่สาม โปรดส่งรูปภาพที่คุณมีสิทธิ์ใช้งาน

  • Name
    429 - Too Many Requests
    Description

    ถึงขีดจำกัดข้อใดข้อหนึ่งต่อไปนี้:

    • คุณส่งคำขอมากกว่าที่การจำกัดอัตราของคุณอนุญาต รอสักสองสามวินาทีแล้วลองใหม่ ดู การจำกัดอัตรา
    • มีงานของคุณที่อยู่ในคิวหรือกำลังทำงานมากเกินไป รอจนกว่าบางงานจะเสร็จสิ้น แล้วลองใหม่
    • หากอยู่ในแพ็กเกจไม่จำกัดแต่ไม่มีเครดิตเหลือ คุณถึงจำนวนงานสูงสุดที่สามารถรันได้โดยไม่ใช้เครดิตแล้ว รอให้งานเสร็จสิ้น หรือเพิ่มเครดิต
  • Name
    500 - Internal Server Error
    Description

    Meshy พบข้อผิดพลาดของเซิร์ฟเวอร์ เช่น เนื่องจากการตรวจสอบเนื้อหา (moderation) หรือการตรวจสอบทรัพย์สินทางปัญญาไม่สามารถทำงานได้ หรือ Meshy ไม่สามารถจัดเก็บรูปภาพได้ Meshy จะไม่สร้างงาน และคืนเครดิตที่เรียกเก็บไปแล้ว ลองส่งคำขออีกครั้ง


POST/openapi/creative-lab/keycap/v1/build

สร้างงาน Keycap build

สร้างคีย์แคป 3D ที่มีพื้นผิวแล้วจากงาน prototype ที่สำเร็จ และหนึ่งในตัวเลือก (candidate) ของมัน สำหรับฟิลด์ต่าง ๆ ดูที่ The Keycap build task object

งาน build หนึ่งงานจะรันทุกขั้นตอน โดยจะสร้างโมเดล 3D จากดีไซน์และใส่สีให้ จากนั้นจะวางและตัดโมเดลลงบนฐานคีย์แคปในท่าตั้งค่าเริ่มต้นที่ผ่านการปรับเทียบแล้ว ดังนั้นคุณจึงไม่จำเป็นต้องปรับเองด้วยมือ สุดท้ายจะประกอบคีย์แคปและส่งออกไฟล์ โดยทั่วไปการ build จะใช้เวลา 3 ถึง 7 นาที

Request

POST
/openapi/creative-lab/keycap/v1/build
# Stage 2: build the chosen candidate into a 3D keycap
curl https://api.meshy.ai/openapi/creative-lab/keycap/v1/build \
  -X POST \
  -H "Authorization: Bearer ${YOUR_API_KEY}" \
  -H 'Content-Type: application/json' \
  -d '{
    "input_task_id": "019c9a4e-2b31-7f6a-8c44-5d2a87b3c1ef",
    "candidate_id": "0198c1b2-33f5-7d21-a4b7-8e9d0c1f2a3b",
    "options": {
      "base_model": "cherry-mx-1x1-r1",
      "head_size_mm": 23,
      "vertical_offset_mm": 0
    }
  }'

Response

{
  "result": "019c9a52-7d18-7e2b-9f01-6e3b98c4d2af"
}

พารามิเตอร์

  • Name
    input_task_id
    Type
    string
    จำเป็น
    Description

    ID ของงาน prototype ที่จะ build ต้องเป็นงานที่บัญชีของคุณสร้างด้วย POST /openapi/creative-lab/keycap/v1/prototype และมีสถานะ SUCCEEDED

    ID ที่ไม่ใช่งาน Keycap prototype ของคุณจาก API จะคืนค่า 404 รวมถึง prototype ที่สร้างในเว็บแอปด้วย

  • Name
    candidate_id
    Type
    string
    จำเป็น
    Description

    ตัวเลือก (candidate) ที่จะ build จากอาร์เรย์ candidate_ids ของงาน prototype หากส่งค่าที่ไม่อยู่ในอาร์เรย์นั้นจะคืนค่า 400

  • Name
    name
    Type
    string
    Description

    ชื่อสำหรับงาน ความยาวไม่เกิน 100 ตัวอักษร ชื่อนี้จะกลับมาในฟิลด์ name ของงาน หากชื่อยาวเกินไปจะคืนค่า 400

options

การตั้งค่าจีออเมทรีที่เป็นทางเลือก แต่ละฟิลด์มีค่าเริ่มต้นที่ผ่านการปรับเทียบแล้ว ดังนั้นให้ส่งเฉพาะฟิลด์ที่คุณต้องการเปลี่ยนแปลง

  • Name
    base_model
    Type
    string
    ค่าเริ่มต้น cherry-mx-1x1-r1
    Description

    ฐานคีย์แคปที่จะใช้ build ค่าเดียวที่ใช้ได้คือ cherry-mx-1x1-r1 ซึ่งเป็นคีย์แคปโปรไฟล์ Cherry MX มาตรฐานขนาด 1u ไม่รองรับขนาดที่กำหนดเอง และค่าอื่นใดจะคืนค่า 400

  • Name
    head_size_mm
    Type
    number
    ค่าเริ่มต้น 23
    Description

    ขนาดของส่วนหัวตามมิติที่ยาวที่สุด หน่วยเป็นมิลลิเมตร ตั้งแต่ 10 ถึง 40 Meshy จะปรับขนาดส่วนหัวให้มิติที่ยาวที่สุดตรงกับค่านี้ หากค่าอยู่นอกช่วงจะคืนค่า 400

    สำหรับค่าที่สูงกว่าประมาณ 32.9 Meshy อาจลดขนาดลงเพื่อให้ส่วนหัวพอดีกับขีดจำกัดพื้นที่ป้องกันของฐาน ส่วนหัวจึงอาจออกมาเล็กกว่าที่คุณขอ ออบเจ็กต์งานไม่ได้คืนค่าขนาดที่ Meshy ใช้จริง หากต้องการตรวจสอบ ให้วัดกล่องขอบเขตของเมช keycap-head ในโมเดล

  • Name
    vertical_offset_mm
    Type
    number
    ค่าเริ่มต้น 0
    Description

    ค่าชดเชยแนวตั้งที่จะใช้กับส่วนหัวก่อนที่ Meshy จะวางลงบนฐาน หน่วยเป็นมิลลิเมตร ตั้งแต่ -5 ถึง 5 หากค่าอยู่นอกช่วงจะคืนค่า 400

ค่าที่ส่งกลับ

ฟิลด์ result เก็บ ID ของงาน build ใหม่ ดึงข้อมูลงาน หรือ สตรีมข้อมูลงาน จนกว่า status จะเป็น SUCCEEDED จากนั้นดาวน์โหลดไฟล์ GLB จาก model_urls.glb และชุดไฟล์ OBJ จาก model_urls.obj_zip

รูปแบบความล้มเหลว

  • Name
    400 - Bad Request
    Description

    คำขอนี้ไม่สามารถรันได้ ตัวอย่างเช่น:

    • input_task_id ขาดหายไป หรือไม่ใช่ ID งานที่ถูกต้อง
    • สถานะของงาน prototype ไม่ใช่ SUCCEEDED หากยังทำงานอยู่ ให้รอแล้วลองใหม่ หากล้มเหลวหรือถูกยกเลิก ให้สร้างงาน prototype ใหม่
    • งาน prototype สำเร็จแล้วแต่ไม่มีตัวเลือก (candidate) ให้สร้างงาน prototype ใหม่
    • candidate_id ขาดหายไป หรือไม่อยู่ใน candidate_ids ของงาน prototype ให้ส่งค่าจากอาร์เรย์นั้น
    • ฟิลด์ใน options อยู่นอกช่วงที่กำหนด หรือ base_model ไม่ใช่ค่าที่ใช้ได้

    ข้อความแสดงข้อผิดพลาดจะระบุว่าพบปัญหาใด

  • Name
    401 - Unauthorized
    Description

    API คีย์ขาดหายไปหรือไม่ถูกต้อง ตรวจสอบเฮดเดอร์ Authorization

  • Name
    402 - Payment Required
    Description

    เกิดกรณีใดกรณีหนึ่งต่อไปนี้:

    • บัญชีมีเครดิตไม่เพียงพอ
    • API คีย์ พื้นที่ทำงาน หรือสมาชิกถึงขีดจำกัดเครดิตแล้ว
    • บัญชีอยู่ในแผนฟรี

    ตรวจสอบเครดิตและแผนของคุณ แล้วลองใหม่

  • Name
    404 - Not Found
    Description

    input_task_id ไม่ตรงกับงาน Keycap prototype ใด ๆ ที่บัญชีของคุณสร้างด้วย API prototype ที่สร้างในเว็บแอปก็จะคืนค่า 404 เช่นกัน ให้ส่ง ID จากฟิลด์ result ของการตอบกลับตอนสร้าง prototype

  • Name
    429 - Too Many Requests
    Description

    มีการถึงขีดจำกัดข้อใดข้อหนึ่งต่อไปนี้:

    • คุณส่งคำขอมากกว่าที่การจำกัดอัตราของคุณอนุญาต ให้รอสักสองสามวินาทีแล้วลองใหม่ ดู Rate limits
    • มีงานของคุณที่อยู่ในคิวหรือกำลังทำงานมากเกินไป ให้รอให้บางงานเสร็จก่อน แล้วลองใหม่
    • หากอยู่ในแผนไม่จำกัดและไม่มีเครดิตเหลือ คุณถึงจำนวนงานสูงสุดที่รันได้โดยไม่ใช้เครดิตแล้ว ให้รอให้งานหนึ่งเสร็จ หรือเติมเครดิต
  • Name
    500 - Internal Server Error
    Description

    Meshy ไม่สามารถสร้างงานได้เนื่องจากข้อผิดพลาดของเซิร์ฟเวอร์ Meshy จะคืนเครดิตที่เรียกเก็บไปแล้ว ให้ลองส่งคำขอใหม่


GET/openapi/creative-lab/keycap/v1/(prototype|build)/:id

ดึงข้อมูลงาน Keycap

คืนค่างาน prototype หรือ build พาธที่ใช้ต้องตรงกับขั้นตอนของงานนั้น งาน build ที่ถูกเรียกผ่าน /prototype/:id จะคืนค่า 404 และงาน prototype ที่ถูกเรียกผ่าน /build/:id ก็เช่นกัน

สำหรับฟิลด์ต่าง ๆ ดูที่ The Keycap prototype task object และ The Keycap build task object

Request

GET
/openapi/creative-lab/keycap/v1/prototype/019c9a4e-2b31-7f6a-8c44-5d2a87b3c1ef
# Prototype
curl https://api.meshy.ai/openapi/creative-lab/keycap/v1/prototype/019c9a4e-2b31-7f6a-8c44-5d2a87b3c1ef \
  -H "Authorization: Bearer ${YOUR_API_KEY}"

# Build
curl https://api.meshy.ai/openapi/creative-lab/keycap/v1/build/019c9a52-7d18-7e2b-9f01-6e3b98c4d2af \
  -H "Authorization: Bearer ${YOUR_API_KEY}"

Prototype Response

{
  "id": "019c9a4e-2b31-7f6a-8c44-5d2a87b3c1ef",
  "type": "creative-lab-keycap-prototype",
  "name": "",
  "status": "SUCCEEDED",
  "progress": 100,
  "created_at": 1753142456000,
  "started_at": 1753142460000,
  "finished_at": 1753142516000,
  "expires_at": 1753401716000,
  "preceding_tasks": 0,
  "task_error": null,
  "consumed_credits": 12,
  "image_urls": [
    "https://assets.meshy.ai/***/design-1.png?Expires=***"
  ],
  "candidate_ids": [
    "0198c1b2-33f5-7d21-a4b7-8e9d0c1f2a3b"
  ]
}

Build Response

{
  "id": "019c9a52-7d18-7e2b-9f01-6e3b98c4d2af",
  "type": "creative-lab-keycap-build",
  "name": "",
  "status": "SUCCEEDED",
  "progress": 100,
  "created_at": 1753142600000,
  "started_at": 1753142610000,
  "finished_at": 1753143050000,
  "expires_at": 1753402250000,
  "preceding_tasks": 0,
  "task_error": null,
  "consumed_credits": 50,
  "model_urls": {
    "glb": "https://assets.meshy.ai/***/tasks/019c9a52-7d18-7e2b-9f01-6e3b98c4d2af/output/model.glb?Expires=***",
    "obj_zip": "https://assets.meshy.ai/***/tasks/019c9a52-7d18-7e2b-9f01-6e3b98c4d2af/output/model-obj.zip?Expires=***"
  },
  "process_image_urls": {
    "head_design": "https://assets.meshy.ai/***/head-design.png?Expires=***",
    "composite": "https://assets.meshy.ai/***/composite.png?Expires=***",
    "base_canvas": "https://assets.meshy.ai/***/base-canvas.png?Expires=***"
  }
}

พารามิเตอร์

  • Name
    id
    Type
    path
    Description

    รหัสงานจากการตอบกลับตอนสร้าง

ค่าที่ส่งกลับ

prototype task object หรือ build task object ขึ้นอยู่กับขั้นตอนที่ตรงกับพาธ

รูปแบบความล้มเหลว

  • Name
    400 - Bad Request
    Description

    id ไม่ใช่รหัสงานที่ถูกต้อง ให้ส่งรหัส result จากการตอบกลับตอนสร้าง

  • Name
    403 - Forbidden
    Description

    ทีมของคุณเปิดใช้งาน IP Filtering และ Meshy ตรวจพบว่ารูปภาพของงาน prototype มีทรัพย์สินทางปัญญาของบุคคลภายนอก งาน build และทีมในแพ็กเกจ Enterprise จะไม่พบข้อผิดพลาดนี้ ให้สร้างงาน prototype จากรูปภาพที่คุณมีสิทธิ์ใช้งาน

  • Name
    404 - Not Found
    Description

    ไม่มีงาน Keycap ที่มีรหัสนี้อยู่ในบัญชีของคุณ หรือขั้นตอนของงานไม่ตรงกับพาธ ตรวจสอบรหัส และใช้ /prototype หรือ /build ให้ตรงกับงาน

  • Name
    500 - Internal Server Error
    Description

    Meshy ไม่สามารถตรวจสอบทรัพย์สินทางปัญญาได้ ให้ลองส่งคำขออีกครั้ง


DELETE/openapi/creative-lab/keycap/v1/(prototype|build)/:id

ลบงาน Keycap

ยกเลิกงาน Keycap การยกเลิกงานที่มีสถานะ PENDING จะคืนเครดิตให้ ส่วนงานที่มีสถานะ IN_PROGRESS จะถูกยกเลิกโดยไม่มีการคืนเครดิต เนื่องจาก worker ได้เริ่มดำเนินการไปแล้ว งานที่มีสถานะสุดท้าย (SUCCEEDED, FAILED หรือ CANCELED) ไม่สามารถยกเลิกได้

พาธต้องตรงกับขั้นตอนของงาน ตัวอย่างเช่น การใช้ DELETE บน /prototype/:id ด้วยรหัสของ build task จะส่งคืน 404

Request

DELETE
/openapi/creative-lab/keycap/v1/prototype/019c9a4e-2b31-7f6a-8c44-5d2a87b3c1ef
curl --request DELETE \
  --url https://api.meshy.ai/openapi/creative-lab/keycap/v1/prototype/019c9a4e-2b31-7f6a-8c44-5d2a87b3c1ef \
  -H "Authorization: Bearer ${YOUR_API_KEY}"

Response

// Returns 204 No Content on success (empty body).

พารามิเตอร์พาธ

  • Name
    id
    Type
    path
    Description

    รหัสของงานที่จะยกเลิก

สิ่งที่ส่งคืน

204 No Content พร้อมเนื้อหาว่างเปล่า

รูปแบบความล้มเหลว

  • Name
    400 - Bad Request
    Description

    หนึ่งในกรณีต่อไปนี้ใช้ได้:

    • id ไม่ใช่รหัสงานที่ถูกต้อง ให้ส่งรหัส result จากการตอบกลับการสร้าง
    • งานมีสถานะสุดท้ายอยู่แล้ว (SUCCEEDED, FAILED หรือ CANCELED) จึงไม่สามารถยกเลิกได้
  • Name
    404 - Not Found
    Description

    ไม่มีงาน Keycap ที่มีรหัสนี้อยู่ในบัญชีของคุณ หรือขั้นตอนของงานไม่ตรงกับพาธ ตรวจสอบรหัส และใช้ /prototype หรือ /build ให้ตรงกับงาน

  • Name
    500 - Internal Server Error
    Description

    Meshy พบข้อผิดพลาดของเซิร์ฟเวอร์ขณะยกเลิกงาน ดังนั้นงานอาจถูกยกเลิกหรือไม่ก็ได้ ให้ดึงข้อมูลงานเพื่อตรวจสอบ status ก่อนลองใหม่อีกครั้ง


GET/openapi/creative-lab/keycap/v1/(prototype|build)/:id/stream

Stream a Keycap task

สตรีมการอัปเดตของ Keycap task ในรูปแบบ Server-Sent Events (SSE) พาธที่ใช้ต้องตรงกับขั้นตอนของ task นั้น หากไม่ตรงกัน เช่น ใช้ ID ของ build task ที่ /prototype/:id/stream สตรีมจะส่ง error event หนึ่งรายการที่มี status_code: 404 แล้วปิดการเชื่อมต่อ

Request

GET
/openapi/creative-lab/keycap/v1/build/019c9a52-7d18-7e2b-9f01-6e3b98c4d2af/stream
curl -N https://api.meshy.ai/openapi/creative-lab/keycap/v1/build/019c9a52-7d18-7e2b-9f01-6e3b98c4d2af/stream \
-H "Authorization: Bearer ${YOUR_API_KEY}"

Response Stream

// Error event example (wrong stage or task not found)
event: error
data: {
  "status_code": 404,
  "message": "Task not found"
}

// Message event examples illustrate task progress.
// Every frame is the full task object; fields not yet populated are null / empty.
// The following PENDING frame is shortened to the fields that change.
event: message
data: {
  "id": "019c9a52-7d18-7e2b-9f01-6e3b98c4d2af",
  "progress": 0,
  "status": "PENDING"
}

event: message
data: {
  "id": "019c9a52-7d18-7e2b-9f01-6e3b98c4d2af",
  "type": "creative-lab-keycap-build",
  "status": "SUCCEEDED",
  "progress": 100,
  "created_at": 1753142600000,
  "started_at": 1753142610000,
  "finished_at": 1753143050000,
  "expires_at": 1753402250000,
  "task_error": null,
  "consumed_credits": 50,
  "model_urls": {
    "glb": "https://assets.meshy.ai/***/tasks/019c9a52-7d18-7e2b-9f01-6e3b98c4d2af/output/model.glb?Expires=***",
    "obj_zip": "https://assets.meshy.ai/***/tasks/019c9a52-7d18-7e2b-9f01-6e3b98c4d2af/output/model-obj.zip?Expires=***"
  },
  "process_image_urls": {
    "head_design": "https://assets.meshy.ai/***/head-design.png?Expires=***"
  }
}

พารามิเตอร์

  • Name
    id
    Type
    path
    Description

    ID ของ task จากการตอบกลับตอนสร้าง

ค่าที่ส่งกลับ

สตรีมของอ็อบเจ็กต์ task ประเภท prototype หรือ build แต่ละ event จะมีอ็อบเจ็กต์ task ฉบับเต็ม เช่นเดียวกับที่ Get a Keycap task ส่งกลับ ในขณะที่ task ยังอยู่ในสถานะ PENDING หรือ IN_PROGRESS ฟิลด์ผลลัพธ์ของมันจะยังว่างอยู่ (null, [], หรือ {}) และ finished_at จะเป็น null

หาก IP Filtering บล็อกผลลัพธ์ของ task สตรีมจะส่ง error event หนึ่งรายการแทน โดยมี status_code เดียวกับที่ get request จะส่งกลับ


GET/openapi/creative-lab/keycap/v1/(prototype|build)

List Keycap tasks

คืนค่างาน Keycap ของคุณในขั้นตอนเดียว ครั้งละหนึ่งหน้า พาธจะเป็นตัวเลือกขั้นตอน /prototype จะคืนค่าเฉพาะงาน prototype และ /build จะคืนค่าเฉพาะงาน build

Request

GET
/openapi/creative-lab/keycap/v1/prototype
# List prototype tasks
curl https://api.meshy.ai/openapi/creative-lab/keycap/v1/prototype?page_size=10 \
  -H "Authorization: Bearer ${YOUR_API_KEY}"

# List build tasks
curl https://api.meshy.ai/openapi/creative-lab/keycap/v1/build?page_size=10 \
  -H "Authorization: Bearer ${YOUR_API_KEY}"

Response (List Prototype Tasks)

[
  {
    "id": "019c9a4e-2b31-7f6a-8c44-5d2a87b3c1ef",
    "type": "creative-lab-keycap-prototype",
    "name": "",
    "status": "SUCCEEDED",
    "progress": 100,
    "created_at": 1753142456000,
    "started_at": 1753142460000,
    "finished_at": 1753142516000,
    "expires_at": 1753401716000,
    "preceding_tasks": 0,
    "task_error": null,
    "consumed_credits": 12,
    "image_urls": [
      "https://assets.meshy.ai/***/design-1.png?Expires=***"
    ],
    "candidate_ids": [
      "0198c1b2-33f5-7d21-a4b7-8e9d0c1f2a3b"
    ]
  }
]

พารามิเตอร์พาธ (Path parameters)

  • Name
    stage
    Type
    path
    จำเป็น
    Description

    ขั้นตอนที่จะแสดงรายการ prototype หรือ build

พารามิเตอร์คิวรี (Query parameters)

  • Name
    page_num
    Type
    integer
    ค่าเริ่มต้น 1
    Description

    หน้าที่จะคืนค่า ตั้งแต่ 1 ถึง 255 ค่า 0 จะคืนค่าหน้าแรก ค่าที่อยู่นอกช่วง 0 ถึง 255 จะคืนค่า 400

  • Name
    page_size
    Type
    integer
    ค่าเริ่มต้น 10
    Description

    จำนวนงานต่อหน้า สูงสุด 100 ค่าตั้งแต่ 101 ถึง 255 จะคืนค่า 100 งาน และ 0 จะใช้ค่าเริ่มต้น ค่าที่อยู่นอกช่วง 0 ถึง 255 จะคืนค่า 400

  • Name
    sort_by
    Type
    string
    ค่าเริ่มต้น -created_at
    Description

    ลำดับการเรียง

    ค่าที่ใช้ได้:

    • -created_at: ใหม่ที่สุดก่อน
    • +created_at: เก่าที่สุดก่อน

ค่าที่คืนกลับ

อาร์เรย์ของ ออบเจ็กต์งาน prototype หรือ ออบเจ็กต์งาน build ตามขั้นตอนที่ระบุในพาธ


อ็อบเจ็กต์งาน Keycap Prototype

งาน prototype จะสร้างภาพของคีย์แคปที่เสร็จสมบูรณ์หนึ่งภาพจากรูปถ่าย หากต้องการเปลี่ยนดีไซน์ให้เป็นคีย์แคป 3 มิติ ให้ส่ง ID ของงานไปยัง build endpoint ในฐานะ input_task_id พร้อมกับรายการ candidate_ids ในฐานะ candidate_id

Properties

Example Keycap Prototype Task Object

{
  "id": "019c9a4e-2b31-7f6a-8c44-5d2a87b3c1ef",
  "type": "creative-lab-keycap-prototype",
  "name": "",
  "status": "SUCCEEDED",
  "progress": 100,
  "created_at": 1753142456000,
  "started_at": 1753142460000,
  "finished_at": 1753142516000,
  "expires_at": 1753401716000,
  "preceding_tasks": 0,
  "task_error": null,
  "consumed_credits": 12,
  "image_urls": [
    "https://assets.meshy.ai/***/design-1.png?Expires=***"
  ],
  "candidate_ids": [
    "0198c1b2-33f5-7d21-a4b7-8e9d0c1f2a3b"
  ]
}
  • Name
    id
    Type
    string
    Description

    ID ของงาน อย่าอ้างอิงรูปแบบของ ID นี้

  • Name
    type
    Type
    string
    Description

    ประเภทของงาน ซึ่งจะเป็น creative-lab-keycap-prototype เสมอ

  • Name
    name
    Type
    string
    Description

    name ที่คุณส่งไปตอนสร้างงาน หรือ "" หากคุณไม่ได้ส่งค่านี้ไป

  • Name
    status
    Type
    string
    Description

    สถานะของงาน

    ค่าที่เป็นไปได้:

    • PENDING: กำลังรอในคิว
    • IN_PROGRESS: กำลังสร้างผลลัพธ์
    • SUCCEEDED: เสร็จสิ้นแล้ว พร้อมผลลัพธ์
    • FAILED: เสร็จสิ้นแล้ว พร้อมเหตุผลใน task_error
    • CANCELED: เสร็จสิ้นแล้ว โดยไม่มีผลลัพธ์
  • Name
    progress
    Type
    integer
    Description

    เปอร์เซ็นต์ของงานที่เสร็จแล้ว ตั้งแต่ 0 ถึง 100 ค่าจะเป็น 0 จนกว่างานจะเริ่มต้น และเป็น 100 เมื่อสำเร็จ

  • Name
    created_at
    Type
    timestamp
    Description

    เวลาที่สร้างงาน ในหน่วยมิลลิวินาทีแบบ Unix

  • Name
    started_at
    Type
    timestamp
    Description

    เวลาที่ worker เริ่มดำเนินงาน ในหน่วยมิลลิวินาทีแบบ Unix เป็น null จนกว่าจะถึงเวลานั้น

  • Name
    finished_at
    Type
    timestamp
    Description

    เวลาที่งานถึงสถานะสุดท้าย ในหน่วยมิลลิวินาทีแบบ Unix เป็น null จนกว่าจะถึงเวลานั้น

  • Name
    expires_at
    Type
    timestamp
    Description

    เวลาที่ผลลัพธ์ของงานจะหมดอายุ ในหน่วยมิลลิวินาทีแบบ Unix คือ 3 วันหลังจาก finished_at หรือ 3 วันหลังจาก created_at จนกว่างานจะเสร็จสิ้น สำหรับบัญชี Enterprise ซึ่งเก็บผลลัพธ์ API ไว้อย่างไม่มีกำหนด ค่านี้จะอยู่ที่ประมาณ 100 ปีถัดไป ดู Asset retention

  • Name
    preceding_tasks
    Type
    integer
    Description

    ตำแหน่งของงานในคิวขณะที่ยังเป็น PENDING ค่า 1 หมายความว่างานนี้จะเริ่มเป็นลำดับถัดไป และเป็น 0 หลังจากงานออกจากคิวแล้ว และจะเป็น 0 เสมอในผลลัพธ์แบบรายการ

  • Name
    task_error
    Type
    object
    Description

    เหตุผลที่งานล้มเหลว เป็น null ยกเว้นเมื่อ status เป็น FAILED ดู ข้อผิดพลาด

  • Name
    consumed_credits
    Type
    integer
    Description

    จำนวนเครดิตที่ถูกเรียกเก็บสำหรับงานนี้ หลังหักการคืนเงินใด ๆ คำขอที่ส่งคืนข้อผิดพลาดจะไม่ถูกเรียกเก็บเครดิต งานที่ล้มเหลวจะเป็น 0 เนื่องจาก Meshy จะคืนเครดิตให้ การยกเลิกจะคืนเครดิตของงานก็ต่อเมื่องานยังอยู่ในสถานะ PENDING หาก Meshy ไม่สามารถอ่านค่าธรรมเนียมได้ การตอบกลับแบบ get หรือ stream จะไม่มีฟิลด์นี้ปรากฏ และคำขอแบบ list จะส่งคืน 500 ในกรณีนั้น ให้ลองส่งคำขอใหม่อีกครั้ง

  • Name
    image_urls
    Type
    array of strings
    Description

    URL สำหรับดาวน์โหลดภาพของคีย์แคปที่เสร็จสมบูรณ์ อาร์เรย์นี้จะมี URL หนึ่งรายการหลังจากงานสำเร็จ และจะว่างเปล่าจนกว่าจะถึงเวลานั้น image_urls[i] แสดงตัวเลือกที่สอดคล้องกับ candidate_ids[i]

    URL เหล่านี้มีไว้สำหรับการแสดงผล ส่วน build endpoint จะรับรายการจาก candidate_ids ไม่ใช่ URL URL เหล่านี้เป็น signed URL ดังนั้นให้ดึงข้อมูลโดยไม่ต้องใส่ส่วนหัว Authorization URL จะใช้งานได้จนถึง expires_at และการเรียกดูงานอีกครั้งจะส่งคืน URL เดิม

  • Name
    candidate_ids
    Type
    array of strings
    Description

    ID ของตัวเลือกต่าง ๆ โดยเรียงลำดับเดียวกับ image_urls หากต้องการสร้างตัวเลือกใดตัวเลือกหนึ่ง ให้ส่ง ID ของตัวเลือกนั้นเป็น candidate_id ในคำขอ build อย่าอ้างอิงรูปแบบของ ID เหล่านี้


The Keycap build task object

งาน build จะสร้างคีย์แคป 3D ที่มีเท็กซ์เจอร์จากงาน prototype ที่สำเร็จและหนึ่งในตัวเลือกของมัน โดยจะสร้างโมเดล 3D ลงสี วางและตัดลงบนฐานคีย์แคป แล้วจึงประกอบและส่งออกคีย์แคป

คุณสมบัติ

Example Keycap Build Task Object

{
  "id": "019c9a52-7d18-7e2b-9f01-6e3b98c4d2af",
  "type": "creative-lab-keycap-build",
  "name": "",
  "status": "SUCCEEDED",
  "progress": 100,
  "created_at": 1753142600000,
  "started_at": 1753142610000,
  "finished_at": 1753143050000,
  "expires_at": 1753402250000,
  "preceding_tasks": 0,
  "task_error": null,
  "consumed_credits": 50,
  "model_urls": {
    "glb": "https://assets.meshy.ai/***/tasks/019c9a52-7d18-7e2b-9f01-6e3b98c4d2af/output/model.glb?Expires=***",
    "obj_zip": "https://assets.meshy.ai/***/tasks/019c9a52-7d18-7e2b-9f01-6e3b98c4d2af/output/model-obj.zip?Expires=***"
  },
  "process_image_urls": {
    "head_design": "https://assets.meshy.ai/***/head-design.png?Expires=***",
    "composite": "https://assets.meshy.ai/***/composite.png?Expires=***",
    "base_canvas": "https://assets.meshy.ai/***/base-canvas.png?Expires=***"
  }
}
  • Name
    id
    Type
    string
    Description

    ID ของงาน อย่าอ้างอิงตามรูปแบบของมัน

  • Name
    type
    Type
    string
    Description

    ประเภทของงาน ซึ่งเป็น creative-lab-keycap-build เสมอ

  • Name
    name
    Type
    string
    Description

    name ที่คุณส่งมาตอนสร้างงาน หรือ "" ถ้าคุณไม่ได้ส่งมา

  • Name
    status
    Type
    string
    Description

    สถานะของงาน

    ค่าที่เป็นไปได้:

    • PENDING: กำลังรอในคิว
    • IN_PROGRESS: กำลังสร้างผลลัพธ์
    • SUCCEEDED: สถานะสุดท้าย พร้อมผลลัพธ์
    • FAILED: สถานะสุดท้าย พร้อมเหตุผลใน task_error
    • CANCELED: สถานะสุดท้าย โดยไม่มีผลลัพธ์
  • Name
    progress
    Type
    integer
    Description

    เปอร์เซ็นต์ของงานที่ทำเสร็จแล้ว ตั้งแต่ 0 ถึง 100 ค่าเป็น 0 จนกว่างานจะเริ่ม และเป็น 100 เมื่องานสำเร็จ

  • Name
    created_at
    Type
    timestamp
    Description

    เวลาที่สร้างงาน หน่วยเป็นมิลลิวินาทีแบบ Unix

  • Name
    started_at
    Type
    timestamp
    Description

    เวลาที่ worker เริ่มงาน หน่วยเป็นมิลลิวินาทีแบบ Unix เป็น null จนกว่าจะถึงเวลานั้น

  • Name
    finished_at
    Type
    timestamp
    Description

    เวลาที่งานถึงสถานะสุดท้าย หน่วยเป็นมิลลิวินาทีแบบ Unix เป็น null จนกว่าจะถึงเวลานั้น

  • Name
    expires_at
    Type
    timestamp
    Description

    เวลาที่ผลลัพธ์ของงานจะหมดอายุ หน่วยเป็นมิลลิวินาทีแบบ Unix คือ 3 วันหลังจาก finished_at หรือ 3 วันหลังจาก created_at จนกว่างานจะเสร็จสิ้น สำหรับบัญชี Enterprise ซึ่งเก็บผลลัพธ์ของ API ไว้ไม่มีกำหนด ค่านี้จะเป็นประมาณ 100 ปีถัดไป ดู การเก็บรักษาแอสเซ็ต

  • Name
    preceding_tasks
    Type
    integer
    Description

    ตำแหน่งของงานในคิวขณะที่สถานะเป็น PENDING ค่า 1 หมายความว่างานนี้จะเริ่มถัดไป ค่าเป็น 0 หลังจากงานออกจากคิว และเป็น 0 เสมอในผลลัพธ์แบบรายการ

  • Name
    task_error
    Type
    object
    Description

    เหตุผลที่งานล้มเหลว เป็น null เว้นแต่ status จะเป็น FAILED ดู ข้อผิดพลาด

  • Name
    consumed_credits
    Type
    integer
    Description

    จำนวนเครดิตที่เรียกเก็บสำหรับงาน หลังจากการคืนเงินใดๆ คำขอที่ส่งคืนข้อผิดพลาดจะไม่ถูกเรียกเก็บเครดิต งานที่ล้มเหลวจะมีค่าเป็น 0 เพราะ Meshy จะคืนเครดิตให้ การยกเลิกจะคืนเครดิตให้เฉพาะเมื่องานยังอยู่ในสถานะ PENDING เท่านั้น ถ้า Meshy ไม่สามารถอ่านค่าการเรียกเก็บเงินได้ การตอบกลับแบบ get หรือ stream จะไม่มีฟิลด์นี้ และคำขอแบบ list จะส่งคืน 500 ในกรณีนั้น ให้ลองคำขอใหม่อีกครั้ง

  • Name
    model_urls
    Type
    object
    Description

    URL สำหรับดาวน์โหลดโมเดล 3D Meshy จะส่งออกไฟล์ GLB และชุด OBJ ตามสเกลจริงเป็นมิลลิเมตร แกน Y ชี้ขึ้น โดยด้านหน้าของคีย์แคปหันไปทาง +Z

    โมเดลมีเมชชื่อ keycap-head และ keycap-base เมื่อ Meshy ใช้การเติมลวดลายสำรองสำหรับฐาน โมเดลจะมีเมช keycap-base-interior สำหรับโพรงก้าน(stem cavity) เพิ่มเติมด้วย อย่าสันนิษฐานว่าโมเดลมีเมชเพียงสองชิ้นเสมอไป

    URL เหล่านี้เป็น URL ที่มีการเซ็นชื่อ ดังนั้นให้เรียกใช้โดยไม่ต้องใส่ header Authorization URL เหล่านี้จะใช้งานได้จนถึง expires_at และการเรียกดูงานอีกครั้งก่อนถึงเวลานั้นจะส่งคืน URL เดิม URL ที่หมดอายุแล้วไม่สามารถต่ออายุได้ ดังนั้นควรดาวน์โหลดและจัดเก็บไฟล์ไว้ก่อนที่จะหมดอายุ

    • Name
      glb
      Type
      string
      Description

      URL สำหรับดาวน์โหลดไฟล์ GLB ที่มีเท็กซ์เจอร์ model.glb

    • Name
      obj_zip
      Type
      string
      Description

      URL สำหรับดาวน์โหลดชุด OBJ ซึ่งเป็นไฟล์ ZIP ที่มี model.obj, model.mtl และเท็กซ์เจอร์ PNG ที่ model.mtl ใช้งาน เมื่อฐานเป็นสีพื้นเดียว เท็กซ์เจอร์เดียวที่มีคือ keycap-head.png เมื่อฐานมีลวดลาย ไฟล์ ZIP จะมี keycap-base.png ด้วย

  • Name
    process_image_urls
    Type
    object
    Description

    URL สำหรับดาวน์โหลดภาพจากขั้นตอนต่างๆ ของการ build โดยจัดเรียงตามประเภท เช่นเดียวกับ model_urls ภาพเหล่านี้เป็น URL ที่มีการเซ็นชื่อ ดังนั้นให้เรียกใช้โดยไม่ต้องใส่ header Authorization URL เหล่านี้จะใช้งานได้จนถึง expires_at และการเรียกดูงานอีกครั้งจะส่งคืน URL เดิม

    อาจมีคีย์เหล่านี้:

    • head_design: ภาพออกแบบของตัวเลือกที่เลือก ซึ่งการ build นำไปแปลงเป็น 3D มีอยู่เสมอ
    • composite: ภาพของคีย์แคปที่เสร็จสมบูรณ์สำหรับตัวเลือกที่เลือก ซึ่งใช้ในการวาดฐาน อาจไม่มีอยู่ก็ได้
    • base_canvas: ฐานคีย์แคปที่ถูกวาดแล้ว อาจไม่มีอยู่ก็ได้

    Meshy อาจเพิ่มคีย์ใหม่ขึ้นมาได้ ดังนั้นให้ข้ามคีย์ที่คุณไม่รู้จักไป


End-to-end example

ตัวอย่างนี้รันกระบวนการทั้งหมด:

  1. สร้างงาน prototype จากรูปภาพ
  2. ดึงข้อมูลงานจนกว่า status จะเป็น SUCCEEDED
  3. เลือกตัวเลือกจาก candidate_ids
  4. สร้างงาน build ด้วยตัวเลือกนั้น
  5. ดึงข้อมูลงาน build จนกว่า status จะเป็น SUCCEEDED
  6. ดาวน์โหลดไฟล์ GLB และชุดไฟล์ OBJ จาก model_urls

ตัวอย่างนี้เลือกตัวเลือกแรก ในแอปของคุณ ควรแสดงรายการ image_urls ให้ผู้ใช้เห็นและให้เลือกเอง ดัชนีของรูปภาพที่เลือกใน image_urls คือดัชนีของ ID ของรูปภาพนั้นใน candidate_ids

Complete flow

POST
/openapi/creative-lab/keycap/v1
#!/usr/bin/env bash
set -euo pipefail

# Requires curl and jq. Point IMAGE_PATH at a local photo, or IMAGE_URL at a public one:
#   export MESHY_API_KEY=msy_...
#   export IMAGE_PATH=./portrait.jpg          # or: export IMAGE_URL=https://...
: "${MESHY_API_KEY:?export MESHY_API_KEY first}"
if [[ -z "${IMAGE_PATH:-}" && -z "${IMAGE_URL:-}" ]]; then
  echo "export IMAGE_PATH (local file) or IMAGE_URL (public url) first" >&2
  exit 1
fi

BASE="https://api.meshy.ai/openapi/creative-lab/keycap/v1"
AUTH="Authorization: Bearer $MESHY_API_KEY"

# api METHOD URL [curl args...] -> prints the response body, non-zero on failure.
# Note we do not use -f/--fail: it discards the body, and the body is the only
# place the reason appears.
api() {
  local method=$1 url=$2 out http_code body
  shift 2
  out=$(curl --silent --show-error --max-time 60 --write-out $'\n%{http_code}' \
    -X "$method" "$url" -H "$AUTH" "$@") || return 1
  http_code=${out##*$'\n'}
  body=${out%$'\n'*}
  if ((http_code >= 400)); then
    echo "HTTP $http_code for $url: $body" >&2
    return 1
  fi
  printf '%s' "$body"
}

# Each task gets its own 40-minute budget.
poll() {
  local kind=$1 id=$2 delay=5 task_status deadline
  deadline=$(($(date +%s) + 2400))
  while :; do
    if (($(date +%s) >= deadline)); then
      echo "gave up waiting for $kind $id" >&2
      return 1
    fi
    task_status=$(api GET "$BASE/$kind/$id" | jq -r '.status')
    echo "$kind: $task_status"
    case "$task_status" in
    SUCCEEDED) return 0 ;;
    FAILED | CANCELED) return 1 ;;
    esac
    sleep "$delay"
    delay=$((delay * 2 > 30 ? 30 : delay * 2))
  done
}

# Build the request body in a file. A base64 data URI must never go on the
# command line or into an exported variable - a photo of any real size will
# exceed the OS argument limit.
BODY=$(mktemp)
trap 'rm -f "$BODY"' EXIT
if [[ -n "${IMAGE_PATH:-}" ]]; then
  # Declare the real type: the API accepts JPEG, PNG and WebP.
  case "$(printf '%s' "${IMAGE_PATH##*.}" | tr 'A-Z' 'a-z')" in
    png) MIME=image/png ;;
    webp) MIME=image/webp ;;
    *) MIME=image/jpeg ;;
  esac
  {
    printf '{"image_url":"data:%s;base64,' "$MIME"
    base64 <"$IMAGE_PATH" | tr -d '\n'
    printf '"}'
  } >"$BODY"
else
  printf '{"image_url":"%s"}' "$IMAGE_URL" >"$BODY"
fi

# 1. Create the prototype task
PROTO_ID=$(api POST "$BASE/prototype" \
  -H 'Content-Type: application/json' --data-binary @"$BODY" | jq -r '.result')

# 2. Wait for the design render
poll prototype "$PROTO_ID"

# 3. Pick a candidate (first one here; show image_urls to a user in production)
CANDIDATE_ID=$(api GET "$BASE/prototype/$PROTO_ID" | jq -r '.candidate_ids[0]')

# 4. Create the build task
jq -n --arg p "$PROTO_ID" --arg c "$CANDIDATE_ID" \
  '{input_task_id: $p, candidate_id: $c}' >"$BODY"
BUILD_ID=$(api POST "$BASE/build" \
  -H 'Content-Type: application/json' --data-binary @"$BODY" | jq -r '.result')

# 5. Wait for the model (a build usually takes 3-7 minutes)
poll build "$BUILD_ID"

# 6. Download the artifacts. These are signed URLs: no Authorization header,
#    and they stay valid for 3 days after the task finishes.
TASK=$(api GET "$BASE/build/$BUILD_ID")
curl --silent --show-error --fail --max-time 900 \
  -o keycap.glb "$(jq -r '.model_urls.glb' <<<"$TASK")"
curl --silent --show-error --fail --max-time 900 \
  -o keycap-obj.zip "$(jq -r '.model_urls.obj_zip' <<<"$TASK")"
echo "Done: keycap.glb + keycap-obj.zip"