JavaScript SDK
Applies to @snowcone-app/sdk@0.19.0
The mockup is a URL
You don’t need the SDK. A mockup is just an image URL — a product, your artwork, and your Shop ID — that you can build in any language and drop into an <img>:
<img src="https://img.snowcone.app/BEEB77?asset=https%3A%2F%2Fcdn.example.com%2Fart.png&shop=YOUR_SHOP_ID" />The @snowcone-app/sdk package is a small, optional convenience for JavaScript and TypeScript: it builds and signs those URLs so you don’t hand-roll query strings, and it reads the public catalog. Everything it does, you can do with a string — it just saves you the encoding and the typos. It ships TypeScript types.
Install
Add the package with npm. There’s no CLI, no postinstall, and nothing to configure — it’s a plain, logic-only library with no stylesheet.
TypeScript definitions are included — no separate @types package.
Build mockup URLs
The same mockup URL, three ways. The SDK’s getMockupUrl() is a pure, synchronous string builder — no fetch, no await — so its output is identical to the raw URL, just without the manual encoding.
<img src="https://img.snowcone.app/BEEB77?asset=https%3A%2F%2Fcdn.example.com%2Fart.png&shop=YOUR_SHOP_ID" />const asset = encodeURIComponent("https://cdn.example.com/art.png");
const src = `https://img.snowcone.app/BEEB77?asset=${asset}&shop=YOUR_SHOP_ID`;
// drop `src` into any <img>import { getMockupUrl } from "@snowcone-app/sdk";
// Pure + synchronous — returns a string. No fetch, no await, no secret.
const src = getMockupUrl("BEEB77", {
shop: "YOUR_SHOP_ID",
asset: "https://cdn.example.com/art.png", // encoded for you
});
// → https://img.snowcone.app/BEEB77?shop=YOUR_SHOP_ID&asset=…
// Use it anywhere a URL goes:
document.querySelector("img").src = src;Every mockup URL has a checkout twin: getBuyUrl() takes the exact same options and returns the hosted buy-page link — the conversion step of the same flow.
import { getBuyUrl, getMockupUrl } from "@snowcone-app/sdk";
// Same options shape as getMockupUrl — the <img> shows the design,
// the <a> sells it. Both pure + synchronous.
const opts = { shop: "YOUR_SHOP_ID", asset: "https://cdn.example.com/art.png" };
const img = getMockupUrl("BEEB77", opts); // → https://img.snowcone.app/BEEB77?…
const buy = getBuyUrl("BEEB77", opts); // → https://buy.snowcone.app/BEEB77?…
// The buy link opens the Snowcone-hosted checkout with this exact design
// in the cart — price and fulfillment resolve from your Shop ID.Your Shop ID is public, like a Cloudinary cloud name. Don’t have one yet? Mint a sandbox shop in one call — claiming, limits, and lifecycle are on Get a Shop ID. Never ship a Shop ID copied from these examples — those attribute sales to someone else.
# Mint your own Shop ID in one call — no signup required to start.
curl -X POST https://api.snowcone.app/shops/sandbox -H 'Content-Type: application/json' -d '{}'
# → { "shop_id": "…", "shop_secret": "scsec_…", "claim": { … } }Variants, placements, crop
The URL grammar is plain query params: opt.<attr> for variants, asset.<key> / color.<key> for extra print areas, and aspect for crop. The helper takes them as typed options and emits the right params for you.
const src = getMockupUrl("BEEB77", {
shop: "YOUR_SHOP_ID",
options: { size: "l" }, // → opt.size=l
design: { // → asset.<key> / color.<key>
front: "https://cdn.example.com/front.png",
back: { src: "https://cdn.example.com/back.png", tile: 2, align: "top" },
sleeve: { color: "#FF5733" }, // → color.sleeve=FF5733
},
aspect: "2:3", // → aspect=2:3 (the only crop: center-crop
}); // to portrait; omit = scene's native shape)The exported types, for reference:
import type { GetMockupUrlOptions, Design, Fill } from "@snowcone-app/sdk";
interface GetMockupUrlOptions {
shop: string; // your public Shop ID → &shop=
asset?: string; // single-placement image → &asset=
design?: Design; // multi-placement map → asset.<key> / color.<key>
options?: Record<string, string>; // variant picks → opt.<attr>
aspect?: "16:9" | "2:3"; // "2:3" center-crops; omit = scene's native shape
width?: number; // render width, snaps down (default 1400)
secret?: string; // server-side only → &signature
}
// A Fill is a string URL, { src, align?, tile? }, or { color }.
// A Design is a map of placement key → Fill.Signing
Public Shop IDs are fine for most shops. If you’d rather only your server mint valid URLs, pass your per-shop secret and the helper appends an &signature — the one place the SDK earns its keep, since the HMAC matches the edge verifier byte-for-byte.
// SERVER-SIDE ONLY — never ship the secret to a browser.
const src = getMockupUrl("BEEB77", {
shop: "YOUR_SHOP_ID",
asset: "https://cdn.example.com/art.png",
secret: process.env.SNOWCONE_SHOP_SECRET, // appends &signature=…
});Read the catalog
The mockup shows what they’re buying; pull the rest with getProduct(). It returns a CatalogProduct: not just name/price/description, but the fields the realtime renderer needs — mockups[].id (the mockupIds you render), placements[].label and requiredPlacements (the print-area names you pass to renderState, e.g. "Front"), and options.combinations[].variantId (the variantId required for color/variant products).
import { getProduct, formatPrice } from "@snowcone-app/sdk";
const product = await getProduct("BEEB77"); // CatalogProduct — any product code
// Identity + pricing
product.id; // "BEEB77"
product.name; // "Framed Canvas"
product.price; // 2399 (MSRP, in minor units / cents — suggested retail)
product.description; // string[] — paragraphs, NOT a single string
// Don't hand-roll a price formatter — one ships in the box:
formatPrice(product.price, "en-US", "USD", true); // "$23.99"
// Mockup scenes → product.mockups[].id are the opaque codes you pass as
// RenderSession mockupIds (e.g. "FV1qjO").
product.mockups?.[0]?.id; // "FV1qjO"
// Placements → the print-area labels. THIS is where "Front" comes from.
product.placements?.map((p) => p.label); // ["Front"]
// requiredPlacements is the renderer's contract: which labels renderState will
// demand, and which are auto-filled by the variant (color placements).
product.requiredPlacements; // [{ label:"Front", type:"image", … }, { label:"Frame", type:"color", autoFilledByVariant:true }]
// Variants → variantId for RenderSession / color products lives HERE.
const variantId = product.options?.combinations?.find((c) => c.variantId)?.variantId;
product.options?.attributesList; // ["Size","Mat","Frame"]description is a string array (paragraphs), and price is in minor units (cents). The placement labels a render needs live at product.placements[].label / product.requiredPlacements[].label, and a product’s variantId comes from product.options.combinations[].variantId — there is no separate endpoint to discover either.getProduct(id) fetches that index’s /documents/{id} with the same public read key — it’s the exact same catalog you can query directly with a raw bearer token, just returned as a typed CatalogProduct. Same data, choose the ergonomics you prefer. See the catalog docs.Realtime canvas render
The SDK isn’t only a URL builder. It also ships the realtime server-side render API — send a whole saved canvas (or just its id) over a WebSocket and the server renders the mockup, fetching the referenced artwork itself. The public surface: RenderSession, mintRealtimeGrant, createDesignState, and the React hook useRealtimeMockup (from @snowcone-app/sdk/react); serializeStateForServer comes from @snowcone-app/canvas.
import { RenderSession, mintRealtimeGrant, createDesignState, getProduct } from "@snowcone-app/sdk";
import { serializeStateForServer } from "@snowcone-app/canvas";
// 1. SERVER: mint a short-lived grant. Signature: mintRealtimeGrant({ apiKey?, shop }).
// e.g. inside POST /api/realtime/grant — the sk_ key never reaches the browser.
// PRODUCTION: pass a secret sk_ key (scope mockups:realtime).
const grant = await mintRealtimeGrant({
apiKey: process.env.SNOWCONE_API_KEY, // sk_… — omit entirely for a sandbox shop
shop: "YOUR_SHOP_ID",
});
// 2. BROWSER: open a session against your grant proxy and render a canvas state.
const product = await getProduct("BEEB77");
const variantId = product.options?.combinations?.find((c) => c.variantId)?.variantId;
const session = new RenderSession({
shop: "YOUR_SHOP_ID",
grantUrl: "/api/realtime/grant",
// variantId is required for color/variant products. Read it from the live catalog
// and pass options too so the SDK can validate missing/invalid variants immediately.
product: { productId: "BEEB77", mockupIds: ["FV1qjO"], variantId, options: product.options },
});
session.onMockups((r) => { img.src = r[0].imageUrl; });
session.onError((message: string) => console.error(message)); // onError gives a string
// First arg is the placement label (getProduct(...).placements[].label, e.g. "Front").
await session.renderState("Front", serializeStateForServer(canvasState), 200);mintRealtimeGrant({ apiKey?, shop }). A sandbox shop authorizes keyless — the publishable shop.id alone is enough, so omit apiKey. In production you pass a secret apiKey (an sk_ key with the mockups:realtime scope). Either way, mintRealtimeGrant runs server-side (e.g. in your /api/realtime/grant route) so the key never reaches the browser — the client only ever holds the short-lived grant. The Realtime page covers this sandbox-vs-production split in full.serializeStateForServer from @snowcone-app/canvas (its stable, semver-protected home — the SDK does not re-export it).
