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.
Checkout Integration Overview
Learn how to connect Fanfare experiences to your checkout flow, from admission to order completion.
Overview
When a consumer is admitted through a Fanfare experience (queue, draw, or auction), they receive an admission token. This guide explains how to securely transition consumers from admission to checkout and complete their purchase.
What you’ll learn:
- Understanding the admission-to-checkout flow
- Working with admission tokens
- Handoff patterns for different platforms
- Completing the checkout cycle
Complexity: Beginner
Time to complete: 25 minutes
Prerequisites
- Fanfare SDK integrated and working
- An existing checkout system
- Understanding of your e-commerce platform
The Checkout Flow
┌─────────────────────────────────────────────────────────────────────┐
│ Consumer Journey │
└─────────────────────────────────────────────────────────────────────┘
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Enter │ │ Wait/ │ │ Admitted │ │ Complete │
│Experience│────▶│Participate│────▶│(Token) │────▶│ Checkout │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│
│ Admission Token
▼
┌──────────┐
│ Validate │
│ Token │
└──────────┘
│
▼
┌──────────┐
│ Place │
│ Order │
└──────────┘
│
▼
┌──────────┐
│ Complete │
│Admission │
└──────────┘
Understanding Admission Tokens
When a consumer is admitted (reaches the front of a queue, wins a draw, wins an auction), they receive an admission token. This token:
- Proves they were legitimately admitted
- Has an expiration time (typically 10-30 minutes)
- Can be validated server-side
- Should only be used once
Token Lifecycle
Created ──▶ Valid ──▶ Used/Expired ──▶ Invalid
│ │ │ │
│ │ │ │
10 min Checkout Complete Rejected
expiry window order
Step 1: Detect Admission
When a consumer is admitted, the journey state changes:
import { useExperienceJourney } from "@waitify-io/fanfare-sdk-react";
import { useEffect } from "react";
function ExperienceWithCheckout({ experienceId }: { experienceId: string }) {
const { state, journey } = useExperienceJourney(experienceId, { autoStart: true });
useEffect(() => {
const snapshot = state?.snapshot;
if (!snapshot) return;
// Detect admission
if (snapshot.sequenceStage === "admitted") {
const { admittanceToken, admittanceExpiresAt } = snapshot.context;
console.log("Consumer admitted!", {
token: admittanceToken,
expiresAt: new Date(admittanceExpiresAt!),
});
// Optionally auto-redirect to checkout
// navigateToCheckout(admittanceToken);
}
}, [state?.snapshot.sequenceStage]);
return <YourExperienceUI state={state} journey={journey} />;
}
Step 2: Show Checkout Call-to-Action
Display a clear checkout action when admitted:
function AdmittedState({ admissionToken, expiresAt }: { admissionToken: string; expiresAt: number }) {
const [timeRemaining, setTimeRemaining] = useState(Math.max(0, Math.floor((expiresAt - Date.now()) / 1000)));
useEffect(() => {
const interval = setInterval(() => {
const remaining = Math.max(0, Math.floor((expiresAt - Date.now()) / 1000));
setTimeRemaining(remaining);
if (remaining <= 0) {
clearInterval(interval);
}
}, 1000);
return () => clearInterval(interval);
}, [expiresAt]);
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, "0")}`;
};
const handleCheckout = () => {
// Navigate to checkout with token
window.location.href = `/checkout?admission_token=${admissionToken}`;
};
if (timeRemaining <= 0) {
return (
<div className="admission-expired">
<h2>Access Expired</h2>
<p>Your checkout window has expired. Please rejoin the experience.</p>
</div>
);
}
return (
<div className="admitted-state">
<div className="success-icon">✓</div>
<h2>You're In!</h2>
<p>You have access to complete your purchase.</p>
<div className="timer">
<span className="time">{formatTime(timeRemaining)}</span>
<span className="label">remaining</span>
</div>
<button onClick={handleCheckout} className="checkout-button">
Continue to Checkout
</button>
<p className="warning">Don't close this page until you complete your purchase.</p>
</div>
);
}
Step 3: Pass Token to Checkout
Several patterns for passing the admission token to your checkout:
URL Parameter (Simple)
// From experience page
function navigateToCheckout(token: string) {
window.location.href = `/checkout?admission_token=${token}`;
}
// In checkout page
function CheckoutPage() {
const params = new URLSearchParams(window.location.search);
const admissionToken = params.get("admission_token");
if (!admissionToken) {
return <NoAccessMessage />;
}
return <CheckoutForm admissionToken={admissionToken} />;
}
Session Storage (Recommended)
// Store token before navigation
function navigateToCheckout(token: string, context: AdmissionContext) {
sessionStorage.setItem(
"fanfare_admission",
JSON.stringify({
token,
context,
storedAt: Date.now(),
})
);
window.location.href = "/checkout";
}
// Retrieve in checkout
function useAdmissionToken() {
const [admission, setAdmission] = useState<StoredAdmission | null>(null);
useEffect(() => {
const stored = sessionStorage.getItem("fanfare_admission");
if (stored) {
const data = JSON.parse(stored);
// Validate it's not too old
if (Date.now() - data.storedAt < 30 * 60 * 1000) {
// 30 minutes
setAdmission(data);
} else {
sessionStorage.removeItem("fanfare_admission");
}
}
}, []);
return admission;
}
SDK Handoff Module (Full Featured)
Use the built-in handoff module for secure token handling:
import { HandoffModule } from "@waitify-io/fanfare-sdk-core";
const handoff = new HandoffModule();
// Create handoff token (includes security features)
async function createCheckoutHandoff(
admissionToken: string,
consumerId: string,
organizationId: string,
experienceId: string,
experienceType: "queue" | "draw" | "auction"
) {
const token = await handoff.createHandoffToken({
admissionToken,
consumerId,
organizationId,
experienceId,
experienceType,
expiresIn: 600, // 10 minutes
});
// Build URL with token
const checkoutUrl = handoff.buildHandoffUrl({
baseUrl: "https://your-store.com/checkout",
token,
});
return checkoutUrl;
}
Step 4: Validate Admission Server-Side
Before allowing checkout, validate the token with Fanfare:
// Your checkout API endpoint
app.post("/api/checkout", async (req, res) => {
const { admissionToken, consumerId, distributionId, distributionType, cart } = req.body;
// 1. Validate admission with Fanfare
const isValid = await validateAdmissionToken({
token: admissionToken,
consumerId,
distributionId,
distributionType,
});
if (!isValid) {
return res.status(403).json({
error: "Invalid admission",
code: "ADMISSION_INVALID",
message: "Your access has expired or is invalid. Please try again.",
});
}
// 2. Process the order
const order = await createOrder(cart, consumerId);
// 3. Mark admission as completed
await completeAdmission(distributionId, distributionType, consumerId);
res.json({ orderId: order.id });
});
Step 5: Complete the Admission
After a successful order, mark the admission as complete:
async function completeAdmission(
distributionId: string,
distributionType: "queue" | "draw" | "auction",
consumerId: string
) {
const endpoints = {
queue: `/queues/${distributionId}/complete`,
draw: `/draws/${distributionId}/complete`,
auction: `/auctions/${distributionId}/complete`,
};
await fetch(`${FANFARE_API_URL}${endpoints[distributionType]}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Organization-Id": FANFARE_ORG_ID,
"X-Secret-Key": FANFARE_SECRET_KEY,
},
body: JSON.stringify({ consumerId }),
});
}
Integration Patterns
Pattern 1: Same-Site Checkout
When checkout is on the same domain:
// Simple navigation with URL params
function handleAdmitted(token: string) {
window.location.href = `/checkout?token=${token}`;
}
Pattern 2: External Checkout (Shopify, etc.)
When checkout is on a different domain:
import { HandoffModule } from "@waitify-io/fanfare-sdk-core";
async function handleAdmitted(admissionContext: AdmissionContext) {
const handoff = new HandoffModule();
// Create secure handoff token
const token = await handoff.createHandoffToken({
admissionToken: admissionContext.token,
consumerId: admissionContext.consumerId,
organizationId: admissionContext.organizationId,
experienceId: admissionContext.experienceId,
experienceType: admissionContext.type,
});
// Redirect to external checkout
const checkoutUrl = handoff.buildHandoffUrl({
baseUrl: "https://your-store.myshopify.com/checkout",
token,
additionalParams: {
cart: encodeURIComponent(JSON.stringify(cart)),
},
});
window.location.href = checkoutUrl;
}
Pattern 3: Embedded Checkout
When checkout happens in an iframe/modal:
function EmbeddedCheckout({ admissionToken, onComplete }: Props) {
const iframeRef = useRef<HTMLIFrameElement>(null);
useEffect(() => {
// Listen for checkout completion message
const handleMessage = (event: MessageEvent) => {
if (event.data.type === "checkout-complete") {
onComplete(event.data.orderId);
}
};
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, [onComplete]);
return <iframe ref={iframeRef} src={`/checkout/embedded?token=${admissionToken}`} className="checkout-iframe" />;
}
Handling Edge Cases
Token Expiration During Checkout
function CheckoutForm({ admissionToken, expiresAt }: Props) {
const [isExpired, setIsExpired] = useState(Date.now() > expiresAt);
useEffect(() => {
if (isExpired) return;
const timeout = setTimeout(() => {
setIsExpired(true);
}, expiresAt - Date.now());
return () => clearTimeout(timeout);
}, [expiresAt, isExpired]);
if (isExpired) {
return (
<div className="checkout-expired">
<h2>Session Expired</h2>
<p>Your checkout session has expired.</p>
<a href="/experience">Return to Experience</a>
</div>
);
}
return <YourCheckoutForm />;
}
Network Errors
async function submitOrder(orderData: OrderData) {
try {
const response = await fetch("/api/checkout", {
method: "POST",
body: JSON.stringify(orderData),
});
if (!response.ok) {
const error = await response.json();
if (error.code === "ADMISSION_INVALID") {
// Token is invalid or expired
showError("Your access has expired. Please rejoin the experience.");
redirectToExperience();
return;
}
throw new Error(error.message);
}
return response.json();
} catch (error) {
// Network error - allow retry
showError("Connection error. Please try again.");
}
}
Page Refresh
function CheckoutPage() {
const [admission, setAdmission] = useState<StoredAdmission | null>(null);
useEffect(() => {
// Try to restore admission from session storage
const stored = sessionStorage.getItem("fanfare_admission");
if (stored) {
setAdmission(JSON.parse(stored));
} else {
// No admission found - redirect to experience
window.location.href = "/experience";
}
}, []);
if (!admission) {
return <LoadingSpinner />;
}
return <CheckoutForm admission={admission} />;
}
Checkout Success/Failure Handling
Success Flow
async function handleCheckoutSuccess(orderId: string) {
// 1. Clear admission data
sessionStorage.removeItem("fanfare_admission");
// 2. Complete admission (mark as used)
await completeAdmission();
// 3. Track success
analytics.track("checkout_complete", { orderId });
// 4. Show confirmation
navigate(`/order-confirmation/${orderId}`);
}
Failure Flow
async function handleCheckoutFailure(error: Error) {
// Don't clear admission - allow retry
console.error("Checkout failed:", error);
// Track failure
analytics.track("checkout_failed", { error: error.message });
// Show error - user can retry
showError("Checkout failed. Please try again.");
}
Best Practices
1. Show Clear Timing
Always display remaining time prominently:
<div className="checkout-timer">
<span>{formatTime(timeRemaining)}</span>
<span>to complete checkout</span>
</div>
2. Prevent Accidental Navigation
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (hasAdmissionToken && !orderComplete) {
e.preventDefault();
e.returnValue = "You have an active checkout session. Are you sure you want to leave?";
}
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [hasAdmissionToken, orderComplete]);
3. Validate Early
Validate the admission token as soon as the checkout page loads, not just on submit:
useEffect(() => {
async function validateOnLoad() {
const isValid = await validateAdmission(admissionToken);
if (!isValid) {
redirectToExperience();
}
}
validateOnLoad();
}, [admissionToken]);
What’s Next