# Adding UGC to a Custom Storefront with the Idukki REST API + Webhooks

A headless storefront can show shoppable UGC without a vendor widget: fetch the gallery over REST, render it in your own components, keep it fresh with webhooks.

By Rohin Aggarwal · 2025-12-15 · (updated 2026-06-13)

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](/blog/ugc-in-email-and-klaviyo-flows), so one source of truth drives several surfaces.

**Quick answer**

- Authenticate with an API key, then GET a gallery as JSON: posts, media URLs, tagged products, and rights status.
- Render the payload in your own React/Vue components so it inherits your design system, your lazy-loading, and your CSP.
- Subscribe to webhooks (post approved, product re-tagged, post removed) to keep galleries fresh without a redeploy.
- Cache the feed at the edge and revalidate on webhook so you serve static-fast JSON, not a live call per visitor.
- You keep the markup and the Core Web Vitals budget; Idukki keeps collection, rights, tagging, and curation.

- **88%** — of shoppers trust UGC more than brand-made content (Representative of Bazaarvoice / Stackla consumer-trust surveys)
- **2.4s** — recommended LCP ceiling on mobile for a "good" score (Google Core Web Vitals thresholds)
- **161%** — lift in conversion when shoppers interact with UGC (Representative range from Bazaarvoice Shopper Experience Index)

_Why the render path is worth controlling yourself_

## 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](/blog/what-is-shoppable-video): 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**
Above the waterline: GET /v1/galleries/{id} → JSON — One read your storefront renders
Below the waterline (What your front end sees):
- 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.

```json
// 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"
}
```

```ts
// 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**
1. **Auth** — Issue an API key per business and store it server-side. Bearer token on every read. (one-time)
2. **Fetch** — GET the gallery as JSON from a server route or edge function. Never call from the browser with the key. (cached read)
3. **Render** — Map posts to your own components: media, author, rights-gated display, tagged products wired to add-to-cart. (your DOM)
4. **Webhook** — Subscribe to approved / re-tagged / removed events. On receipt, revalidate the cached feed. (live updates)
_Four moving parts. Two are one-time setup; two run on every change._

## 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](/blog/core-web-vitals-ugc-widgets).

```tsx
// 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**
Product: WROGN Men Silver-Toned Watch ($24.76)
Hotspot: Add to cart
- Rights-gated: Only granted posts render
- 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.

```ts
// 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');
}
```

> **The same JSON feeds more than the storefront:** The gallery payload is not the only public feed. Idukki also exposes an agentfeed (public JSON + JSON-LD + an llms.txt + an MCP feed) so [AI shopping agents can read your shoppable UGC](/blog/make-product-content-readable-by-ai-shopping-agents), and a Klaviyo-compatible web feed for lifecycle email. One source of truth, several surfaces. If you are wiring AEO, see the agentfeed alongside your storefront build.

### Widget embed vs API render: which path fits your stack?
_Both are supported. The right answer is mostly about how much control you need over the markup._

**Fast path: Drop-in widget**
Paste a snippet, done. Best on Shopify, Woo, BigCommerce.
~10 min — time to live
- ✓ Live in minutes, no engineering
- ✓ Updates ship from the platform
- ✓ Curation and rights handled for you
- ✗ Injects external script + CSS
- ✗ Less control over markup and CSP
- ✗ Harder to fit a strict bundle budget

**Headless path: REST API + webhooks**
Fetch JSON, render your own components. Best on custom / headless.
full — render control
- ✓ Your markup, your design system, your CWV budget
- ✓ No third-party script under your CSP
- ✓ Same feed reused across front end, email, ads
- ✗ Requires front-end engineering time
- ✗ You own caching + webhook plumbing
- ✗ You build the player and add-to-cart wiring

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

**The one thing to remember:** Treat UGC as a cached JSON feed plus a webhook, not as an injected widget. The platform owns collection, rights, tagging, and curation; your storefront owns the markup, the player, the add-to-cart wiring, and the Core Web Vitals budget. That split gives you a shoppable gallery that looks native, stays fresh on its own, and never drags an external script under your CSP.

### FAQs

**Q: Do I need to use a UGC widget, or can I build my own gallery UI?**

A: Widgets are the fast path on Shopify or Woo, but a REST API is available for headless and custom storefronts. You fetch the gallery as JSON and render it with your own React or Vue components, which means your own design system, lazy-loading, and CSP apply instead of a third-party script.

**Q: How do you authenticate with a UGC REST API?**

A: Authentication uses an API key issued per business, treated as a server secret rather than something shipped to the browser. The pattern is a bearer token on a server-side fetch, so the key never reaches the client and the browser only ever sees the resulting JSON.

**Q: How do webhooks keep a UGC gallery updated without a redeploy?**

A: Instead of polling, the platform sends a webhook the instant a post is approved, removed, or a product is re-tagged. Your server verifies the signature and revalidates the cached feed for that gallery, so the next request rebuilds it with the latest content.

**Q: Should a storefront call the UGC API on every page load?**

A: No. UGC changes a few times a day, not a few times a second, so a live call per visitor only adds latency and rate-limit risk. Cache the gallery JSON at the edge with a generous revalidation window, then bust the cache by tag when a webhook fires.

**Q: What fields does a UGC gallery API response include?**

A: Each gallery response returns an ordered list of posts, and each post carries its playable media file, the author handle, a rights status flag, and any tagged products with id, title, price, and a link for add-to-cart. The same JSON can also feed email flows or an agent feed for AI shopping agents.

### Sources & further reading
- [Google: Core Web Vitals thresholds (LCP, INP, CLS)](https://web.dev/articles/vitals) — Performance budget for media-heavy pages
- [MDN: HTTP caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching) — Cache-Control and revalidation patterns
- [Bazaarvoice: Shopper Experience Index](https://www.bazaarvoice.com/resources/) — UGC trust and conversion-lift ranges
- [web.dev: Fast load times](https://web.dev/explore/fast) — Lazy-loading and media performance guidance
- [Idukki: agentfeed & web feed (AEO surfaces)](https://idukki.io) — Public JSON-LD, llms.txt, MCP, Klaviyo-compatible feed

---
Canonical: https://idukki.io/blog/add-ugc-custom-storefront-rest-api
Tags: Headless commerce, REST API, UGC
