MessyPoly developer documentation

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.

OpenAPI spec Open workspace
01 · Start here

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.

02 · Security

Authentication, scopes, and idempotency

Create a key in the workspace account menu under Developer settings. Send it on every request:

Authorization: Bearer mpk_…
ScopeAllows
assets:readImports, assets, versions, projects, and print setup reads.
assets:writeComplete imports; create, edit, or delete assets, versions, and projects.
jobs:readJob and batch status.
jobs:writeQueue actions, create imports and batches, cancel jobs, and request print quotes.
artifacts:readList job artifacts and download batch archives.
webhooks:manageList, create, and delete webhook endpoints.
Idempotency: every mutating /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"}'
03 · Uploads

Import lifecycle

  1. POST /api/v1/imports with filename, sizeBytes, and optional contentType/sha256. Save the import id and upload URL.
  2. Upload the exact bytes with PUT to that presigned URL. Do not send the bearer token to object storage.
  3. POST /api/v1/imports/:importId/complete after the upload succeeds.
  4. Use GET /api/v1/imports/:importId to inspect upload readiness without starting work.
  5. Queue an import action. Use generate for an image import; use inspect or optimize for a model import.
  6. 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.

04 · Processing

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.

ActionCostUse it onIntentSettings
generate75Image importrealistic or stylizedgeneration: optional seed, textureSize (512/1024/2048).
optimize5Model import or versionrealistic, stylized, or customcustom: exactly one of targetTriangleRatio/targetTriangleCount; optional texture/error limits.
retopo15Retained versionretoporetopo.targetQuadCount (minimum 4).
inspect0Model import or versionNoneNo settings. Validates and analyzes the source and publishes the Original version.
repair1Retained versionNoneNo settings. Applies safe geometry repair and re-validates.
fill_holes1Retained versionNoneNo settings. Fills reviewed simple planar openings only.
unwrap1Retained versionNoneunwrap: layoutMode, atlasSize, marginPixels, rebake.
appearance1Retained versionNoneappearance: material, texture-optimize, paint, or channel-replace operation.
resize1Retained versionNoneresize: dimension/bounds/volume target, units, origin, grounding, optional quaternion.
add_base1Retained versionNonebase: round or rounded-rectangle dimensions, overlap, color, optional text.
hollow1Retained versionNonehollow: wall/drain dimensions and position; optional interior, ballast, and FDM acknowledgement.
print_prepare1Retained versionNoneprintPrepare: printProfileId, targetHeightMm, automaticRepair:true, optional snapshot.
print_recipequoteRetained versionNoneFirst quote a complete printRecipe; then submit it with quoteId.
split1Retained versionNonesplit: axis/position, connector and layout modes, optional print profile and seam controls.
rig1Retained versionNonerig: humanoid profile, body plan, 16 reviewed landmarks, maxInfluencesPerVertex:4.
animate1Retained versionNoneanimation: rig profile and 1–4 licensed clipIds.
Not silently rewritten: 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.
05 · Payloads

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.

06 · Library and print

Resources and management endpoints

Method and pathWhat it doesBody/query
GET/api/v1/assetsList assets with pagination and filters.limit, before, search, origin, action, tag, project.
GET/api/v1/assets/:assetIdGet one asset.
GET/api/v1/assets/:assetId/versionsList retained versions for an asset.
PATCH/api/v1/assets/:assetIdRename, tag, move, or choose a featured/last-opened version.name, tags, projectId, featuredVersionId, lastOpenedVersionId.
DELETE/api/v1/assets/:assetIdDelete an asset and its retained history.
PATCH/api/v1/versions/:versionIdEdit a version label or notes.label, notes.
DELETE/api/v1/versions/:versionIdDelete a version.Optional ?mode=files or ?mode=branch.
GET/api/v1/projectsList projects.
POST/api/v1/projectsCreate a project.{"name":"Characters"}.
PATCH/api/v1/projects/:projectIdRename a project.{"name":"Characters"}.
DELETE/api/v1/projects/:projectIdDelete a project.
GET/api/v1/print-setupsList available printer profiles.
POST/api/v1/batchesCreate a batch optimize job set.intent (realistic/stylized) and non-empty files array.
GET/api/v1/batches/:batchIdGet batch progress and outcomes.
GET/api/v1/batches/:batchId/archiveDownload the completed batch archive.Returns a binary archive response.
07 · Results

Jobs, cancellation, and artifacts

Method and pathUse
GET/api/v1/jobs/:jobIdPoll status, action, progress, and terminal error information.
POST/api/v1/jobs/:jobId/cancelCancel a non-terminal job. A terminal job is returned unchanged; it is never falsely reported as canceled.
GET/api/v1/jobs/:jobId/artifactsList artifact metadata and short-lived download URLs. Refresh this endpoint when a URL expires.
POST/api/v1/jobs/:jobId/appearance-inputsReserve 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.

09 · Events

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"]}'
EventWhen it is sentdata contains
import.completedUploaded source is confirmed.importId, status, sourceType, jobStatus.
import.rejectedDefinitive source validation failure.Same import fields; a missing upload object remains retryable and does not trigger this event.
job.completedProcessing reaches COMPLETE.jobId, status, kind, intent, error:null.
job.failedProcessing fails.Job fields plus error:{code,message}.
job.canceled / job.expiredJob reaches that terminal state.Job fields plus terminal status/error.
artifact.completedPrimary/export artifacts are durably published.jobId, status, formats, kind.
artifact.failedArtifact 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.

10 · Troubleshooting

Errors and safe retries

Code/statusMeaningWhat to do
401/403Missing/invalid key or insufficient scope/feature access.Check the bearer token and key scopes; do not retry unchanged.
ACTION_UNKNOWNAction ID is not in the public catalog.Use one of the 16 actions listed above.
ACTION_NOT_AVAILABLEPlanned/internal action such as convert or render.Wait for a published action; do not substitute another action.
ACTION_INTENT_REJECTEDIntent 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_IMPORTAction 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_REQUIREDPrint recipe changed, inspection changed, or quote expired.Quote the current recipe again, then claim the new quote.
409 idempotency conflictSame key was reused with a different request.Generate a new key for the new request; preserve the old key only for the original retry.
429Quota, credits, or rate limit.Honor retry guidance, check usage, or add credits; do not create duplicate requests.
5xxTransient service failure.Retry with the same idempotency key for mutations and exponential backoff.
The OpenAPI document is the complete typed contract: it includes every path, request schema, enum, action settings object, and the published economy-v2 cost map. If this guide and the spec ever differ, report the discrepancy and use the spec for validation.