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
Both POST endpoints require a paid subscription plan. Requests from
free-plan accounts are rejected with 402 Payment Required.
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 rendercurlhttps://api.meshy.ai/openapi/creative-lab/keycap/v1/prototype \-XPOST \-H"Authorization: Bearer ${YOUR_API_KEY}" \-H'Content-Type: application/json' \-d'{ "image_url": "<your publicly accessible image url or base64-encoded data URI>" }'
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.
Both the GLB and the OBJ bundle are exported at real-world
millimeter scale, with a Y-up coordinate system and the front of
the keycap facing +Z.
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 keycapcurlhttps://api.meshy.ai/openapi/creative-lab/keycap/v1/build \-XPOST \-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 } }'
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.
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.
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.
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.
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.
A timestamp represents the number of milliseconds elapsed since January 1, 1970 UTC, following
the RFC 3339 standard.
For example, Friday, September 1, 2023 12:00:00 PM GMT is represented as 1693569600000. This applies
to all timestamps in Meshy API.
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.
The value of this field is meaningful only if the task 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
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.
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.
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 bashset-euopipefail# 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:-}" ]]; thenecho"export IMAGE_PATH (local file) or IMAGE_URL (public url) first">&2exit1fiBASE="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 bodyshift2 out=$(curl--silent--show-error--max-time60--write-out$'\n%{http_code}' \-X"$method""$url"-H"$AUTH""$@") ||return1 http_code=${out##*$'\n'} body=${out%$'\n'*}if ((http_code >=400)); thenecho"HTTP $http_code for $url: $body">&2return1fiprintf'%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:; doif (($(date +%s) >= deadline)); thenecho"gave up waiting for $kind $id">&2return1fi task_status=$(apiGET"$BASE/$kind/$id"|jq-r'.status')echo"$kind: $task_status"case"$task_status"inSUCCEEDED)return0 ;;FAILED|CANCELED)return1 ;;esacsleep"$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"'EXITif [[ -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')"inpng) 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"elseprintf'{"image_url":"%s"}'"$IMAGE_URL">"$BODY"fi# 1. Create the prototype taskPROTO_ID=$(apiPOST"$BASE/prototype" \-H'Content-Type: application/json'--data-binary@"$BODY"|jq-r'.result')# 2. Wait for the design renderpollprototype"$PROTO_ID"# 3. Pick a candidate (first one here; show image_urls to a user in production)CANDIDATE_ID=$(apiGET"$BASE/prototype/$PROTO_ID"|jq-r'.candidate_ids[0]')# 4. Create the build taskjq-n--argp"$PROTO_ID"--argc"$CANDIDATE_ID" \'{input_task_id: $p, candidate_id: $c}'>"$BODY"BUILD_ID=$(apiPOST"$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)pollbuild"$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=$(apiGET"$BASE/build/$BUILD_ID")curl--silent--show-error--fail--max-time900 \-okeycap.glb"$(jq-r '.model_urls.glb' <<<"$TASK")"curl--silent--show-error--fail--max-time900 \-okeycap-obj.zip"$(jq-r '.model_urls.obj_zip' <<<"$TASK")"echo"Done: keycap.glb + keycap-obj.zip"