JavaScript SDK

An optional convenience for building mockup URLs and reading the catalog — in any JS environment.

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>:

html
<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.

Building in another language, or no framework at all? Skip this page — go straight to the URL primitive. The SDK adds nothing the URL can’t.

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.

npm install @snowcone-app/sdk

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" />

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.

tsx
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.

bash
# 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.

tsx
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)
Same grammar the raw URL uses — see Options and Multiple placements.

The exported types, for reference:

tsx
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.

tsx
// 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=…
});
The secret is server-side only — never ship it to a browser. For high-volume patterns, see Signing at scale.

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).

tsx
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"]
Heads up on shapes: 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.
The catalog is a standard Meilisearch index with a public read key, so any Meilisearch client works too — the SDK is just a typed shortcut. Under the hood 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.

tsx
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);
Two auth paths for 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.
This is the same primitive that powers mockups on snowcone.app. For the full flow — the grant proxy, the keyless sandbox path, multi-placement, saved-state-by-id, the id-space rules, and a copy-paste Next.js App Router quickstart — see the dedicated Realtime server-side render page. Import serializeStateForServer from @snowcone-app/canvas (its stable, semver-protected home — the SDK does not re-export it).

Next steps