Instant mockups

The core primitive: your artwork on a real product, as a public image URL rendered in under a second.

A mockup is a URL

The whole platform rests on one idea: a mockup is just an image URL. Three values — a product, your artwork, and your Shop ID — and you have your art on real, buyable merchandise. It’s an <img> source, so you can build it in any language with no SDK and no server to stand up.

<img src="https://img.snowcone.app/hoodie-black?asset=https%3A%2F%2Fcdn.example.com%2Fart.png&shop=YOUR_SHOP_ID" />

Your shop was created when you signed up — copy its ID at snowcone.app/studio/api-keys — public, like a Cloudinary cloud name. No account? Mint a sandbox shop on Get a Shop ID.

Try it

Change the product, artwork, and params below and watch the real render update. The URL it builds is the exact one you’d ship — copy it, swap in your own Shop ID and asset URL, and drop it into an <img>.

Product
Artwork (asset=)
width=
aspect=
Live Snowcone mockup render

Live render · demo shop (rate-limited)

https://img.snowcone.app/BEEB77?asset=https%3A%2F%2FdqMwU8Jct3.storage.snowcone.app%2Fdemo%2Fdemo-008.jpg&shop=YOUR_SHOP_ID

Why “instant”

Nothing is pre-computed. A brand-new image — one a shopper uploaded or an AI generated a second ago — renders on the fly in under a second, so you can show people their own art on merchandise at scale with no pipeline to manage. The URL is the render request and the cache key at once: the first hit renders, every hit after is served from the edge.

Because the URL fully describes the render, identical URLs are deduplicated and cached for free — no build step, no warm-up, no stale assets to invalidate.

Anatomy of the URL

The product is the path; everything else is a query param. Only asset and shop are required — the rest default from the product’s catalog record when omitted.

bash
https://img.snowcone.app / hoodie-black ? asset=… & shop=YOUR_SHOP_ID
└──────── host ────────┘   └─ product ─┘   └ artwork ┘ └─── Shop ID ───┘

# Optional, all defaulted from the catalog if omitted:
#   &placement=back     which print area        → Multiple placements
#   &variant=L_BLK      which size/color         → Options
#   &mockup=FV1qjO      which scene/angle
#   &width=1200         render width (snapped down; default 1400)
#   &aspect=2:3         center-crop to portrait — the only crop; omit it and
#                       the image keeps the scene's native shape
#   &signature=…        L3 signed URL            → Signed URLs

Render width

Add &width= to control resolution. Requests snap down to the largest allowed width that doesn’t exceed what you asked for — a floor, not a round-to-nearest — so URLs stay a small, stable set of cache keys rather than an infinite range. 900 snaps to 800, not 1000; omit width entirely and you get the default, 1400.

html
<!-- Ask for a width; the renderer snaps DOWN to the nearest allowed size. -->
<img src="https://img.snowcone.app/hoodie-black?asset=…&shop=YOUR_SHOP_ID&width=1200" />
<!-- allowed: 400 600 800 1000 1200 1400 1600 1800 2000 2500 3000 4000 -->
<!-- asking for 900 snaps to 800, not 1000 -->

Displaying mockups

The image keeps its scene’s native shape — 16:9 for most products, square for some — with the product composed centered. Don’t assume the dimensions: use object-fit: cover to fit any frame shape — cropping is safe.

html
<!-- Fit any frame shape with object-fit: cover. -->
<div style="aspect-ratio: 1 / 1; overflow: hidden;">
  <img
    src="https://img.snowcone.app/hoodie-black?asset=…&shop=YOUR_SHOP_ID&width=1600"
    style="width:100%; height:100%; object-fit:cover;" />
</div>
A cover crop shows only a slice of the render, so request about 2× the displayed size or it looks blurry: a 320–400px retina card wants width=14001600, not 800.

To crop in the render instead, pass &aspect=2:3 — it center-crops the scene to portrait, and it’s the only crop the renderer applies. Omit it (or pass 16:9) and no crop happens — you get the full scene at its native shape.

html
<!-- aspect=2:3 center-crops the scene to portrait. -->
<img src="https://img.snowcone.app/hoodie-black?asset=…&shop=YOUR_SHOP_ID&aspect=2:3" />
<!-- Omit aspect= (or pass 16:9) for the full scene at its native shape. -->
The URL responds with a 302 redirect to the rendered AVIF on cdn.snowcone.app. A plain <img> follows it transparently, but proxies that fetch the URL themselves need to allow both hosts — e.g. Next.js images.remotePatterns must list img.snowcone.app and cdn.snowcone.app.

Cost & timing headers

Every render tells you what it cost. The image response carries three headers — whether it was a cache hit or a fresh render, how long the render took, and what it cost in USD — so your very first curl doubles as a bill preview, and a slow page is diagnosable from the response alone.

bash
curl -i -L -o mockup.avif "https://img.snowcone.app/hoodie-black?asset=…&shop=YOUR_SHOP_ID"
# x-render-cache: miss        whether this hit rendered fresh (miss) or came from cache (hit)
# x-render-ms: 742            server render time for this image, in milliseconds
# x-render-cost-usd: 0.02     what this render cost, in USD
# A cached repeat returns x-render-cache: hit — it isn't re-rendered (and isn't re-billed).
Identical URLs are cached (see Why “instant”), so expect miss once per unique URL and hit after that. The realtime channel reports the same telemetry per render: each result carries renderMs, costUsd, and renderCache.

Where it leads

Once the mockup renders, the same URL grammar carries you the rest of the way: