Shopper App API
Base URL https://share.plunk.io. The API behind
the consumer-facing visualizer apps: use the one-line embed, or call the same
endpoints from your own shopper experience.
Keys & authentication
Keys are minted in Studio under Visualizer Apps; the raw key is
shown once, at creation. Every API request carries it in the
x-visualizer-key header. There is no shopper login:
the key is the app's identity.
| Key type | Prefix | Where it runs | Rules the API enforces |
|---|---|---|---|
| Device | kk_ |
Controlled in-store displays and showroom tablets you manage | No origin check, no daily cap by default; the device itself is the security boundary. |
| Site | sk_ |
Embedded in your public website, visible by design | Origin-locked to the domains registered on the key, daily render cap, per-IP burst limiting, and optional capture-first (an email is required before a render is spent). |
Each key is also scoped to surfaces:
com (the COM/COL Visualizer),
sectional (the Sectional Builder), or both. Your
app declares which surface it is with the x-surface
header (or ?surface=); requests outside the key's
scope are refused with 403.
Website embed
The fastest integration is no integration: one script tag renders the full app in an auto-sizing iframe where the tag sits. Use a site key.
<script src="https://share.plunk.io/embed.js"
data-key="sk_XXXXXXXXXXXX"
data-surface="com"></script>
<!-- data-surface: "com" (COM/COL Visualizer) or "sectional" (Sectional Builder)
data-height: optional starting height in px (default 720); the frame then
follows the app's reported height automatically -->
GET/embed?key=&surface=
The iframe fallback for CMSes that strip scripts: point a plain
<iframe> at it. The response carries a
per-key Content-Security-Policy: frame-ancestors
header listing the key's registered domains (and their subdomains), so the
browser itself refuses to frame your app anywhere else. A key with no
registered origins embeds anywhere. Site keys only; a device key here
returns 403.
App config
Building a custom shopper app? Bootstrap from the key: branding and behavior flags come from your workspace, not your code.
GET/visualizer/config
curl https://share.plunk.io/visualizer/config \
-H "x-visualizer-key: kk_XXXXXXXXXXXX"
{
"storeName": "Main Street Showroom",
"brand": "Your Company Name",
"keyType": "device",
"captureFirst": false,
"surfaces": ["com", "sectional"],
"tenantId": "a123456789012"
}
tenantId lets an app launched with only a key
resolve its catalog provisioning without a slug in the URL.
Visualizer renders com
The COM/COL flow: the shopper picks a piece and a material (including a photo of their own fabric or leather) and sees the piece rendered in it. Renders ride your workspace's render pipeline and spend its credit pool.
POST/visualizer/render
| Field | Type | Notes |
|---|---|---|
productImageUrl | string | Required. The piece to render |
swatchUrl | string | Required. The material: a swatch image, or the shopper's own photo |
aspectRatio | string | Default 1:1 |
email | string | Required up front only when the key is capture-first |
curl -X POST https://share.plunk.io/visualizer/render \
-H "x-visualizer-key: sk_XXXXXXXXXXXX" -H "Content-Type: application/json" \
-d '{
"productImageUrl": "https://your.cdn/pieces/track-arm-sofa.jpg",
"swatchUrl": "https://your.cdn/swatches/customer-linen.jpg"
}'
{
"runId": "9f4c1e07b2…"
}
| Status | Code | When |
|---|---|---|
422 | capture_required | Capture-first key, no valid email in the body |
429 | rate_limited | Per-IP burst limit on site keys |
429 | daily_cap_reached | The key's daily render cap is spent |
402 | The workspace is out of render credits |
GET/visualizer/render/{runId}
Poll until the render lands.
{
"status": "processing",
"outputUrl": null
}
# …and when it's done:
{
"status": "completed",
"outputUrl": "https://…/render.png"
}
Lead capture
The take-home moment: the shopper leaves an email, gets a branded share link to their render, and the lead, with product and material interest attached, lands in your workspace.
POST/visualizer/capture
| Field | Type | Notes |
|---|---|---|
email | string | Required. |
renderUrl | string | With it, a 60-day share link is minted and returned |
productSku | string | Interest attribution |
productName | string | Titles the share link |
fabricName | string | Interest attribution |
{
"ok": true,
"shareUrl": "https://share.plunk.io/v/a81f04c2be55d1"
}
Sectional Builder sectional
The configurator flow: bootstrap the unit and fabric libraries, render the shopper's exact configuration, and save it to a branded board. Approved popular combinations serve instantly from cache; new ones render on demand, unless your workspace runs in curated mode, where only staff-published configurations serve.
GET/sb/bootstrap
Everything the app needs to draw itself. price
is null when the key hides pricing (trade and
designer apps); builderMode is
open or curated;
publishedConfigs is populated in curated mode.
{
"brand": "Your Company Name",
"captureFirst": false,
"showPricing": true,
"builderMode": "open",
"units": [
{
"sku": "3772-ALC",
"series": "3772",
"title": "Armless Chair",
"unitType": "armless",
"armSide": null,
"attachEdges": "left,right",
"gridW": 1,
"gridH": 1,
"widthIn": 32.0,
"price": 899.00,
"imageUrl": "https://…/3772-alc-neutral.png"
}
],
"fabrics": [
{
"fabricKey": "2001-05",
"fabricName": "Stone",
"swatchUrl": "https://…/2001-05.jpg",
"family": "Performance weave"
}
],
"options": [
{
"kind": "leg",
"optionKey": "espresso",
"name": "Espresso legs",
"swatchUrl": "https://…/leg-espresso.jpg",
"sort": 1
}
],
"publishedConfigs": []
}
POST/sb/render
Render a configuration: 2–8 units placed on the layout grid, a fabric, and any
selected options. A cached, approved configuration returns
ready immediately; otherwise the render starts
and you poll. In curated mode an unpublished combination returns
403 with
status: "unavailable".
curl -X POST https://share.plunk.io/sb/render \
-H "x-visualizer-key: sk_XXXXXXXXXXXX" -H "x-surface: sectional" \
-H "Content-Type: application/json" \
-d '{
"units": [
{ "sku": "3772-LAL", "gx": 0, "gy": 0 },
{ "sku": "3772-ALC", "gx": 1, "gy": 0 },
{ "sku": "3772-COR", "gx": 2, "gy": 0 },
{ "sku": "3772-RAC", "gx": 2, "gy": 1 }
],
"fabricKey": "2001-05",
"options": { "leg": "espresso" }
}'
# Cache hit. An approved render serves instantly:
{
"status": "ready",
"url": "https://…/hero.png",
"configHash": "b04c…"
}
# Cache miss: 202, poll /sb/render/{renderId}; `fallback` carries the
# per-unit images so the app can show its exploded view meanwhile:
{
"status": "rendering",
"renderId": "f2a91c…",
"configHash": "b04c…",
"fallback": {
"units": [
{ "sku": "3772-LAL", "url": "https://…/3772-lal-neutral.png" }
]
},
"schematicUrl": "https://…/schematic.png"
}
GET/sb/render/{renderId}
Poll the render. Every result is fidelity-checked before it can serve;
pending_review means it rendered but is waiting
on staff approval and will not serve until approved.
{ "status": "rendering" }
{ "status": "pending_review" }
{ "status": "failed", "verifyNote": "…" }
{
"status": "ready",
"url": "https://…/hero.png"
}
POST/sb/save
Save the configuration, and when the shopper leaves an email, mint a branded
board with the hero render and email them the link. If the render is still in
flight, pass the renderId from
/sb/render: the board minting waits for the
render and the email goes out when it's ready.
| Field | Type | Notes |
|---|---|---|
units | array | Required. Same placement objects as /sb/render |
fabricKey | string | Required. |
options | object | Selected options, kind → key |
title | string | Board title, default "My sectional" |
email | string | Captures the lead + emails the board |
renderId | string | An in-flight render to attach the board to |
{
"configId": "8c1b04d2ee…",
"totalPrice": 4796.00,
"boardUrl": "https://share.plunk.io/b/1f8e02aa41cd6b",
"pendingBoard": false,
"message": null
}
# totalPrice is null when the key hides pricing. If the render was still
# in flight: pendingBoard = true and the board link is emailed on approval.