Adding UGC to a Custom Storefront with the Idukki REST API + Webhooks
A custom or headless storefront can show shoppable UGC without dropping in a vendor widget: fetch a gallery over a REST API, render it in your own React or Vue components, and keep it fresh with webhooks. Here is the integration path, with representative code.
The lead engineer on a headless build had a one-line requirement from the marketing team: "put the customer videos on the product page." Easy, until she opened the vendor's install guide and found a third-party script that wanted to mount its own DOM, ship its own CSS, and phone home on every page view. Her storefront was a hand-built Next.js app with a strict bundle budget and a CSP that did not allow arbitrary external scripts. She did not want a widget. She wanted data.
Adding UGC to a custom storefront over an API means you fetch the gallery as structured JSON, render it with your own components, and subscribe to webhooks so new or moderated content updates without a redeploy. You own the markup and the performance budget; the platform owns collection, rights, tagging, and the feed. No injected widget script required.
That separation matters most for headless and bespoke stacks. If you are on Shopify or Woo, the drop-in widget is genuinely the fast path. But a React, Vue, or Astro storefront with its own design system usually wants the content as a payload, not as someone else's iframe. The same JSON that feeds your front end can also feed your email and Klaviyo flows, so one source of truth drives several surfaces.
In this article
0%
of shoppers trust UGC more than brand-made content
Representative of Bazaarvoice / Stackla consumer-trust surveys
0.0s
recommended LCP ceiling on mobile for a "good" score
Google Core Web Vitals thresholds
0%
lift in conversion when shoppers interact with UGC
Representative range from Bazaarvoice Shopper Experience Index
What does the API surface look like in 5 minutes?
Strip away the marketing and a UGC platform exposes three things a storefront actually needs: a way to read a gallery, a way to read the products tagged inside it, and a way to be told when either changes. Everything else (collection, moderation dashboards, social OAuth) lives in the platform and never touches your front end.
A gallery read returns an ordered list of posts. Each post carries its media (image or video, with the playable file URL, not a social permalink), the author handle, a rights flag so you never render content you do not have permission to use, and the products tagged on it. Those tagged products are what turn a video into a shoppable one: each carries an id, title, price, and a link or variant you can wire to add-to-cart. The shapes below are representative of that contract, not a verbatim endpoint reference; check the live API docs for exact field names and routes.
"It's just a JSON feed" — what sits below the waterline
GET /v1/galleries/{id} → JSON
One read your storefront renders
What your front end sees
What you actually build and maintain
- Social collection + OAuth
Instagram, TikTok, YouTube, X, LinkedIn, Threads pulled and normalised
- Rights & consent
Automated permission requests and a per-post rights flag you can trust
- AI tagging + curation
Products matched to clips; brand-safe scoring decides what surfaces
- Media pipeline + CDN
Transcoding, thumbnails, deterministic URLs behind a CDN
- Moderation + analytics
Removal, blocking, and per-post performance tracking
How do I authenticate and fetch a gallery?
Authentication is an API key issued per business. Treat it like any other server secret: it belongs in your backend or an edge function, never shipped to the browser. The pattern is a bearer token on a server-side fetch, then you hand the already-shaped JSON to your client. The response below is illustrative of the gallery contract.
// Representative shape of GET /v1/galleries/{galleryId}
{
"galleryId": "gal_8f21",
"title": "Spring lookbook",
"posts": [
{
"id": "post_4c10",
"source": "Instagram",
"author": "Golfbreakscom",
"rights": { "status": "granted", "grantedAt": "2026-05-02" },
"media": {
"type": "video",
"poster": "https://cdn.idukki.io/p/4c10/poster.jpg",
"file": "https://cdn.idukki.io/p/4c10/hd.mp4"
},
"products": [
{
"id": "prod_77",
"title": "WROGN Men Silver-Toned Watch",
"price": "$24.76",
"url": "https://shop.example.com/products/wrogn-watch"
}
]
}
],
"updatedAt": "2026-06-24T09:12:00Z"
}// Server-side fetch (Next.js route handler / edge function).
// Key stays on the server; the browser only ever sees the JSON.
export async function getGallery(galleryId: string) {
const res = await fetch(
`https://api.idukki.io/v1/galleries/${galleryId}`,
{
headers: { Authorization: `Bearer ${process.env.IDUKKI_API_KEY}` },
// Cache the feed; let a webhook bust it (see below).
next: { revalidate: 3600, tags: [`gallery:${galleryId}`] },
}
);
if (!res.ok) throw new Error(`gallery fetch failed: ${res.status}`);
return res.json();
}The integration path, end to end
- 01
Auth
Issue an API key per business and store it server-side. Bearer token on every read.
one-time
- 02
Fetch
GET the gallery as JSON from a server route or edge function. Never call from the browser with the key.
cached read
- 03
Render
Map posts to your own components: media, author, rights-gated display, tagged products wired to add-to-cart.
your DOM
- 04
Webhook
Subscribe to approved / re-tagged / removed events. On receipt, revalidate the cached feed.
live updates
How do I render it in React or Vue without the widget?
Once the JSON is on your server, rendering is ordinary front-end work. You map over posts, drop the media into your own card, and wire each tagged product to your existing add-to-cart action. Because it is your markup, it inherits your spacing tokens, your image-loading strategy, and your accessibility patterns instead of fighting them.
Two rules earn their keep here. Honour the rights flag: render only posts whose rights status is granted, so you never publish content you cannot legally use. And gate video preload so off-screen clips do not download. A carousel that sets preload="auto" on every slide can pull megabytes of unseen video on a mobile load and wreck your LCP; pass preload="none" to anything not in view. We have written more about that in Core Web Vitals for UGC widgets.
// Representative React render. Your design system, your rules.
function ShoppableGallery({ posts }: { posts: Post[] }) {
return (
<ul className="ugc-grid">
{posts
.filter((p) => p.rights.status === 'granted')
.map((post, i) => (
<li key={post.id}>
<video
poster={post.media.poster}
src={post.media.file}
// Only the visible window gets 'auto'.
preload={i < 3 ? 'auto' : 'none'}
muted
playsInline
/>
{post.products.map((prod) => (
<button key={prod.id} onClick={() => addToCart(prod.id)}>
{prod.title} — {prod.price}
</button>
))}
</li>
))}
</ul>
);
}What the shopper taps
Tap to shop
WROGN Men Silver-Toned Watch
$24.76
- 1
Rights-gated
Only granted posts render
- 2
Your DOM
No injected widget script
How do webhooks keep galleries fresh without a redeploy?
A static fetch goes stale the moment a moderator approves a new clip or re-tags a product. Polling fixes that badly: too slow and content lags, too fast and you hammer the API for no change. Webhooks invert it. The platform tells you the instant something changes, and you do exactly one thing in response: invalidate the cached feed for that gallery so the next request rebuilds it.
The events worth subscribing to are small in number: a post approved (add it), a post removed or blocked (drop it), and a product re-tagged (the shoppable links changed). Verify the signature header so you only act on genuine calls, then revalidate by tag. In Next.js that is one line.
// Representative webhook receiver.
export async function POST(req: Request) {
const raw = await req.text();
if (!verifySignature(raw, req.headers.get('x-idukki-signature'))) {
return new Response('bad signature', { status: 401 });
}
const event = JSON.parse(raw);
// post.approved | post.removed | post.retagged
if (event.galleryId) {
revalidateTag(`gallery:${event.galleryId}`); // bust the cache
}
return new Response('ok');
}Drop-in widget
Paste a snippet, done. Best on Shopify, Woo, BigCommerce.
Wins at
- Live in minutes, no engineering
- Updates ship from the platform
- Curation and rights handled for you
Struggles with
- Injects external script + CSS
- Less control over markup and CSP
- Harder to fit a strict bundle budget
REST API + webhooks
Fetch JSON, render your own components. Best on custom / headless.
Wins at
- Your markup, your design system, your CWV budget
- No third-party script under your CSP
- Same feed reused across front end, email, ads
Struggles with
- Requires front-end engineering time
- You own caching + webhook plumbing
- You build the player and add-to-cart wiring
Both are supported. The right answer is mostly about how much control you need over the markup.
How do I cache it so a visitor never waits on a live call?
The mistake is calling the API on every page render. UGC changes a few times a day, not a few times a second, so a per-visitor live call buys you nothing but latency and rate-limit risk. Cache the feed at the edge with a long revalidation window, then let the webhook bust the cache on real change. Your visitors get static-fast JSON; your content is still near-real-time because the moment a moderator acts, the next request rebuilds.
- Cache the gallery JSON by tag (gallery:{id}) at the edge or in your framework's data cache, with a generous revalidate window as a backstop.
- Bust by tag on webhook so changes appear within seconds, not on a poll interval.
- Pre-generate poster images and serve video lazily; never let off-screen clips download (preload="none").
- Set a sane stale-while-revalidate so a cache miss serves slightly old content instead of blocking the render.
- Treat the rights flag as load-bearing: filter at render time, and re-filter on every webhook rebuild.
A headless team does not want a widget. They want data they can render and a webhook that tells them when it changed. Give them that, and the storefront stays theirs.
Rohin Aggarwal, Co-founder, Idukki.io
Sources & further reading
- 1Google — Core Web Vitals thresholds (LCP, INP, CLS) · Performance budget for media-heavy pages
- 2MDN — HTTP caching · Cache-Control and revalidation patterns
- 3Bazaarvoice — Shopper Experience Index · UGC trust and conversion-lift ranges
- 4web.dev — Fast load times · Lazy-loading and media performance guidance
- 5Idukki — agentfeed & web feed (AEO surfaces) · Public JSON-LD, llms.txt, MCP, Klaviyo-compatible feed
More from Rohin Aggarwal
- Industry playbook
How to run a UGC competition that fills your gallery, online and in-store
The runbook for a UGC competition that actually fills the gallery: the mechanism, five formats, an end-to-end schedule, paste-ready copy templates, and the one thing ASOS, Starbucks, e.l.f. and Gymshark all got right that most brands skip.
- Conversational commerce
Why we built the Conversational PDP
Most product-page exits are a single unanswered question, asked silently. Here is the case for answering it on the page, from your own evidence, and the story of why we built a Q&A that is curated-first and AI-second.
- Strategy
PDP before and after UGC: what actually changes on the page
Strip a product page back to brand-only content, layer verified customer photos, video and reviews into the middle scroll, and watch what moves. A scroll-by-scroll look at the before and after, the numbers the public studies actually support, and where "just add UGC" gets oversold.