Customer API and webhooks
Build imports, run every shipped model action, manage the asset library, prepare print recipes, and receive durable signed events. This guide is the human-readable companion to the machine-readable OpenAPI contract.
One request flow
Use https://api.messypoly.com as the API base URL. The normal model flow is: create an import, upload the bytes to the returned presigned URL, mark the import complete, queue an action, poll the job, then fetch artifact URLs.
export MESSYPOLY_API_URL=https://api.messypoly.com export MESSYPOLY_API_KEY=mpk_… # A complete working example is also available in examples/node/import-and-optimize.mjs node examples/node/import-and-optimize.mjs ./model.glb
JSON API
Send Content-Type: application/json for JSON requests. Presigned file uploads use the upload URL and the content type returned by the import or input reservation.
Public IDs
Use the returned id, jobId, assetId, versionId, batchId, and endpointId values exactly as returned; do not use database IDs.
Authentication, scopes, and idempotency
Create a key in the workspace account menu under Developer settings. Send it on every request:
Authorization: Bearer mpk_…
| Scope | Allows |
|---|---|
assets:read | Imports, assets, versions, projects, and print setup reads. |
assets:write | Complete imports; create, edit, or delete assets, versions, and projects. |
jobs:read | Job and batch status. |
jobs:write | Queue actions, create imports and batches, cancel jobs, and request print quotes. |
artifacts:read | List job artifacts and download batch archives. |
webhooks:manage | List, create, and delete webhook endpoints. |
/api/v1 request requires an Idempotency-Key header (1–256 characters). Reuse the same key only when retrying the same method, path, and JSON body. A different request with an existing key returns a conflict; a failed request does not leave the key locked indefinitely.curl -X POST "$MESSYPOLY_API_URL/api/v1/imports" \
-H "Authorization: Bearer $MESSYPOLY_API_KEY" \
-H "Idempotency-Key: import-$(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"filename":"model.glb","sizeBytes":123456,"contentType":"model/gltf-binary"}'
Import lifecycle
POST /api/v1/importswithfilename,sizeBytes, and optionalcontentType/sha256. Save the importidand upload URL.- Upload the exact bytes with
PUTto that presigned URL. Do not send the bearer token to object storage. POST /api/v1/imports/:importId/completeafter the upload succeeds.- Use
GET /api/v1/imports/:importIdto inspect upload readiness without starting work. - Queue an import action. Use
generatefor an image import; useinspectoroptimizefor a model import. - Poll
GET /api/v1/jobs/:jobId, or subscribe to terminal webhooks, until the job is terminal.
const imported = await api('POST', '/api/v1/imports', {
filename: 'model.glb', sizeBytes, contentType: 'model/gltf-binary'
});
await fetch(imported.upload.url, { method: 'PUT',
headers: { 'content-type': 'model/gltf-binary' }, body: bytes });
await api('POST', `/api/v1/imports/${imported.id}/complete`);
const queued = await api('POST', `/api/v1/imports/${imported.id}/actions`, {
action: 'optimize', intent: 'realistic'
});
const job = await api('GET', `/api/v1/jobs/${queued.jobId}`);
Import status is pending_upload, ready, processing, or terminal. A missing object is retryable; it is not treated as a rejected import until source validation definitively fails.
Every action and its credit cost
Action requests go to POST /api/v1/imports/:importId/actions for intake, or POST /api/v1/versions/:versionId/actions for a retained model version. Put the action-specific object at the top level shown below. Costs are economy-v2 and are reserved when a paid job is accepted.
| Action | Cost | Use it on | Intent | Settings |
|---|---|---|---|---|
generate | 75 | Image import | realistic or stylized | generation: optional seed, textureSize (512/1024/2048). |
optimize | 5 | Model import or version | realistic, stylized, or custom | custom: exactly one of targetTriangleRatio/targetTriangleCount; optional texture/error limits. |
retopo | 15 | Retained version | retopo | retopo.targetQuadCount (minimum 4). |
inspect | 0 | Model import or version | None | No settings. Validates and analyzes the source and publishes the Original version. |
repair | 1 | Retained version | None | No settings. Applies safe geometry repair and re-validates. |
fill_holes | 1 | Retained version | None | No settings. Fills reviewed simple planar openings only. |
unwrap | 1 | Retained version | None | unwrap: layoutMode, atlasSize, marginPixels, rebake. |
appearance | 1 | Retained version | None | appearance: material, texture-optimize, paint, or channel-replace operation. |
resize | 1 | Retained version | None | resize: dimension/bounds/volume target, units, origin, grounding, optional quaternion. |
add_base | 1 | Retained version | None | base: round or rounded-rectangle dimensions, overlap, color, optional text. |
hollow | 1 | Retained version | None | hollow: wall/drain dimensions and position; optional interior, ballast, and FDM acknowledgement. |
print_prepare | 1 | Retained version | None | printPrepare: printProfileId, targetHeightMm, automaticRepair:true, optional snapshot. |
print_recipe | quote | Retained version | None | First quote a complete printRecipe; then submit it with quoteId. |
split | 1 | Retained version | None | split: axis/position, connector and layout modes, optional print profile and seam controls. |
rig | 1 | Retained version | None | rig: humanoid profile, body plan, 16 reviewed landmarks, maxInfluencesPerVertex:4. |
animate | 1 | Retained version | None | animation: rig profile and 1–4 licensed clipIds. |
convert and render are not available yet. Unknown actions, invalid intents, and actions sent to the wrong import/version endpoint return stable 400 errors instead of being rewritten to another action.Action request examples
All examples below are bodies for the action endpoint. The exact JSON Schema, including bounds and required fields, is authoritative in the OpenAPI spec.
Optimize with a fixed triangle target
{
"action": "optimize",
"intent": "custom",
"custom": {
"targetTriangleCount": 50000,
"maxTextureSize": 2048,
"simplifyErrorBudget": 0.02
}
}Resize to a dimension
{
"action": "resize",
"resize": {
"target": {"kind":"dimension","axis":"z","value":120,"unit":"mm"},
"origin": "bottom-center",
"ground": true
}
}Hollow for printing
{
"action": "hollow",
"hollow": {
"wallThicknessMm": 2,
"drainHoleDiameterMm": 4,
"drainPositionPercent": [50, 5],
"drainDirection": "bottom"
}
}Split into printable parts
{
"action": "split",
"split": {
"axis": "z",
"positionPercent": 50,
"connectorMode": "peg-and-socket",
"layoutMode": "separated",
"maximumPartCount": 2
}
}Appearance operations
material patches PBR factors by materialIndex; texture-optimize takes maxTextureSize and colorEncoding; paint takes UV strokes with normalized u/v points; channel-replace takes a PNG, JPEG, or WebP input. Replacement inputs are limited to 20 MB and must match their filename extension and content type.
{"action":"appearance","appearance":{"operation":"material","patches":[{"materialIndex":0,"roughnessFactor":0.55,"metallicFactor":0.1}]}}Rigging and animation
Rigging requires the 16 landmarks named in the schema: hips, chest, neck, head, both shoulder/elbow/wrist chains, and both hip/knee/ankle chains. Use profile messypoly-humanoid-v1 or messypoly-humanoid-v2 and maxInfluencesPerVertex:4. Animation accepts 1–4 clips such as idle, walk-in-place, run, wave, or jump.
Resources and management endpoints
| Method and path | What it does | Body/query |
|---|---|---|
| GET/api/v1/assets | List assets with pagination and filters. | limit, before, search, origin, action, tag, project. |
| GET/api/v1/assets/:assetId | Get one asset. | — |
| GET/api/v1/assets/:assetId/versions | List retained versions for an asset. | — |
| PATCH/api/v1/assets/:assetId | Rename, tag, move, or choose a featured/last-opened version. | name, tags, projectId, featuredVersionId, lastOpenedVersionId. |
| DELETE/api/v1/assets/:assetId | Delete an asset and its retained history. | — |
| PATCH/api/v1/versions/:versionId | Edit a version label or notes. | label, notes. |
| DELETE/api/v1/versions/:versionId | Delete a version. | Optional ?mode=files or ?mode=branch. |
| GET/api/v1/projects | List projects. | — |
| POST/api/v1/projects | Create a project. | {"name":"Characters"}. |
| PATCH/api/v1/projects/:projectId | Rename a project. | {"name":"Characters"}. |
| DELETE/api/v1/projects/:projectId | Delete a project. | — |
| GET/api/v1/print-setups | List available printer profiles. | — |
| POST/api/v1/batches | Create a batch optimize job set. | intent (realistic/stylized) and non-empty files array. |
| GET/api/v1/batches/:batchId | Get batch progress and outcomes. | — |
| GET/api/v1/batches/:batchId/archive | Download the completed batch archive. | Returns a binary archive response. |
Jobs, cancellation, and artifacts
| Method and path | Use |
|---|---|
| GET/api/v1/jobs/:jobId | Poll status, action, progress, and terminal error information. |
| POST/api/v1/jobs/:jobId/cancel | Cancel a non-terminal job. A terminal job is returned unchanged; it is never falsely reported as canceled. |
| GET/api/v1/jobs/:jobId/artifacts | List artifact metadata and short-lived download URLs. Refresh this endpoint when a URL expires. |
| POST/api/v1/jobs/:jobId/appearance-inputs | Reserve a PNG/JPEG/WebP replacement upload (maximum 20 MB) before submitting a channel-replace appearance operation; PUT bytes to uploadUrl and pass the returned input object. |
Terminal statuses include COMPLETE, FAILED, CANCELED, EXPIRED, NO_SAFE_RESULT, and REJECTED_PREFLIGHT. A successful job can still publish an artifact.failed event if durable artifact publication fails.
Quote before charging
print_recipe is the only action without a fixed cost. Submit the complete recipe and source version to POST /api/v1/print-recipes/quote. The response supplies a short-lived quoteId and credit amount. Submit the same recipe to the version action endpoint with that quote; the server rejects stale, changed, or already-claimed quotes.
const quote = await api('POST', '/api/v1/print-recipes/quote', {
sourceVersionId,
recipe: {
schemaVersion: 2,
process: 'fdm',
preparationPresetId: 'fdm-standard',
sizing: { kind:'dimension', axis:'z', value:120, unit:'mm', origin:'bottom-center', ground:true },
fixes: { automaticRepair:true, fillReviewedOpenings:true, rebuildAsSolid:false },
structural: { kind:'none' },
review: { acceptedInspectionRevision, acceptedAt: new Date().toISOString() }
}
});
await api('POST', `/api/v1/versions/${sourceVersionId}/actions`, {
action: 'print_recipe', quoteId: quote.quoteId,
idempotencyKey: 'recipe-claim-unique', printRecipe: quote.recipe
});
A recipe must include its inspection revision acceptance, sizing, fixes, structural stage, and review fields. FDM hollowing additionally requires the explicit acknowledgement described by the schema.
Signed webhooks
Webhook endpoints require the webhooks:manage scope. Use GET /api/v1/webhook-endpoints to list endpoints and DELETE /api/v1/webhook-endpoints/:endpointId to remove one. Endpoints must be absolute HTTPS URLs with a publicly resolvable, non-private address. Create the endpoint once; the signing secret is returned only on creation.
curl -X POST "$MESSYPOLY_API_URL/api/v1/webhook-endpoints" \
-H "Authorization: Bearer $MESSYPOLY_API_KEY" \
-H "Idempotency-Key: webhook-endpoint-1" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/messypoly/webhook","eventTypes":["job.completed","job.failed","artifact.completed","artifact.failed"]}'
| Event | When it is sent | data contains |
|---|---|---|
import.completed | Uploaded source is confirmed. | importId, status, sourceType, jobStatus. |
import.rejected | Definitive source validation failure. | Same import fields; a missing upload object remains retryable and does not trigger this event. |
job.completed | Processing reaches COMPLETE. | jobId, status, kind, intent, error:null. |
job.failed | Processing fails. | Job fields plus error:{code,message}. |
job.canceled / job.expired | Job reaches that terminal state. | Job fields plus terminal status/error. |
artifact.completed | Primary/export artifacts are durably published. | jobId, status, formats, kind. |
artifact.failed | Artifact publication fails after processing. | jobId, status, error, kind. |
Verify before parsing
Read the raw request body without re-serializing it. Compute HMAC-SHA256 over {timestamp}.{rawBody} with the one-time whsec_… secret. Compare the hex digest in MessyPoly-Webhook-Signature: v1=… with a constant-time comparison and reject timestamps older than five minutes.
MessyPoly-Webhook-Id: evt_job_job_123_job_completed MessyPoly-Webhook-Timestamp: 1786200000 MessyPoly-Webhook-Signature: v1=hex_hmac_sha256(timestamp + "." + raw_body)
Deliveries are POSTed as JSON, retried after non-2xx responses or network failures, and deduplicated by stable event ID per endpoint. Acknowledge quickly and process asynchronously. Payloads never contain presigned download URLs; fetch artifacts from the authenticated artifacts endpoint.
Errors and safe retries
| Code/status | Meaning | What to do |
|---|---|---|
401/403 | Missing/invalid key or insufficient scope/feature access. | Check the bearer token and key scopes; do not retry unchanged. |
ACTION_UNKNOWN | Action ID is not in the public catalog. | Use one of the 16 actions listed above. |
ACTION_NOT_AVAILABLE | Planned/internal action such as convert or render. | Wait for a published action; do not substitute another action. |
ACTION_INTENT_REJECTED | Intent is missing, unsupported, or supplied to a deterministic action. | Follow the action table and omit intent where it says “None”. |
ACTION_REQUIRES_VERSION / ACTION_REQUIRES_IMAGE_IMPORT | Action was sent to the wrong endpoint or source type. | Use an import action for intake/generation and a version action for retained model work. |
FRESH_QUOTE_REQUIRED | Print recipe changed, inspection changed, or quote expired. | Quote the current recipe again, then claim the new quote. |
409 idempotency conflict | Same key was reused with a different request. | Generate a new key for the new request; preserve the old key only for the original retry. |
429 | Quota, credits, or rate limit. | Honor retry guidance, check usage, or add credits; do not create duplicate requests. |
5xx | Transient service failure. | Retry with the same idempotency key for mutations and exponential backoff. |