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=
bg=
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
#   &bg=f5f0ea          background color behind the mockup → Background color
#   &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.

Background color

Some product scenes are studio cutouts with a transparent background. By default that transparency is preserved — the image composites over whatever your page puts behind it. To bake a color in instead, pass &bg= with a 6-digit hex (no #).

html
<!-- bg= paints a color behind the mockup (6-hex, no '#'). -->
<img src="https://img.snowcone.app/hoodie-black?asset=…&shop=YOUR_SHOP_ID&bg=f5f0ea" />
<!-- Only visible where the scene itself is transparent (studio-cutout shots). -->
<!-- Omit bg= to use your shop's configured default, else transparent. -->
<!-- bg=transparent forces the raw cutout, overriding a set shop default. -->
<img src="https://img.snowcone.app/hoodie-black?asset=…&shop=YOUR_SHOP_ID&bg=transparent" />

You can also set a shop-wide default so every render gets the color without touching URLs: in the dashboard under Shops → Branding → Mockup Background, or for a sandbox shop via the API (see Shops). Precedence is simple: a URL’s own bg wins, then the shop default, then transparent — and bg=transparent is the explicit escape hatch: it pins the raw cutout for one render even when the shop has a default set.

bg is part of the URL, so it’s part of the cache key: the same design with two backgrounds is two cached images. Changing the shop default only affects new renders — URLs already cached at the edge keep the background they rendered with.

Cost & timing headers

Every render tells you what it cost. The image response carries headers for 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. Fresh renders also include a Server-Timing stage breakdown — prepare, assets, queue, compute, encode, total — which Chrome DevTools renders as a waterfall in the request’s Timing tab, and which your code can read via the Resource Timing API (serverTiming on the resource entry).

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
# server-timing: cache;desc="miss", prepare;dur=12, assets;dur=310,
#                queue;dur=8, compute;dur=270, encode;dur=140, total;dur=742
# A cached repeat returns x-render-cache: hit — it isn't re-rendered (and isn't re-billed);
# its server-timing is just cache;desc="edge-hit" with the (tiny) total.
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: