Composite artwork onto a product and get the picture back. format: "svg" (the default) answers a data URI, the same in test and live mode, with the artwork's own bytes inlined (artwork_inlined: true) so it paints anywhere, inside an <img> included: the artwork is read server-side through the same guarded, size-capped fetch the raster formats use. The inlined artwork is fitted to the print area's own pixels first (a PNG where it has transparency, else a JPEG), so the answer stays a bounded size; artwork that cannot be read safely, or is still over 512 KB once fitted, is referenced by its URL instead (artwork_inlined: false, with artwork_inline_reason), and an SVG loaded as an image does not load a referenced file. thumbnail_url is its own 300 px render, never a second copy of mockup_url. A product type that is one of our own plant's SKUs naming its size (POSTER-18X24, DECAL-WALL-24X36) is drawn at that size's proportions, in every format (size_in). format: "png" or "jpeg" answers mockup_url as a STORED, publicly fetchable image plus content_type, width and height: that is the form a storefront can publish, because a data URI is not something Shopify or TikTok will fetch and an SVG loaded as an image may not load the artwork it references. If storage is not configured the render still comes back, inline as a data URI, with stored: false and storage_reason saying why. A stored render also answers asset_id — the Asset row DELETE /v1/assets/{id} takes — so a caller can delete its own render without matching it by URL in GET /v1/assets; it is null, not absent, when stored is false. With cut_line: true on a die-cut SKU naming its size (STICKER-DIE-CUT-3X3), the mockup also shows the cut line, in every format, for a print proof: the outline of the artwork's transparent background, offset outward by the SKU's bleed, as a thin dashed magenta line (cut_line in the response says whether it was drawn; off by default, so a product photograph is unchanged).
- Credential
- An API key in
x-api-key - Scopes
mockups:write
Parameters
Idempotency-Keyheader · string (uuid)Unique key for idempotent requests. If you retry a request with the same key within 24 hours, you receive the original response. The key is bound to the request payload: reusing it for a different payload returns 422
idempotency_key_reuserather than the original response, so a changed request is never silently dropped. The binding is over the JSON's content, not its bytes — keys in another order or other whitespace are the same request. Generate a new key for each new request. On an operation that moves money (an order, a batch, an item, a shipping or address change, a cancellation's refund), a key that cannot be checked because the idempotency ledger is unreachable is answered 503idempotency_unavailablewithRetry-After, and nothing runs or is charged: retry with the same key.
Request body application/json, required
artwork_urlstring (uri)requiredURL of the artwork file, or a data: URI (the form /designs/generate answers). For a raster format an https URL is fetched and the bytes must be a PNG, JPEG, WebP or SVG — the extension and the served content type are not trusted.
product_typestringrequiredProduct type for the mockup (sticker, poster, magnet, …) or a SKU. A SKU of our own plant's that names its size is drawn at that size's proportions (
size_in). Where the portal sells only our own plant's products, a product type it does not make (a tee, a mug) is a 400 validation_error withdetails.product_type: ["not_offered"].backgroundstringBackground style for the mockup
One of
whitetransparentlifestyleDefault white
anglestringViewing angle for the mockup
One of
frontangledflat-layDefault front
formatstringsvg returns a data URI (unchanged default). png and jpeg return a stored, publicly fetchable image — the form a store listing can carry.
One of
svgpngjpegDefault svg
sizeintegerSquare edge in pixels for the raster formats. The SVG is always 1200.
Default 1200 · 200 to 2000
cut_linebooleanOn a die-cut SKU (STICKER-DIE-CUT-3X3), draw the cut line over the artwork: a thin dashed magenta line along the outline of the artwork's transparent background, offset outward by the SKU's bleed, where the plant's blade goes. Opt-in, for a print proof: off by default, because a mockup is what a seller publishes as a product photograph. No effect on a product cut along a rectangle. The response's
cut_linesays whether it was drawn.Default false
Request
POST /api/v1/mockups/generate
curl -X POST "https://portal.printedsimply.com/api/v1/mockups/generate" \
-H "x-api-key: ps_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: generate-mockup-$(date +%s)" \
-d '{
"artwork_url": "https://example.com/logo.png",
"product_type": "sticker",
"background": "white",
"format": "png",
"size": 1200
}'Response 200
Mockup generated
data
mockup_urlstringrequiredThe render. A hosted URL when
storedis true, otherwise an inline data URI.thumbnail_urlstringA 300 px render of the same mockup: for
svg, its own data URI with the artwork fitted to it; for a stored raster, the render's URL; for an unstored raster, a 300 px inline copy. Never a second full-size copy ofmockup_url.asset_idstring | nullast_…for the stored render — whatDELETE /v1/assets/{id}takes. Null whenstoredis false, because there is no row to name.product_typestringrequiredbackgroundstringOne of
whitetransparentlifestyleanglestringOne of
frontangledflat-layformatstringOne of
svgpngjpegcontent_typestringOne per
format. A narrower list thanPOST /v1/assetsaccepts: no render produces a PDF.One of
image/svg+xmlimage/pngimage/jpegstoredbooleanrequiredFalse when this deployment has no asset storage; the render is then inline only.
storage_reasonstringWhy the render was not stored. Present only when
storedis false.widthintegerheightintegerdimensionsobjectdimensions.widthintegerdimensions.heightintegersize_innumber[] | nullThe print's [width, height] in inches the product was drawn at, read from a SKU of our own plant's that names its size; null when the product type names none, and the family's own plate was drawn.
2 to 2 entries
cut_lineobject | nullThe die-cut line drawn on the mockup: null when
cut_line: truewas not sent, or the product is not cut around its artwork. On a die-cut SKU,drawnsays whether the line is on the picture;offset_inis the SKU's bleed, the distance the line sits outside the artwork;piecesis how many separate outlines were drawn (more than one means the shapes are too far apart to cut as one sticker: seevalidate_artwork'sno_cutline);not_drawn_reason, when not drawn: artwork_unread (the file could not be read or decoded within the bound), no_print_size, no_transparency (cut as a rectangle), empty, too_complex or photo_plate.cut_line.drawnbooleanrequiredcut_line.offset_innumber | nullrequiredThe SKU's bleed in inches: how far outside the artwork the line runs.
cut_line.piecesintegerPresent when
drawn: separate outlines on the picture.cut_line.not_drawn_reasonstringPresent when not
drawn: why.One of
artwork_unreadno_print_sizeno_transparencyemptytoo_complexphoto_plateartwork_inlinedbooleanSVG only: true when the artwork's bytes are inside the SVG, so it paints anywhere, an
<img>included; false when it is referenced by URL (seeartwork_inline_reason).artwork_inline_reasonstringSVG only, when
artwork_inlinedis false: why the artwork is referenced — artwork_blocked, artwork_unreachable, artwork_too_large (over the 15 MB read limit, or still over 512 KB once fitted to the print area), artwork_unsupported_type or artwork_empty.generated_atstring (date-time)
meta
pageintegertotalintegerlimitintegerThe page size actually applied, which is not necessarily the one asked for — the operation's own ceiling wins.
pagesintegertotaloverlimit, rounded up.next_cursorstring | nullOn a cursor-paged operation: send this back as
cursorfor the next page. Null on the last page. Absent on operations that do not page by cursor.has_morebooleanOn a list operation whose
metaisListMeta: whether another page follows this one.
{
"status": "success",
"data": {
"mockup_url": "https://assets.example.com/assets/1792076422000-945098792998-mockup-sticker-front.png",
"thumbnail_url": "https://assets.example.com/assets/1792076422000-945098792998-mockup-sticker-front.png",
"asset_id": "ast_MeOk7RY2Euc22EZ15k_2agc2",
"product_type": "sticker",
"background": "white",
"angle": "front",
"format": "png",
"content_type": "image/png",
"stored": true,
"width": 1200,
"height": 1200,
"dimensions": {
"width": 1200,
"height": 1200
},
"size_in": null,
"cut_line": null,
"generated_at": "2026-10-15T15:00:22.000Z"
},
"request_id": "4e3753e1-48dd-4313-bfd8-48e4af4e116a"
}Errors
Invalid request body or parameters. When the request failed the operation's schema,
detailscarriesformErrors(problems with the request as a whole),fieldErrors(messages keyed by top-level field) anderrors: one{ pointer, code, message }per refused value,pointeran RFC 6901 JSON pointer into the body (or into the object of query parameters, for a query parameter) —/items/0/sku, with~written~0and/written~1, and""for the request as a whole. POST /v1/orders/bulk answerserrorsas one{ index, details }per refused order, eachdetailswith its own pointers from the request body (/orders/3/items/0/sku); POST /v1/orders/multi-ship keepserrorsas the{ formErrors, fieldErrors }it always published; POST /v1/mockups/generate and POST /v1/products/recommend keep their fields at the top ofdetails, witherrorsbeside them. A refusal of a rule the schema cannot state (an asset that does not exist, an expiry too soon) names it inmessageand carries noerrors.Missing, invalid or expired API key:
missing_api_key(none sent),invalid_api_key(unknown or revoked),key_expired(past the expiry set when it was created; create a new key).payload_too_large — the request body exceeded this route's byte cap (256 KB unless the route raises it with a written reason). It is refused before it is parsed, so nothing was created. Send fewer rows, or use the endpoint's own asynchronous door where it has one.
Rate limit exceeded. Two windows can produce this status and
error.codesays which:rate_limit_exceededfor the per-minute sliding window,daily_quota_exceededfor the plan's daily quota. Both carryRetry-After; the daily one also carriesX-Daily-LimitandX-Daily-Remaining. The per-minute limits are plan-based — see Rate Limits above, or call GET /v1/me for this credential's own figures.503Production system error