Headless storefronts
Running Evident on a custom React, Next.js, Vue or Astro storefront — no theme, no platform connection required.
9 min read
On this page
Evident does not need to live inside a theme. If your storefront is your own code, you push your catalogue and orders over the API and render with the same widget SDK everyone else uses.
1. Get your store environment ID
Settings → Store. Copy the store environment ID.
This is the only credential the storefront needs. There is no separate storefront token to request and nothing secret to keep on a server — it identifies which store a request belongs to. What controls who may call the API is the origin allow-list in the next step.
2. Allow-list the origins you render from
Still under Settings → Store, add every origin your storefront runs on to Allowed Storefront Origins — production, staging, and http://localhost:3000 for local development. Changes take effect immediately.
https://yourstore.com
https://staging.yourstore.com
http://localhost:3000
An origin is scheme, host and port — no paths, no trailing slash. A bare domain is treated as https.
Two things trip people up here:
- Preview deployments. Every Vercel or Netlify preview URL is a different origin and is blocked until listed.
*is not a wildcard. The list is exact-match. There is no pattern syntax.
And a debugging note that will save you an hour: curl cannot reproduce a CORS failure. It sends no Origin header, so the request succeeds and tells you nothing. Reproduce in a browser.
3. Load the SDK
Add the script once, in your app shell or root layout. It reads your store environment ID from the data attribute, pulls in its own stylesheet, and mounts every widget container it finds — including ones your framework renders later.
<script
src="https://app.evidentugc.com/widgets/evident-sdk.min.js"
data-store-env-id="YOUR_STORE_ENV_ID"
></script>
4. Place a widget container
Widgets mount into any element carrying data-evident-widget. A gallery needs only its slug, which you will find under Galleries:
<div
data-evident-widget="gallery"
data-gallery-slug="customer-photos"
data-columns="3"
></div>
Galleries, FAQs and store-wide review widgets work immediately. Product-scoped widgets — star badges, per-product review lists, review forms — need your catalogue in Evident first. That is step 6.
5. React, Next.js, Vue and other SPAs
The SDK watches the DOM, so a container your framework renders after page load is mounted automatically, and re-mounted if a re-render wipes it.
If you would rather mount explicitly:
'use client';
import { useEffect, useRef } from 'react';
export function EvidentGallery({ slug }) {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
window.Evident?.mount(el);
return () => window.Evident?.unmount(el);
}, [slug]);
return (
<div
ref={ref}
data-evident-widget="gallery"
data-gallery-slug={slug}
/>
);
}
window.Evident may not exist yet on first paint, which is why the optional chaining matters — the container is still picked up when the script finishes loading.
The full API: refresh() re-scans and mounts anything blank, mount(el) mounts one container, unmount(el) tears one down, and init() is safe to call more than once — React StrictMode double-invokes effects in development.
The widget renders into a wrapper element it owns, rather than into your node directly, so a React re-render cannot blank it.
6. Push your catalogue
Platform stores sync their catalogue automatically. A headless store pushes it. Create an API key under Settings → API Keys, then upsert products — up to 250 per call, keyed on your own product ID, so re-sending the same payload is safe.
curl -X PUT https://api.evidentugc.com/api/v1/products \
-H "Authorization: Bearer evnt_YOUR_API_KEY" \
-H "x-store-env-id: YOUR_STORE_ENV_ID" \
-H "Content-Type: application/json" \
-d '{
"products": [
{
"platformProductId": "sku-1024",
"name": "Merino Wool Runner",
"imageUrl": "https://cdn.example.com/mwr.jpg",
"productUrl": "https://example.com/p/merino-wool-runner",
"price": 98.00
}
]
}'
This is a replace, not a merge. A field you omit is cleared, so send the whole product each time.
Items are independent: one rejected row does not fail the batch. Check the failed count in the response rather than the status code alone.
The platformProductId you send here is the value your widgets pass as data-product-id.
7. Push your orders
Orders are what connect a reviewer to a purchase. Push them and you get the verified-purchase badge plus automated review requests.
Push your catalogue first — line items link to products by platformProductId, and verified purchase matches on that link, so an order whose products are missing is stored but inert. The response tells you how many line items are still unlinked.
curl -X PUT https://api.evidentugc.com/api/v1/orders \
-H "Authorization: Bearer evnt_YOUR_API_KEY" \
-H "x-store-env-id: YOUR_STORE_ENV_ID" \
-H "Content-Type: application/json" \
-d '{
"scheduleReviewRequests": false,
"orders": [
{
"platformOrderId": "order-5581",
"customerEmail": "buyer@example.com",
"customerFirstName": "Dana",
"status": "Shipped",
"orderDate": "2026-08-01T14:22:00.000Z",
"lineItems": [
{ "platformProductId": "sku-1024", "quantity": 2, "price": 98.00 }
]
}
]
}'
Things worth knowing before you go live
Review requests are opt-in, and only for recent orders. Pushing an order never emails anyone unless you set scheduleReviewRequests. Even then, orders older than 30 days are skipped: the send delay is counted from when you push, not from the order date, so a backfilled order would otherwise be treated as if it shipped today.
Turn it on only once you are pushing orders as they happen. Review requests also trigger on the statuses configured under Settings → Store (“Shipped” by default), so the status you send decides eligibility.
Content Security Policy. If you enforce one, the widgets need these sources:
script-src https://app.evidentugc.com
style-src https://app.evidentugc.com
connect-src https://api.evidentugc.com https://upload.cloudflarestream.com
img-src https://cdn.evidentugc.com https://*.cloudflarestream.com <your product image host>
frame-src https://*.cloudflarestream.com
connect-src is the one most often missed. The SDK fetches every review, rating and FAQ from api.evidentugc.com, so without it the widget loads, reserves its space, and renders nothing — the only clue is a blocked request in the browser console. The Cloudflare Stream entries are for video reviews: playback is an embedded player, and the review form uploads straight to Stream. If you do not accept video, you can leave them out. Product images are the URLs you pushed in your catalogue, so allow whichever host serves them.
Roll a new policy out as report-only first. A report-only policy logs violations without blocking — the widget will stop rendering the moment you enforce one that is missing a source.
Script tag, not a package. The SDK ships as a script tag rather than an npm package today. It is framework-agnostic and needs no build step, but there is no typed React component to import — wrap it yourself as in step 5.
Loyalty accrual works on pushed orders, provided the status you send is one Evident counts. Points are derived from the order row itself rather than from a platform event, so a pushed order earns exactly like a synced one — but only if its status is Completed, Shipped or Awaiting Fulfillment, and only once loyalty has an orders start date set. See Earning rules.
Something missing or out of date? Email support@evidentugc.com — docs corrections go straight to the team that builds the feature.