Creative Lab — Keycap API

Turn a source photo into a full-color custom mechanical keyboard keycap in two stages: prototype generates a "finished keycap" design render from your input photo. Once you have confirmed that render, build turns it into a textured 3D keycap model in a single run — white-model generation, automatic seating and cutting on a calibrated default pose, full-model coloring, and final assembly all happen inside one build task. The two stages are linked via input_task_id plus candidate_id.

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

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

Create a Keycap Prototype Task

Generate a finished-keycap design render from the source photo. The task result carries an image_urls array (the display render of the finished keycap) and a parallel candidate_ids array; both hold a single entry. Call this endpoint again for another render if the result is not what you want — each call is billed separately. Pass the candidate_id together with the prototype task ID to the build endpoint. Refer to The Keycap Prototype Task Object for the response shape.

Parameters

  • Name
    image_url
    Type
    string
    Required
    Description

    Source photo for Meshy to turn into keycap design images. We currently support .jpg, .jpeg, .png, and .webp formats.

    The format is detected by decoding the image data, not from the URL's file extension — a URL with no extension, or one that redirects, works as long as the bytes decode to a supported format. HTTP redirects are followed. EXIF orientation is normalized, so a rotated phone photo is used the way it looks.

    Limits: at least 32 pixels on each side, at most 178,956,970 pixels in total, and at most 20,000,000 bytes once downloaded. For a data URI the limit applies to the decoded bytes, so the source file itself may be up to that size — it is the base64 text that is about a third larger, which matters for your request body, not for this limit. A data URI must declare an image/* content type and ;base64.

    There are two ways to provide the image:

    • Publicly accessible URL: A URL that is accessible from the public internet.
    • Data URI: A base64-encoded data URI of the image. Example of a data URI: data:image/jpeg;base64,<your base64-encoded image data>.
  • Name
    name
    Type
    string
    Description

    Optional task name for display purposes. Maximum 100 characters.

Returns

The result property of the response contains the task id of the newly created keycap prototype task. Poll the Get a Task endpoint or subscribe to the stream until the task reaches SUCCEEDED, then take the entry from candidate_ids and pass it, together with the task ID, to the build endpoint.

Failure Modes

  • Name
    400 - Bad Request
    Description

    The request was unacceptable. Common causes:

    • Missing parameter: image_url is required.
    • Invalid image format: The provided image_url is not a supported format (.jpg, .jpeg, .png, .webp).
    • Image dimensions out of range: The image is too small, exceeds the maximum file size, or exceeds the maximum pixel count.
    • Unreachable URL: The image_url could not be downloaded (404 or timeout).
    • Invalid Data URI: The base64 string is malformed.
    • Content flagged: The input image was flagged by NSFW moderation.
  • Name
    401 - Unauthorized
    Description

    Authentication failed. Please check your API key.

  • Name
    402 - Payment Required
    Description

    The account is on the free plan (a paid plan is required to create tasks) or has insufficient credits.

  • Name
    403 - Forbidden
    Description

    The input image was flagged by intellectual property moderation.

  • Name
    429 - Too Many Requests
    Description

    You have exceeded your rate limit.

  • Name
    500 - Internal Server Error
    Description

    An unexpected server-side error occurred — for example the content-moderation service was unavailable, staging the input image failed, or the task could not be created. No task is created in this case, so retrying is safe.

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"
}

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

Create a Keycap Build Task

Generate the final textured 3D keycap model from a succeeded prototype task and one of its candidates. A single build task runs the whole pipeline end to end — white-model generation from the chosen design, automatic seating and cutting onto the keycap base using a calibrated default pose (no interactive adjustment needed), full-model coloring, and final assembly and export. A build typically takes 3–7 minutes, toward the upper end when several builds run concurrently. Refer to The Keycap Build Task Object for the response shape.

Parameters

  • Name
    input_task_id
    Type
    string
    Required
    Description

    The task ID of a prototype task created via this same OpenAPI endpoint. The prototype must have been created by the same Meshy account, must have reached SUCCEEDED, and must have produced at least one candidate.

    Prototype tasks created through the webapp are not accepted — the build endpoint accepts only prototype tasks produced by POST /openapi/creative-lab/keycap/v1/prototype and refuses any other source with 404.

  • Name
    candidate_id
    Type
    string
    Required
    Description

    The candidate to build, taken from the candidate_ids array of the succeeded prototype task. Must belong to that task; any other value is rejected with 400.

  • Name
    name
    Type
    string
    Description

    Optional task name for display purposes. Maximum 100 characters.

options

Optional geometry tuning. Every field has a calibrated default — send only the ones you want to override.

  • Name
    base_model
    Type
    string
    default cherry-mx-1x1-r1
    Description

    The keycap base to build on. Currently the only available value is cherry-mx-1x1-r1 — a standard Cherry MX profile 1u keycap. 3–5 additional mainstream standard sizes are planned; custom sizes are not supported.

  • Name
    head_size_mm
    Type
    number
    default 23
    Description

    Target size of the sculpted head, in millimeters: its longest dimension is scaled to this value. Range: [10, 40]. Values above roughly 32.9 may be reduced so the head still fits the base's protective footprint limit, so the delivered longest dimension can be smaller than requested. The applied value is not echoed back on the task object today — if you need to confirm the size you actually received, measure the bounding box of the keycap-head mesh in the downloaded model.

  • Name
    vertical_offset_mm
    Type
    number
    default 0
    Description

    Vertical offset applied to the head before it is seated on the base, in millimeters. Range: [-5, 5].

Returns

The result property of the response contains the task id of the newly created keycap build task. Poll the Get a Task endpoint or subscribe to the stream until the task reaches SUCCEEDED, then download the artifacts from model_urls.glb and model_urls.obj_zip.

Failure Modes

  • Name
    400 - Bad Request
    Description

    The request was unacceptable. Common causes:

    • Missing parameter: input_task_id and candidate_id are required.
    • Invalid UUID: The input_task_id is not a valid UUID.
    • Parent not succeeded: The referenced prototype task has not reached SUCCEEDED yet.
    • No candidates: The prototype task succeeded but produced no candidates.
    • Unknown candidate: candidate_id is not one of the input task's candidates.
    • Options out of range: One of the options fields fell outside its allowed range or enum set.
  • Name
    401 - Unauthorized
    Description

    Authentication failed. Please check your API key.

  • Name
    402 - Payment Required
    Description

    The account is on the free plan (a paid plan is required to create tasks) or has insufficient credits.

  • Name
    404 - Not Found
    Description

    The referenced prototype task does not exist, belongs to a different user, or was created through the webapp (only API-mode prototype tasks chain into build).

  • Name
    429 - Too Many Requests
    Description

    You have exceeded your rate limit.

  • Name
    500 - Internal Server Error
    Description

    An unexpected server-side error occurred — for example the content-moderation service was unavailable, staging the input image failed, or the task could not be created. No task is created in this case, so retrying is safe.

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"
}

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

Retrieve a Keycap Task

Retrieve a prototype or build task given a valid task id. The URL path must match the task's stage — a build task fetched through /prototype/:id returns 404, and vice versa.

Refer to The Keycap Prototype Task Object and The Keycap Build Task Object for response shapes.

Parameters

  • Name
    id
    Type
    path
    Description

    Unique identifier for the keycap task to retrieve.

Returns

The response contains the keycap task object. The shape depends on which stage was requested.

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=***"
  }
}

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

Delete a Keycap Task

Cancel a keycap task. If the task is still PENDING, the credits consumed at create-time are refunded. Tasks that are already IN_PROGRESS are cancelled without a refund (the worker may already be burning resources). Tasks that have already reached a terminal state (SUCCEEDED, FAILED, CANCELED) cannot be cancelled.

The URL path must match the task's stage — DELETE on /prototype/:buildId returns 404.

Path Parameters

  • Name
    id
    Type
    path
    Description

    Unique identifier for the keycap task to cancel.

Returns

Returns 204 No Content on success with an empty body.

Failure Modes

  • Name
    400 - Bad Request
    Description

    The task is already in a terminal state and cannot be cancelled.

  • Name
    404 - Not Found
    Description

    The task does not exist, belongs to a different user, or its stage does not match the URL path.

  • Name
    500 - Internal Server Error
    Description

    An unexpected server-side error occurred while cancelling. The task may or may not have been cancelled — re-read it to confirm before retrying.

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).

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

Stream a Keycap Task

Stream real-time updates for a keycap task via Server-Sent Events (SSE). The URL path must match the task's stage — opening a stream at /prototype/:buildId/stream emits a single event: error payload with status_code: 404 and closes the stream.

Parameters

  • Name
    id
    Type
    path
    Description

    Unique identifier for the keycap task to stream.

Returns

Returns a stream of Keycap Prototype or Keycap Build task objects as Server-Sent Events. For PENDING or IN_PROGRESS tasks, the response stream will only include the necessary progress and status fields.

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.
// For PENDING or IN_PROGRESS tasks, the response stream will not include all fields.
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=***"
  }
}

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

List Keycap Tasks

Retrieve a paginated list of your keycap tasks for a single stage. The URL path selects the stage — /prototype returns prototype tasks; /build returns build tasks. Tasks from the other stage are not included in either response.

Path Parameters

  • Name
    stage
    Type
    path
    Required
    Description

    Either prototype or build. The collection returns only tasks whose stage matches the URL — fetching /prototype never returns build tasks and vice versa.

Query Parameters

  • Name
    page_num
    Type
    integer
    default 1
    Description

    Page number for pagination.

  • Name
    page_size
    Type
    integer
    default 10
    Description

    Page size limit. Maximum allowed is 100 items.

  • Name
    sort_by
    Type
    string
    default -created_at
    Description

    Field to sort by. Available values:

    • +created_at: Sort by creation time in ascending order.
    • -created_at: Sort by creation time in descending order.

Returns

Returns a paginated list of the per-stage task object — either the keycap prototype task object when listing /prototype or the keycap build task object when listing /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"
    ]
  }
]

The Keycap Prototype Task Object

The Keycap Prototype Task object is a work unit that Meshy keeps track of to generate one finished-keycap design image from a source photo. The output of this stage is chained into the build stage via input_task_id plus candidate_id.

Properties

  • Name
    id
    Type
    string
    Description

    Unique identifier for the task. While we use a k-sortable UUID for task ids as the implementation detail, you should not make any assumptions about the format of the id.

  • Name
    type
    Type
    string
    Description

    Type of the task. The value is creative-lab-keycap-prototype.

  • Name
    name
    Type
    string
    Description

    The task name supplied when the task was created. Empty string if no name was provided.

  • Name
    status
    Type
    string
    Description

    Status of the task. Possible values are one of PENDING, IN_PROGRESS, SUCCEEDED, FAILED, CANCELED.

  • Name
    progress
    Type
    integer
    Description

    Progress of the task. If the task is not started yet, this property will be 0. Once the task has succeeded, this will become 100.

  • Name
    created_at
    Type
    timestamp
    Description

    Timestamp of when the task was created, in milliseconds.

  • Name
    started_at
    Type
    timestamp
    Description

    Timestamp of when the task was started, in milliseconds. If the task is not started yet, this property will be 0.

  • Name
    finished_at
    Type
    timestamp
    Description

    Timestamp of when the task was finished, in milliseconds. If the task is not finished yet, this property will be 0.

  • Name
    expires_at
    Type
    timestamp
    Description

    Timestamp of when the task result expires, in milliseconds.

  • Name
    preceding_tasks
    Type
    integer
    Description

    The count of preceding tasks.

  • Name
    task_error
    Type
    object
    Description

    Error details for failed tasks. See Errors for the full task_error object reference.

  • Name
    consumed_credits
    Type
    integer
    Description

    The number of credits consumed by this task. A task that reaches SUCCEEDED is charged the full amount for its stage. A task that never gets created (a 4xx at request time, including a moderation rejection) is not charged at all. A task that reaches FAILED returns 0 — the charge is refunded, including an asynchronous moderation block. Cancelling via DELETE refunds only while the task is still PENDING; a task already IN_PROGRESS stays charged, because the work has been spent.

  • Name
    image_urls
    Type
    array of strings
    Description

    Downloadable URL of the finished-keycap design render — what the candidate looks like as a finished keycap. Holds a single entry; image_urls[i] corresponds to candidate_ids[i]. Empty until the task reaches SUCCEEDED. The URL is for display only; the build endpoint consumes candidate_ids, not these URLs. Same URL lifecycle as model_urls: signed, no Authorization header, valid until expires_at, and stable when the task is re-read.

  • Name
    candidate_ids
    Type
    array of strings
    Description

    Opaque candidate identifiers, parallel to image_urls. Pass the entry matching your chosen design as the build request's candidate_id. Do not make any assumptions about the format of these ids.

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"
  ]
}

The Keycap Build Task Object

The Keycap Build Task object is a work unit that Meshy keeps track of to generate the final textured 3D keycap from a succeeded prototype task and a chosen candidate. A single build runs the full pipeline — white-model generation, automatic seating and cutting, coloring, assembly, and export.

Properties

  • Name
    id
    Type
    string
    Description

    Unique identifier for the task.

  • Name
    type
    Type
    string
    Description

    Type of the task. The value is creative-lab-keycap-build.

  • Name
    name
    Type
    string
    Description

    The task name supplied when the task was created. Empty string if no name was provided.

  • Name
    status
    Type
    string
    Description

    Status of the task. Possible values are one of PENDING, IN_PROGRESS, SUCCEEDED, FAILED, CANCELED.

  • Name
    progress
    Type
    integer
    Description

    Progress of the task. If the task is not started yet, this property will be 0. Once the task has succeeded, this will become 100.

  • Name
    created_at
    Type
    timestamp
    Description

    Timestamp of when the task was created, in milliseconds.

  • Name
    started_at
    Type
    timestamp
    Description

    Timestamp of when the task was started, in milliseconds.

  • Name
    finished_at
    Type
    timestamp
    Description

    Timestamp of when the task was finished, in milliseconds.

  • Name
    expires_at
    Type
    timestamp
    Description

    Timestamp of when the task result expires, in milliseconds.

  • Name
    preceding_tasks
    Type
    integer
    Description

    The count of preceding tasks. Meaningful only when status is PENDING.

  • Name
    task_error
    Type
    object
    Description

    Error details for failed tasks. See Errors for the full task_error object reference.

  • Name
    consumed_credits
    Type
    integer
    Description

    The number of credits consumed by this task. A task that reaches SUCCEEDED is charged the full amount for its stage. A task that never gets created (a 4xx at request time, including a moderation rejection) is not charged at all. A task that reaches FAILED returns 0 — the charge is refunded, including an asynchronous moderation block. Cancelling via DELETE refunds only while the task is still PENDING; a task already IN_PROGRESS stays charged, because the work has been spent.

  • Name
    model_urls
    Type
    object
    Description

    Downloadable URLs for the generated model artifacts. Both the GLB and the OBJ bundle are exported at real-world millimeter scale, Y-up, with the front of the keycap facing +Z. Meshes are named keycap-head and keycap-base; when the base falls back to a pattern fill, a third mesh keycap-base-interior is also present for the stem cavity. Do not assume exactly two meshes.

    These are signed URLs: fetch them without an Authorization header. They stay valid until expires_at, which is 3 days after finished_at, and re-reading the task inside that window returns the identical URL rather than a freshly signed one. Download and store the files yourself before then — there is no way to refresh an expired link.

    • Name
      glb
      Type
      string
      Description

      Downloadable URL to the final textured model.glb.

    • Name
      obj_zip
      Type
      string
      Description

      Downloadable URL to a zip bundle containing model.obj, model.mtl, and the texture PNGs its MTL actually references. A solid-color base ships only keycap-head.png; a patterned base also ships keycap-base.png.

  • Name
    process_image_urls
    Type
    object
    Description

    Downloadable URLs for intermediate process images, keyed by kind. Same URL lifecycle as model_urls: signed, no Authorization header, valid until expires_at, and stable when the task is re-read. Currently emitted kinds:

    • head_design — the chosen candidate's design image the build consumed (always present).
    • composite — the finished-keycap display render of the chosen candidate (present when available).
    • base_canvas — the painted keycap-base canvas (present when available).

    Treat the key set as open-ended; new kinds may be added without a breaking change.

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=***"
  }
}

End-to-End Example

The complete flow: create a prototype from a photo, poll it to SUCCEEDED, pick a candidate from candidate_ids, create a build with that candidate, poll the build to SUCCEEDED, then download the GLB and the OBJ bundle from model_urls.

The example picks the first candidate programmatically. In a real integration you would display the image_urls entry to the end user and let them choose; the chosen index maps 1:1 onto 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"