Developer docs
API reference
Everything in the product is available over REST: renders, templates, assets, bulk jobs, print files, presets, and the embedded editor. An interactive OpenAPI explorer with try-it-out lives at /api/docs.
Quickstart
Three steps from zero to a rendered mockup. You need an account and an API key from the API keys page.
1 · Upload a PSD template (or pick a library one)
curl -H "Authorization: Bearer $KEY" \ -F "file=@mockup.psd" -F "name=Tee front" \ https://your-domain.com/api/v1/templates
The response lists every smart-object slot with its id, name, source size, and warp info. Templates from the public library can be rendered directly by their id — no upload needed.
2 · Render
curl -H "Authorization: Bearer $KEY" \ -F "template_id=$TEMPLATE" \ -F "Front Design=@art.png" \ https://your-domain.com/api/v1/renders -o mockup.png
Design files are multipart parts whose field name is the slot key (layer name, layer id, or smart-object uuid). The image comes back directly — typically in about a second.
3 · Automate
Use async jobs with webhooks from code, or plug the same request into Zapier, Make, or n8n with zero code.
Authentication
Create keys on the API keys page — multiple named keys per account, revocable individually. Send the key as a bearer token on every request:
Authorization: Bearer soe_xxxxxxxxxxxxxxxx
Keys are shown once at creation. The same page holds your webhook signing secret (for verifying webhook deliveries) and your website key (for the embedded editor — safe to expose in client-side code, unlike API keys).
No-code automation — Zapier, Make & n8n
Every render is one HTTP request, so any tool with an HTTP module can generate mockups. Reference your artwork with design_url (a public URL — e.g. from Google Drive, Dropbox, Airtable, or your store) and there is no file handling at all.
The one request every tool makes
Method POST
URL https://your-domain.com/api/v1/renders
Headers Authorization: Bearer soe_YOUR_KEY
Body multipart/form-data
template_id tee-front-001
options {"slots":{"Front Design":{"design_url":"https://…/art.png"}}}
Response: the finished PNG (binary). Save it, attach it, upload it onward.Zapier
Add a "Webhooks by Zapier" action → Custom Request or POST. Set the URL and Authorization header as above; in Data, add template_id and options as form fields, mapping the design URL from your trigger (new Drive file, new sheet row, new order) into design_url. Zapier receives the image as the response body — feed it to any later step (upload to Drive, attach to email, send to your store).
Make
Add an HTTP → "Make a request" module. Method POST, body type multipart/form-data, fields template_id and options. Make exposes the response as binary data you can pass straight to Google Drive, Dropbox, or an email module. For long renders, use the async flow below and a Custom webhook trigger to resume the scenario when the render finishes.
n8n
Drop in an HTTP Request node — method POST, body Form-Data, response format File. For async, set webhook_url to the URL of an n8n Webhook node; your workflow sleeps while we render and resumes the moment the render.completed event arrives — no polling loop.
Async recipe (Make & n8n)
POST /api/v1/render-jobs ← same fields as /renders, plus:
webhook_url https://your-n8n.com/webhook/abcd1234
→ 202 {"id":"rj_...","status":"queued"}
… we render, then POST to your webhook_url:
X-MGW-Event: render.completed
X-MGW-Signature: <HMAC-SHA256 of the raw body, hex>
{"id":"rj_...","status":"done","result_url":"https://…","duration_ms":1043}Verify X-MGW-Signature with your webhook secret (see async jobs for code samples), then download result_url. The same pattern works for bulk jobs — the webhook delivers a link to the finished zip.
Templates
/api/v1/templatesMultipart: file (the PSD) and optional name. We parse the PSD once at upload — every smart object becomes a render slot. Returns 201 with the template, including per-slot editor geometry (warped outline, placement quad, source size).
/api/v1/templates/api/v1/templates/{id}/api/v1/templates/{id}List, inspect, and delete your templates. Slot keys accepted in render calls: the layer name (e.g. Front Design), the layer id, or the smart-object uuid.
/api/public/library/api/public/library/{id}The public template library (no auth required). Filter with ?category=. Library template ids work in every render endpoint exactly like your own.
Design assets
Upload artwork once, reference it everywhere by id — no re-uploading the same design on every render call.
/api/v1/assetsMultipart: file (PNG, JPEG, or WebP, up to 30 MB). Returns 201 with id, name, dimensions, and a thumbnail URL.
/api/v1/assets/api/v1/assets/{id}Use an asset in any render via the asset_id slot option:
-F 'options={"slots":{"Front Design":{"asset_id":"ast_8f3k2"}}}'Renders (sync)
/api/v1/rendersThe workhorse. Multipart form: template_id, optional options JSON, and designs supplied any of three ways — an uploaded file part named after the slot, a design_url slot option, or an asset_id slot option. A slot can also be just a color fill with no design at all.
curl -H "Authorization: Bearer $KEY" \
-F "template_id=$TEMPLATE" \
-F "Front Design=@art.png" \
-F 'options={
"format": "png", "image_size": 2048, "mode": "full",
"slots": {
"Front Design": {"fit":"contain","scale":0.8,"rotation":-4},
"Shirt Color": {"color":"#111827"}
}
}' \
https://your-domain.com/api/v1/renders -o out.pngThe response is the image itself, with X-Render-Id and X-Render-Ms headers. Add store=true to also keep a copy in your workspace (retrievable later via /api/v1/render-jobs/{id}/result). Rendered files never expire.
Render options
Top-level keys of the options JSON object (simple ones may also be sent as plain form fields):
| Option | Values | Description |
|---|---|---|
format | png · jpeg · webp (default png) | Output encoding. PNG keeps transparency. |
image_size | 16 – 8192 | Output width in pixels. Default: the template's native canvas width. |
background | template · transparent | "template" composes onto the product photo; "transparent" renders the warped placement alone with real alpha — ideal for layering. Requires mode: fast. |
mode | fast · full | Rendering pipeline — see quality modes. |
quality | 1 – 100 (default 90) | JPEG/WebP encode quality. |
supersample | 1 – 4 (default 2) | Anti-aliasing factor for the warp resampling. |
fit | cover · contain · stretch | Default fit for slots that don't set their own. |
slots | object | Per-slot placement and styling — see slot options. |
Slot options
Keys of options.slots.<slot>. The slot key is the layer name, layer id, or smart-object uuid (listed in the template detail response). All options compose — a design can be tiled, recolored, adjusted, and repositioned in one call.
Content
| Option | Values | Description |
|---|---|---|
design_url | https URL | Fetch the design from a public URL — the no-code favorite. |
asset_id | string | Use a design from your asset library. |
color | hex or CSS name | Fill the slot with a flat color — alone (product color variants) or under a design. |
Placement
| Option | Values | Description |
|---|---|---|
fit | cover · contain · stretch | How the design fills the slot. |
offset_x / offset_y | pixels (source space) | Nudge the design inside the slot. |
scale | number (1 = fitted size) | Resize around the slot center. |
rotation | degrees | Rotate around the slot center. |
Adjustments (applied to the design before warping)
| Option | Range | Description |
|---|---|---|
opacity | 0 – 100 (default 100) | Design transparency. |
brightness | -150 – 150 | Lighten / darken. |
contrast | -100 – 100 | Flatten / punch up. |
saturation | -100 – 100 | Desaturate / saturate. |
vibrance | -100 – 100 | Boosts muted colors more than vivid ones. |
blur | 0 – 100 | Gaussian blur (0–10 px at design resolution). |
Pattern tiling
| Option | Values | Description |
|---|---|---|
pattern | true · false | Tile the design seamlessly across the whole slot (all-over prints, wrapping paper, fabric). |
pattern_scale | 2 – 400 (default 50) | Tile width as a percentage of the slot width. offset_x/offset_y shift the tile origin. |
Quality modes
| Mode | What it does | When to use |
|---|---|---|
fast | Composites your warped design over the flattened template. ~1 s typical. Supports transparent backgrounds. | Interactive editors, previews, most production renders. |
full | Re-composites the original PSD layer stack with your design injected — shadow, lighting, and texture layers above the slot modulate the artwork exactly as in Photoshop. | Hero shots and templates with overlay effects. Slower; template background only. |
-F 'options={"mode":"full"}'Async jobs + webhooks
/api/v1/render-jobsIdentical fields to /renders, but returns 202 immediately with a job id. Add webhook_url and we POST the result to you — no polling.
curl -H "Authorization: Bearer $KEY" \
-F "template_id=$TEMPLATE" -F "Front Design=@art.png" \
-F "webhook_url=https://yourapp.com/hooks/mockups" \
https://your-domain.com/api/v1/render-jobs
→ 202 {"id":"rj_7h2k","status":"queued","mode":"async", ...}/api/v1/render-jobs/api/v1/render-jobs/{id}/api/v1/render-jobs/{id}/resultList recent jobs (?limit=), poll status, or fetch the finished image. Stored results never expire.
Webhook delivery
Events: render.completed / render.failed (and bulk.completed / bulk.cancelled for bulk jobs). Each delivery carries the event name in X-MGW-Event and an HMAC-SHA256 signature of the raw body (hex) in X-MGW-Signature, signed with the secret from your API keys page. Failed deliveries are retried once.
{
"id": "rj_7h2k",
"status": "done",
"template_id": "tpl_x91",
"error": null,
"duration_ms": 1043,
"result_url": "https://your-domain.com/api/v1/render-jobs/rj_7h2k/result"
}Verifying signatures
# Python
import hmac, hashlib
def verify(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)// Node
import { createHmac, timingSafeEqual } from "crypto";
function verify(rawBody, signature, secret) {
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}Batch renders
/api/v1/renders/batchUp to 50 variations in one call. Attach design files under any field names, plus a batch JSON array referencing them. Results are stored and returned as URLs; per-item errors don't fail the batch.
curl -H "Authorization: Bearer $KEY" \
-F "artA=@design-a.png" -F "artB=@design-b.png" \
-F 'batch=[
{"template_id":"tee-front","designs":{"Front Design":"artA"},"label":"tee-a"},
{"template_id":"tee-front","designs":{"Front Design":"artB"},"label":"tee-b"},
{"template_id":"mug-right","designs":{"Wrap":"artA"},
"options":{"slots":{"Wrap":{"scale":0.9}}},"label":"mug-a"}
]' \
https://your-domain.com/api/v1/renders/batch
→ {"total":3,"successful":3,"failed":0,
"renders":[{"label":"tee-a","status":"done","render_id":"rj_1","url":"…"}, …]}Items may also use design_url / asset_id slot options instead of uploaded files. You're only billed for successful renders.
Bulk generation
/api/v1/bulk-jobsThe full matrix: designs × templates × colors rendered server-side into one zip (up to 1000 renders per job). Returns 202 — the job keeps running even if your client disconnects.
curl -H "Authorization: Bearer $KEY" \
-F "design-1=@a.png" -F "design-2=@b.png" \
-F 'spec={
"templates": [
{"id": "tee-front"},
{"id": "hoodie-front", "preset_id": "pst_3k2"},
{"id": "mug-right", "design_slot": "Wrap", "color_slot": "Mug Color"}
],
"assets": ["ast_8f3k2"],
"colors": ["#111827", "#f8fafc", "olive"],
"options": {"format": "png", "image_size": 2000},
"webhook_url": "https://yourapp.com/hooks/bulk"
}' \
https://your-domain.com/api/v1/bulk-jobs| Spec key | Type | Description |
|---|---|---|
templates | array (required) | Templates to render. Each entry: id; optional design_slot / color_slot to pin which slot gets the design / color (defaults to the largest slot); optional preset_id — a saved placement preset, snapshotted at submission so later edits don't change a running job. |
assets | array of asset ids | Designs from your library, combined with any uploaded files. |
colors | array | Color variants — the matrix multiplies by these. |
options | object | Same render options as /renders, applied to every cell. |
webhook_url | url | Get bulk.completed / bulk.cancelled with the zip's result_url. |
/api/v1/bulk-jobs/api/v1/bulk-jobs/{id}/api/v1/bulk-jobs/{id}/cancel/api/v1/bulk-jobs/{id}/resultStatus responses include live completed/total progress and credits_used. Honest billing on cancel: stop a job at the 8th of 50 renders and you're charged exactly 8 — and the 8 finished images are still delivered as a partial zip via result_url.
Print files
/api/v1/renders/print-filesProduction exports: each slot's content composed flat — no warp, no product photo — at the slot's full source resolution, as PNG with embedded DPI metadata. The file your print shop actually needs, from the exact same placement options as your mockup render.
curl -H "Authorization: Bearer $KEY" \
-F "template_id=$TEMPLATE" -F "Front Design=@art.png" \
-F "image_dpi=300" \
https://your-domain.com/api/v1/renders/print-files
→ {"print_files":[
{"slot":"Front Design","width":4500,"height":5400,"dpi":300,"url":"…"}
]}image_dpi: 72–1200, default 300. One render credit per file. Slots with only a color fill export as flat color sheets.
Placement presets
Save a placement (fit, offsets, scale, rotation, …) per template once — usually from the visual editor — then reuse it in API renders and bulk jobs for consistent positioning across thousands of files.
/api/v1/presets{"template_id":"tee-front","name":"chest small",
"slots":{"Front Design":{"fit":"contain","scale":0.45,"offset_y":-380}}}/api/v1/presets?template_id={id}/api/v1/presets/{id}Apply via preset_id in a bulk spec entry, or copy its slots object into any render's options.
Embedded editor
Let your customers personalize products on your site. Issue a website key (wk_…) — safe for client-side code, scoped to read-and-render only:
/api/v1/keys/website-keyThen drop in the iframe:
<iframe
src="https://your-domain.com/embed/TEMPLATE_ID?wk=wk_xxxxxxxx"
style="width:100%;height:640px;border:0"
></iframe>
<script>
window.addEventListener("message", (e) => {
if (e.data?.type === "mockup:rendered") {
console.log("render ready:", e.data.url);
}
});
</script>For headless integrations the same key works against the embed API:
/api/embed/templates/{id}/api/embed/templates/{id}/renderSend the key as an X-Website-Key header or ?wk= query param. The render endpoint accepts the same multipart fields and slot options as /api/v1/renders.
Media URLs
| URL | Returns | |
|---|---|---|
/media/templates/{id}/preview.jpg | Template preview (720 px JPEG). | |
/media/assets/{id}/thumb | Asset thumbnail. | |
/media/assets/{id} | Asset original. | |
/media/renders/{job_id} | A stored render result. |
Asset and render media require auth (or a presigned URL, which result payloads already contain when S3-compatible storage is configured).
Errors & limits
Errors are JSON: {"detail": "human-readable message"}.
| Status | Meaning | |
|---|---|---|
401 | Missing or invalid API key / website key. | |
402 | Plan quota reached (monthly renders, templates, or assets). The message shows your usage. | |
404 | Resource not found — or owned by a different account. | |
413 | Design file exceeds 30 MB. | |
422 | Validation error: unknown slot, bad option value, malformed JSON, oversized bulk matrix. | |
500 | Render failed — the message includes the engine error. |
| Limit | Value | |
|---|---|---|
Max output width | 8192 px | |
Max design upload | 30 MB | |
Batch size | 50 renders per call | |
Bulk matrix | 1000 renders per job | |
Webhook retries | 1 retry on connection failure |
Full machine-readable schema: interactive OpenAPI explorer · openapi.json. Questions the docs don't answer? They should — tell us.