Skip to main content

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} />;
}
// 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;
}
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