Creative Lab Keycap API
Turn a photo into a full-color custom keycap for a mechanical keyboard in two stages. A prototype task generates an image of the finished keycap from the photo. A build task turns that image into a textured 3D keycap model. It takes the prototype task's ID as input_task_id and the image's candidate ID as candidate_id.
POST /openapi/creative-lab/keycap/v1/prototypePOST /openapi/creative-lab/keycap/v1/build
Both POST endpoints need a paid plan. A request from a free-plan account returns 402.
Create a Keycap prototype task
Generates one image of the finished keycap from a photo. The task's image_urls holds the image, and candidate_ids holds its ID, with one entry in each. If you don't like the design, create another prototype task. Meshy charges for each prototype task separately.
To build the 3D keycap, pass this task's ID to the build endpoint as input_task_id, with the candidate_ids entry as candidate_id. For the task's fields, see The Keycap prototype task object.
Request
# 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"
}
Parameters
- Name
- image_url
- Type
- string
- Required
- Description
The photo to turn into a keycap design, as a public URL or a data URI (
data:image/jpeg;base64,<your base64-encoded image data>).The image must be a JPG, JPEG, PNG, or WebP file of at most 20 MB (20,000,000 bytes). Each side must be at least 32 pixels, and the image can have at most 178,956,970 pixels in total. An image outside these limits returns
400.Meshy detects the format from the image data, not from the file extension in the URL. A URL with no extension works, and Meshy follows HTTP redirects. Meshy applies the EXIF orientation, so a rotated phone photo comes out the way it looks.
A data URI must be base64-encoded with an image type, such as
data:image/png;base64,..., and the decoded image must be JPEG, PNG, or WebP. The 20 MB limit applies to the decoded image, so the base64 text can be about a third larger.
- Name
- name
- Type
- string
- Description
A name for the task, up to 100 characters. It comes back in the task's
namefield. A longer name returns400.
- Name
- remove_background
- Type
- boolean
- default false
- Description
Whether to remove the background from the prototype image.
truereturns a transparent RGBA PNG, so you can place the keycap on any background. If Meshy can't remove the background, it returns the image with its background, and the task still succeeds.The head design that the build turns into 3D keeps its background either way. The build paints the keycap base from the image in
image_urls, without its background if you senttrue. The build returns that image asprocess_image_urls.composite.
Returns
The result field holds the ID of the new prototype task. Get the task or stream it until status is SUCCEEDED. Then pass the ID to the build endpoint as input_task_id, with the entry from candidate_ids as candidate_id.
Failure modes
- Name
400 - Bad Request- Description
The request can't run. For example:
image_urlis missing.- The image isn't a JPG, JPEG, PNG, or WebP file.
- The image is too small, over 20 MB, or over the maximum pixel count.
- Meshy can't download the image, for example because the URL returns
404or times out. - The data URI isn't valid base64.
- The image was flagged as NSFW.
The error message says which problem it found. Fix the request, then send it again.
- Name
401 - Unauthorized- Description
The API key is missing or invalid. Check the
Authorizationheader.
- Name
402 - Payment Required- Description
One of these applies:
- The account doesn't have enough credits.
- The API key, workspace, or member reached its credit limit.
- The account is on the free plan.
Check your credits and plan, then retry.
- Name
403 - Forbidden- Description
Your team has IP Filtering turned on, and Meshy flagged the image for third-party intellectual property. Send an image you have the rights to use.
- Name
429 - Too Many Requests- Description
One of these limits was reached:
- You sent more requests than your rate limit allows. Wait a few seconds, then retry. See Rate limits.
- Too many of your tasks are queued or running. Wait for some to finish, then retry.
- On an unlimited plan with no credits left, you reached the number of tasks that can run without credits. Wait for a task to finish, or add credits.
- Name
500 - Internal Server Error- Description
Meshy hit a server error, for example because content moderation or the intellectual property check couldn't run, or Meshy couldn't store the image. Meshy doesn't create the task, and refunds any credits it charged. Retry the request.
Create a Keycap build task
Generates the textured 3D keycap from a succeeded prototype task and one of its candidates. For its fields, see The Keycap build task object.
One build task runs every step. It generates a 3D model from the design and colors it. Then it seats and cuts the model onto the keycap base in a calibrated default pose, so you don't need to adjust it by hand. Last, it assembles the keycap and exports the files. A build typically takes 3 to 7 minutes.
Request
# 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"
}
Parameters
- Name
- input_task_id
- Type
- string
- Required
- Description
The ID of the prototype task to build. It must be a task your account created with
POST /openapi/creative-lab/keycap/v1/prototype, with the statusSUCCEEDED.An ID that isn't one of your Keycap prototype tasks from the API returns
404, including a prototype made in the web app.
- Name
- candidate_id
- Type
- string
- Required
- Description
The candidate to build, from the
candidate_idsarray of the prototype task. A value that isn't in that array returns400.
- Name
- name
- Type
- string
- Description
A name for the task, up to 100 characters. It comes back in the task's
namefield. A longer name returns400.
options
Optional geometry settings. Each field has a calibrated default, so send only the fields you want to change.
- Name
- base_model
- Type
- string
- default cherry-mx-1x1-r1
- Description
The keycap base to build on. The only value is
cherry-mx-1x1-r1, a standard Cherry MX profile 1u keycap. Custom sizes aren't supported, and any other value returns400.
- Name
- head_size_mm
- Type
- number
- default 23
- Description
The size of the head along its longest dimension, in millimeters, from
10to40. Meshy scales the head so its longest dimension matches this value. A value outside the range returns400.For a value above about
32.9, Meshy might reduce the size so the head fits the base's protective footprint limit. The head can then come out smaller than you asked. The task object doesn't return the size Meshy applied. To check it, measure the bounding box of thekeycap-headmesh in the model.
- Name
- vertical_offset_mm
- Type
- number
- default 0
- Description
The vertical offset to apply to the head before Meshy seats it on the base, in millimeters, from
-5to5. A value outside the range returns400.
Returns
The result field holds the ID of the new build task. Get the task or stream it until status is SUCCEEDED. Then download the GLB file from model_urls.glb and the OBJ bundle from model_urls.obj_zip.
Meshy exports the GLB file and the OBJ bundle at real-world scale in millimeters. Both use a Y-up coordinate system, with the front of the keycap facing +Z.
Failure modes
- Name
400 - Bad Request- Description
The request can't run. For example:
input_task_idis missing or isn't a valid task ID.- The prototype task's status isn't
SUCCEEDED. If it's still running, wait and retry. If it failed or was canceled, create a new prototype task. - The prototype task succeeded but has no candidates. Create a new prototype task.
candidate_idis missing, or isn't in the prototype task'scandidate_ids. Send a value from that array.- A field in
optionsis out of range, orbase_modelisn't an available value.
The error message says which problem it found.
- Name
401 - Unauthorized- Description
The API key is missing or invalid. Check the
Authorizationheader.
- Name
402 - Payment Required- Description
One of these applies:
- The account doesn't have enough credits.
- The API key, workspace, or member reached its credit limit.
- The account is on the free plan.
Check your credits and plan, then retry.
- Name
404 - Not Found- Description
input_task_iddoesn't match any Keycap prototype task your account created with the API. A prototype made in the web app also returns404. Send theresultID from a prototype create response.
- Name
429 - Too Many Requests- Description
One of these limits was reached:
- You sent more requests than your rate limit allows. Wait a few seconds, then retry. See Rate limits.
- Too many of your tasks are queued or running. Wait for some to finish, then retry.
- On an unlimited plan with no credits left, you reached the number of tasks that can run without credits. Wait for a task to finish, or add credits.
- Name
500 - Internal Server Error- Description
Meshy couldn't create the task because of a server error. Meshy refunds any credits it charged. Retry the request.
Get a Keycap task
Returns a prototype or build task. The path must match the task's stage. A build task requested through /prototype/:id returns 404, and so does a prototype task requested through /build/:id.
For the fields, see The Keycap prototype task object and The Keycap build task object.
Request
# 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=***"
}
}
Parameters
- Name
- id
- Type
- path
- Description
The task ID from the create response.
Returns
The prototype task object or the build task object, matching the stage in the path.
Failure modes
- Name
400 - Bad Request- Description
idisn't a valid task ID. Send theresultID from the create response.
- Name
403 - Forbidden- Description
Your team has IP Filtering turned on, and Meshy flagged the prototype task's image for third-party intellectual property. Build tasks and teams on the Enterprise plan don't get this error. Create a prototype task from an image you have the rights to use.
- Name
404 - Not Found- Description
No Keycap task with this ID belongs to your account, or the task's stage doesn't match the path. Check the ID, and use
/prototypeor/buildto match the task.
- Name
500 - Internal Server Error- Description
Meshy couldn't run the intellectual property check. Retry the request.
Delete a Keycap task
Cancels a Keycap task. Canceling a PENDING task refunds its credits. An IN_PROGRESS task is canceled without a refund, because a worker has already started it. A task with a final status (SUCCEEDED, FAILED, or CANCELED) can't be canceled.
The path must match the task's stage. For example, DELETE on /prototype/:id with a build task's ID returns 404.
Request
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).
Path parameters
- Name
- id
- Type
- path
- Description
The ID of the task to cancel.
Returns
204 No Content with an empty body.
Failure modes
- Name
400 - Bad Request- Description
One of these applies:
idisn't a valid task ID. Send theresultID from the create response.- The task already has a final status (
SUCCEEDED,FAILED, orCANCELED), so it can't be canceled.
- Name
404 - Not Found- Description
No Keycap task with this ID belongs to your account, or the task's stage doesn't match the path. Check the ID, and use
/prototypeor/buildto match the task.
- Name
500 - Internal Server Error- Description
Meshy hit a server error while canceling the task, so the task might or might not be canceled. Get the task to check its
statusbefore you retry.
Stream a Keycap task
Streams a Keycap task's updates as Server-Sent Events (SSE). The path must match the task's stage. If it doesn't, for example a build task's ID at /prototype/:id/stream, the stream sends one error event with status_code: 404 and closes.
Request
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=***"
}
}
Parameters
- Name
- id
- Type
- path
- Description
The task ID from the create response.
Returns
A stream of prototype or build task objects. Each event has the full task object, as Get a Keycap task returns it. While the task is PENDING or IN_PROGRESS, its output fields are still empty (null, [], or {}), and finished_at is null.
If IP Filtering blocks the task's result, the stream sends one error event in its place, with the status_code that a get request would return.
List Keycap tasks
Returns your Keycap tasks of one stage, one page at a time. The path selects the stage. /prototype returns only prototype tasks, and /build returns only build tasks.
Request
# 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
- Required
- Description
The stage to list,
prototypeorbuild.
Query parameters
- Name
- page_num
- Type
- integer
- default 1
- Description
The page to return, from
1to255.0returns the first page. A value outside0to255returns400.
- Name
- page_size
- Type
- integer
- default 10
- Description
Number of tasks per page, up to
100. A value from101to255returns 100 tasks, and0uses the default. A value outside0to255returns400.
- Name
- sort_by
- Type
- string
- default -created_at
- Description
The sort order.
Available values:
-created_at: newest first+created_at: oldest first
Returns
An array of prototype task objects or build task objects, matching the stage in the path.
The Keycap prototype task object
A prototype task generates one image of the finished keycap from a photo. To turn the design into a 3D keycap, pass the task's ID to the build endpoint as input_task_id, with its candidate_ids entry as 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
The task ID. Don't rely on its format.
- Name
- type
- Type
- string
- Description
The task type, always
creative-lab-keycap-prototype.
- Name
- name
- Type
- string
- Description
The
nameyou sent when you created the task, or""if you didn't send one.
- Name
- status
- Type
- string
- Description
The task's status.
Available values:
PENDING: waiting in the queueIN_PROGRESS: generating the resultSUCCEEDED: final, with the resultFAILED: final, with the reason intask_errorCANCELED: final, with no result
- Name
- progress
- Type
- integer
- Description
Percentage of the work done, from
0to100. It's0until the task starts and100when it succeeds.
- Name
- created_at
- Type
- timestamp
- Description
Time the task was created, in Unix milliseconds.
- Name
- started_at
- Type
- timestamp
- Description
Time a worker started the task, in Unix milliseconds.
nulluntil then.
- Name
- finished_at
- Type
- timestamp
- Description
Time the task reached a final status, in Unix milliseconds.
nulluntil then.
- Name
- expires_at
- Type
- timestamp
- Description
Time the task's result expires, in Unix milliseconds. It's 3 days after
finished_at, or 3 days aftercreated_atuntil the task finishes. For Enterprise accounts, which keep API results indefinitely, it's about 100 years later. See Asset retention.
- Name
- preceding_tasks
- Type
- integer
- Description
The task's position in the queue while it's
PENDING.1means it starts next.0after the task leaves the queue, and always0in list results.
- Name
- task_error
- Type
- object
- Description
Why the task failed.
nullunlessstatusisFAILED. See Errors.
- Name
- consumed_credits
- Type
- integer
- Description
Number of credits charged for the task, after any refund. A request that returns an error isn't charged. A failed task is
0, because Meshy refunds it. Canceling refunds a task only while it'sPENDING. If Meshy can't read the charge, a get or stream response leaves the field out, and a list request returns500. In that case, retry the request.
- Name
- image_urls
- Type
- array of strings
- Description
Download URLs for the image of the finished keycap. The array has one URL after the task succeeds, and is empty until then.
image_urls[i]shows the candidate incandidate_ids[i].These URLs are for display. The build endpoint takes an entry from
candidate_ids, not a URL. They're signed URLs, so fetch them without anAuthorizationheader. They stay valid untilexpires_at, and getting the task again returns the same URLs.
- Name
- candidate_ids
- Type
- array of strings
- Description
IDs of the candidates, in the same order as
image_urls. To build a candidate, send its ID ascandidate_idin the build request. Don't rely on the format of these IDs.
The Keycap build task object
A build task generates the textured 3D keycap from a succeeded prototype task and one of its candidates. It generates the 3D model, colors it, seats and cuts it onto the keycap base, and then assembles and exports the keycap.
Properties
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
The task ID. Don't rely on its format.
- Name
- type
- Type
- string
- Description
The task type, always
creative-lab-keycap-build.
- Name
- name
- Type
- string
- Description
The
nameyou sent when you created the task, or""if you didn't send one.
- Name
- status
- Type
- string
- Description
The task's status.
Available values:
PENDING: waiting in the queueIN_PROGRESS: generating the resultSUCCEEDED: final, with the resultFAILED: final, with the reason intask_errorCANCELED: final, with no result
- Name
- progress
- Type
- integer
- Description
Percentage of the work done, from
0to100. It's0until the task starts and100when it succeeds.
- Name
- created_at
- Type
- timestamp
- Description
Time the task was created, in Unix milliseconds.
- Name
- started_at
- Type
- timestamp
- Description
Time a worker started the task, in Unix milliseconds.
nulluntil then.
- Name
- finished_at
- Type
- timestamp
- Description
Time the task reached a final status, in Unix milliseconds.
nulluntil then.
- Name
- expires_at
- Type
- timestamp
- Description
Time the task's result expires, in Unix milliseconds. It's 3 days after
finished_at, or 3 days aftercreated_atuntil the task finishes. For Enterprise accounts, which keep API results indefinitely, it's about 100 years later. See Asset retention.
- Name
- preceding_tasks
- Type
- integer
- Description
The task's position in the queue while it's
PENDING.1means it starts next.0after the task leaves the queue, and always0in list results.
- Name
- task_error
- Type
- object
- Description
Why the task failed.
nullunlessstatusisFAILED. See Errors.
- Name
- consumed_credits
- Type
- integer
- Description
Number of credits charged for the task, after any refund. A request that returns an error isn't charged. A failed task is
0, because Meshy refunds it. Canceling refunds a task only while it'sPENDING. If Meshy can't read the charge, a get or stream response leaves the field out, and a list request returns500. In that case, retry the request.
- Name
- model_urls
- Type
- object
- Description
Download URLs for the 3D model. Meshy exports the GLB file and the OBJ bundle at real-world scale in millimeters, Y-up, with the front of the keycap facing +Z.
The model has meshes named
keycap-headandkeycap-base. When Meshy falls back to a pattern fill for the base, the model also has akeycap-base-interiormesh for the stem cavity. Don't assume the model has exactly two meshes.These are signed URLs, so fetch them without an
Authorizationheader. They stay valid untilexpires_at, and getting the task again before then returns the same URLs. An expired URL can't be renewed, so download and store the files before then.- Name
glb- Type
- string
- Description
Download URL for the textured GLB file,
model.glb.
- Name
obj_zip- Type
- string
- Description
Download URL for the OBJ bundle, a ZIP file with
model.obj,model.mtl, and the PNG textures thatmodel.mtluses. With a solid-color base, the only texture iskeycap-head.png. With a patterned base, the ZIP file also haskeycap-base.png.
- Name
- process_image_urls
- Type
- object
- Description
Download URLs for images from the build's steps, keyed by kind. Like
model_urls, they're signed URLs, so fetch them without anAuthorizationheader. They stay valid untilexpires_at, and getting the task again returns the same URLs.It can have these keys:
head_design: the design image of the chosen candidate, which the build turned into 3D. Always present.composite: the image of the finished keycap for the chosen candidate, which the build painted the base from. Can be missing.base_canvas: the painted keycap base. Can be missing.
Meshy may add keys, so ignore a key you don't recognize.
End-to-end example
The example runs the whole flow:
- Create a prototype task from a photo.
- Get the task until its
statusisSUCCEEDED. - Pick a candidate from
candidate_ids. - Create a build task with that candidate.
- Get the build task until its
statusisSUCCEEDED. - Download the GLB file and the OBJ bundle from
model_urls.
The example picks the first candidate. In your app, show the image_urls entries to your users and let them choose. A chosen image's index in image_urls is the index of its ID in candidate_ids.
Complete flow
#!/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"