Skip to main content

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