Documentation Index
Fetch the complete documentation index at: https://docs.fanfare.io/llms.txt
Use this file to discover all available pages before exploring further.
Shopify Integration Guide
Learn how to integrate Fanfare experiences with your Shopify store, from installation to checkout completion.
Overview
Fanfare provides a native Shopify app that integrates virtual queues, draws, and auctions directly with your Shopify storefront. This guide covers both app installation and custom theme integration.
What you’ll learn:
- Installing and configuring the Fanfare Shopify app
- Embedding experiences in your theme
- Connecting experiences to products
- Handling checkout with Shopify’s native cart
- Processing order webhooks
Complexity: Intermediate
Time to complete: 45 minutes
Prerequisites
- Shopify store (Basic plan or higher)
- Fanfare account with active organization
- Access to Shopify theme editor
Installation Options
Option 1: Fanfare Shopify App (Recommended)
Install the official Fanfare app from the Shopify App Store:
- Visit the Fanfare app listing in the Shopify App Store
- Click “Add app” and authorize the permissions
- Connect your Fanfare organization in the app settings
- Configure your first experience
The app handles:
- OAuth authentication with Shopify
- Automatic webhook registration
- Theme app extension for embedding
- Order completion notifications
Option 2: Custom Integration
For stores requiring custom implementations:
<!-- In your theme's product.liquid or collection.liquid -->
<script src="https://cdn.fanfare.io/sdk/v1/fanfare-sdk.min.js"></script>
<div id="fanfare-experience" data-experience-id="{{ product.metafields.fanfare.experience_id }}"></div>
<script>
document.addEventListener("DOMContentLoaded", function () {
const container = document.getElementById("fanfare-experience");
const experienceId = container.dataset.experienceId;
if (experienceId) {
Fanfare.init({
organizationId: "{{ shop.metafields.fanfare.organization_id }}",
environment: "production",
});
Fanfare.renderExperience(container, {
experienceId: experienceId,
onAdmitted: function (admission) {
// Redirect to checkout with admission token
window.location.href = "/cart?fanfare_token=" + admission.token;
},
});
}
});
</script>
Create your experience in the Fanfare admin panel:
- Navigate to Experiences > Create New
- Select experience type (Queue, Draw, or Auction)
- Configure timing and capacity
- Note the Experience ID for later use
Linking to Shopify Products
Connect experiences to specific products or collections:
// Using the Fanfare Admin API
const experience = await fanfareAdmin.experiences.create({
name: "Limited Sneaker Drop",
type: "queue",
config: {
capacity: 500,
admissionWindow: 600, // 10 minutes
},
metadata: {
shopifyProductId: "gid://shopify/Product/1234567890",
shopifyVariantIds: ["gid://shopify/ProductVariant/111", "gid://shopify/ProductVariant/222"],
},
});
Step 2: Theme App Extension
The Fanfare Shopify app includes a theme app extension for easy embedding.
Adding to Product Pages
- Open Online Store > Themes > Customize
- Navigate to your product template
- Add the Fanfare Experience block
- Configure the block settings:
| Setting | Description |
|---|
| Experience Source | Product metafield or manual ID |
| Display Mode | Inline, modal, or full-page |
| Auto-redirect | Redirect to cart on admission |
| Show countdown | Display time remaining |
Block Configuration (Liquid)
{% comment %} sections/fanfare-experience.liquid {% endcomment %}
{% schema %}
{
"name": "Fanfare Experience",
"settings": [
{
"type": "text",
"id": "experience_id",
"label": "Experience ID",
"info": "Leave blank to use product metafield"
},
{
"type": "select",
"id": "display_mode",
"label": "Display Mode",
"options": [
{ "value": "inline", "label": "Inline" },
{ "value": "modal", "label": "Modal" },
{ "value": "fullpage", "label": "Full Page" }
],
"default": "inline"
},
{
"type": "checkbox",
"id": "auto_redirect",
"label": "Auto-redirect to cart on admission",
"default": true
}
]
}
{% endschema %}
<div
id="fanfare-experience-{{ section.id }}"
class="fanfare-experience"
data-experience-id="{{ section.settings.experience_id | default: product.metafields.fanfare.experience_id }}"
data-display-mode="{{ section.settings.display_mode }}"
data-auto-redirect="{{ section.settings.auto_redirect }}"
data-product-id="{{ product.id }}"
data-variant-id="{{ product.selected_or_first_available_variant.id }}"
></div>
Step 3: JavaScript Integration
Initialize the SDK
// assets/fanfare-integration.js
(function () {
// Wait for SDK to load
function initFanfare() {
if (typeof Fanfare === "undefined") {
setTimeout(initFanfare, 100);
return;
}
// Get organization ID from theme settings
const orgId = window.FanfareConfig?.organizationId;
if (!orgId) {
console.error("Fanfare: Missing organization ID");
return;
}
Fanfare.init({
organizationId: orgId,
environment: "production",
});
// Initialize all experience containers on page
document.querySelectorAll(".fanfare-experience").forEach(initExperience);
}
function initExperience(container) {
const experienceId = container.dataset.experienceId;
if (!experienceId) return;
const config = {
experienceId: experienceId,
displayMode: container.dataset.displayMode || "inline",
productId: container.dataset.productId,
variantId: container.dataset.variantId,
};
Fanfare.renderExperience(container, {
...config,
onStateChange: function (state) {
handleStateChange(container, state);
},
onAdmitted: function (admission) {
handleAdmission(container, admission);
},
onError: function (error) {
handleError(container, error);
},
});
}
function handleStateChange(container, state) {
// Update UI based on state
const statusEl = container.querySelector(".fanfare-status");
if (statusEl) {
statusEl.textContent = getStatusText(state);
}
}
function handleAdmission(container, admission) {
const autoRedirect = container.dataset.autoRedirect === "true";
const variantId = container.dataset.variantId;
if (autoRedirect && variantId) {
// Add to cart and redirect to checkout
addToCartAndCheckout(variantId, admission.token);
} else {
// Show checkout button
showCheckoutButton(container, admission);
}
}
function addToCartAndCheckout(variantId, admissionToken) {
// Add item to Shopify cart
fetch("/cart/add.js", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
items: [
{
id: variantId,
quantity: 1,
properties: {
_fanfare_token: admissionToken,
_fanfare_admitted_at: new Date().toISOString(),
},
},
],
}),
})
.then(function (response) {
if (response.ok) {
// Redirect to checkout
window.location.href = "/checkout";
} else {
throw new Error("Failed to add to cart");
}
})
.catch(function (error) {
console.error("Cart error:", error);
alert("Failed to add item to cart. Please try again.");
});
}
function showCheckoutButton(container, admission) {
const button = document.createElement("button");
button.className = "fanfare-checkout-btn";
button.textContent = "Complete Purchase";
button.onclick = function () {
addToCartAndCheckout(container.dataset.variantId, admission.token);
};
container.appendChild(button);
}
function handleError(container, error) {
console.error("Fanfare error:", error);
const errorEl = document.createElement("div");
errorEl.className = "fanfare-error";
errorEl.textContent = "Unable to load experience. Please refresh the page.";
container.appendChild(errorEl);
}
function getStatusText(state) {
const statusMap = {
not_started: "Experience not started",
entering: "Joining...",
routing: "Finding your place...",
waiting: "You're in the queue",
admitted: "You're in! Complete your purchase",
completed: "Purchase complete",
expired: "Session expired",
};
return statusMap[state.stage] || state.stage;
}
// Start initialization
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initFanfare);
} else {
initFanfare();
}
})();
Include in Theme
{% comment %} layout/theme.liquid {% endcomment %}
<script>
window.FanfareConfig = {
organizationId: "{{ shop.metafields.fanfare.organization_id }}",
};
</script>
<script src="https://cdn.fanfare.io/sdk/v1/fanfare-sdk.min.js" async></script>
{{ "fanfare-integration.js" | asset_url | script_tag }}
Step 4: Cart Line Properties
Store Fanfare data in cart line properties for order processing:
// When adding to cart with admission token
const cartData = {
items: [
{
id: variantId,
quantity: 1,
properties: {
_fanfare_token: admission.token,
_fanfare_experience_id: experienceId,
_fanfare_consumer_id: admission.consumerId,
_fanfare_distribution_type: admission.distributionType,
_fanfare_admitted_at: new Date().toISOString(),
},
},
],
};
fetch("/cart/add.js", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(cartData),
});
Protecting Against Tampering
Validate tokens server-side in your checkout flow:
{% comment %} In checkout.liquid or via Shopify Functions {% endcomment %}
{% for item in checkout.line_items %}
{% if item.properties._fanfare_token %}
{% comment %} Token will be validated via webhook {% endcomment %}
<input type="hidden" name="fanfare_token" value="{{ item.properties._fanfare_token }}">
{% endif %}
{% endfor %}
Step 5: Webhook Integration
The Fanfare app automatically registers these webhooks:
| Webhook Topic | Purpose |
|---|
orders/create | Validate admission and complete |
orders/paid | Confirm successful transaction |
orders/cancelled | Handle cancellations |
refunds/create | Track refund events |
Manual Webhook Setup
If using custom integration, register webhooks via Shopify Admin API:
// Register order webhook
const webhook = await shopifyAdmin.webhook.create({
topic: "orders/create",
address: "https://api.fanfare.io/webhooks/shopify/orders",
format: "json",
});
Webhook Handler
// Your webhook endpoint (or Fanfare's built-in handler)
app.post("/webhooks/shopify/orders", async (req, res) => {
const hmac = req.headers["x-shopify-hmac-sha256"];
// Verify webhook signature
if (!verifyShopifyWebhook(req.body, hmac)) {
return res.status(401).send("Invalid signature");
}
const order = req.body;
// Extract Fanfare data from line items
for (const item of order.line_items) {
const fanfareToken = item.properties?.find((p: { name: string }) => p.name === "_fanfare_token")?.value;
if (fanfareToken) {
const experienceId = item.properties?.find((p: { name: string }) => p.name === "_fanfare_experience_id")?.value;
const consumerId = item.properties?.find((p: { name: string }) => p.name === "_fanfare_consumer_id")?.value;
const distributionType = item.properties?.find(
(p: { name: string }) => p.name === "_fanfare_distribution_type"
)?.value;
// Complete the admission in Fanfare
await fanfareApi.completeAdmission({
experienceId,
consumerId,
distributionType,
orderId: order.id.toString(),
orderAmount: parseFloat(order.total_price),
metadata: {
platform: "shopify",
shopDomain: order.shop_domain,
orderNumber: order.order_number,
},
});
}
}
res.status(200).send("OK");
});
Store Fanfare configuration in Shopify metafields:
Using Shopify Admin
- Go to Settings > Custom data > Products
- Add metafield definition:
- Namespace:
fanfare
- Key:
experience_id
- Type: Single line text
Using Admin API
// Set experience ID on a product
await shopifyAdmin.metafield.create({
namespace: "fanfare",
key: "experience_id",
value: "exp_abc123xyz",
type: "single_line_text_field",
owner_resource: "product",
owner_id: productId,
});
// Set organization-level config on shop
await shopifyAdmin.metafield.create({
namespace: "fanfare",
key: "organization_id",
value: "org_xyz789abc",
type: "single_line_text_field",
owner_resource: "shop",
});
Step 7: Checkout UI Extension (Shopify Plus)
For Shopify Plus stores, create a checkout UI extension:
// extensions/fanfare-checkout/src/Checkout.tsx
import { reactExtension, Banner, useCartLines, useApplyCartLinesChange } from "@shopify/ui-extensions-react/checkout";
export default reactExtension("purchase.checkout.block.render", () => <FanfareCheckoutValidation />);
function FanfareCheckoutValidation() {
const cartLines = useCartLines();
const applyCartLinesChange = useApplyCartLinesChange();
// Check for Fanfare tokens in cart
const fanfareItems = cartLines.filter((line) => line.attributes?.some((attr) => attr.key === "_fanfare_token"));
if (fanfareItems.length === 0) {
return null;
}
// Validate tokens are still valid
const [validationStatus, setValidationStatus] = useState<"pending" | "valid" | "invalid">("pending");
useEffect(() => {
validateFanfareTokens(fanfareItems).then((isValid) => {
setValidationStatus(isValid ? "valid" : "invalid");
});
}, [fanfareItems]);
if (validationStatus === "invalid") {
return (
<Banner status="critical" title="Access Expired">
Your access to purchase these items has expired. Please return to the product page to rejoin the experience.
</Banner>
);
}
if (validationStatus === "valid") {
return (
<Banner status="success" title="Access Verified">
Your exclusive access has been verified. Complete your purchase before the timer expires.
</Banner>
);
}
return null;
}
async function validateFanfareTokens(items: CartLine[]): Promise<boolean> {
for (const item of items) {
const token = item.attributes?.find((a) => a.key === "_fanfare_token")?.value;
if (!token) continue;
const response = await fetch("https://api.fanfare.io/v1/admission/validate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token }),
});
if (!response.ok) {
return false;
}
}
return true;
}
Styling the Experience
CSS Customization
/* assets/fanfare-custom.css */
.fanfare-experience {
padding: 20px;
border-radius: 8px;
background: var(--color-background);
}
.fanfare-queue-position {
font-size: 2rem;
font-weight: bold;
text-align: center;
}
.fanfare-timer {
font-size: 1.5rem;
color: var(--color-primary);
}
.fanfare-checkout-btn {
width: 100%;
padding: 16px 24px;
background: var(--color-primary);
color: white;
border: none;
border-radius: 4px;
font-size: 1.1rem;
cursor: pointer;
transition: background 0.2s;
}
.fanfare-checkout-btn:hover {
background: var(--color-primary-dark);
}
.fanfare-error {
padding: 12px;
background: #fee;
color: #c00;
border-radius: 4px;
}
Best Practices
1. Pre-load Experience Data
// Prefetch experience status on page load
Fanfare.prefetch(experienceId).then(function (status) {
if (status.state === "not_started") {
showCountdown(status.startsAt);
}
});
// Prevent users from losing their place
window.addEventListener("popstate", function (event) {
if (Fanfare.hasActiveSession()) {
if (!confirm("Leaving may forfeit your place in the experience. Continue?")) {
history.pushState(null, "", location.href);
}
}
});
3. Mobile Optimization
{% comment %} Responsive container {% endcomment %}
<style>
@media (max-width: 768px) {
.fanfare-experience {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
border-radius: 16px 16px 0 0;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
}
}
</style>
Troubleshooting
Experience Not Loading
- Check that organization ID metafield is set on shop
- Verify experience ID is correct in product metafield
- Check browser console for SDK errors
- Ensure SDK script is loading (check network tab)
Cart Properties Not Appearing
Shopify may strip properties starting with _ in some contexts:
// Use non-underscore prefixed properties if needed
properties: {
fanfare_token: admission.token,
fanfare_experience_id: experienceId,
}
Webhook Not Firing
- Verify webhook is registered in Shopify admin
- Check webhook delivery logs
- Ensure endpoint URL is publicly accessible
- Verify HMAC signature validation
Token Validation Failing
// Debug token validation
async function debugTokenValidation(token) {
const response = await fetch("https://api.fanfare.io/v1/admission/validate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token }),
});
const data = await response.json();
console.log("Validation response:", data);
if (!response.ok) {
console.error("Validation failed:", data.error);
console.error("Token may be expired or already used");
}
return response.ok;
}
What’s Next