Skip to main content
Use the core SDK when you want to own the UI. The core SDK gives you a journey handle. Your application reads the current JourneyView and calls only the actions available in that view.

Install

pnpm add @fanfare-io/fanfare-sdk-core

Initialize the SDK

import initFanfare from "@fanfare-io/fanfare-sdk-core";

const sdk = await initFanfare({
  organizationId: "org_123",
  publishableKey: "pk_live_123",
});
Create the SDK once in the part of your application that owns the consumer experience. When that application area unmounts permanently, call sdk.destroy().

Create a journey

const journey = sdk.journeys.get("exp_123");
The journey exposes four stores:
StoreUse for
view$Normal UI rendering and valid actions
snapshot$Diagnostics or exhaustive state inspection
events$User-facing journey events
latestEvent$Toasts, announcements, or analytics triggers
Most integrations should render from view$. These are reactive readable stores. Use .get() for the current value, and .listen(...) when your UI needs to update as the journey changes. Always keep the unsubscribe function and call it when your component, page, or integration mount point is removed. React integrations usually do not need to subscribe to these stores directly because useExperienceJourney returns the current view and snapshot for React rendering.

Render from JourneyView

import type { JourneyView } from "@fanfare-io/fanfare-sdk-core/experiences";

const unsubscribe = journey.view$.listen((view) => {
  renderJourney(view);
});

function renderJourney(view: JourneyView) {
  if (view.journeyStage === "ready") {
    showStartButton(() => view.start());
    return;
  }

  if (view.journeyStage === "routing") {
    showLoadingState();
    return;
  }

  if (view.journeyStage === "gated") {
    showGate(view.gates, (accessCode) => view.reroute({ accessCode }));
    return;
  }

  renderSequence(view.sequence);
}
The key rule: actions live on the view for the current state. If an action is not present in the current view, your UI should not offer it. A routed view may also surface view.offers (informational, always-gated upgrade routes) and view.submitAccessCode(code) for an in-place upgrade attempt. See Gates and Auth for how this differs from the gated-stage reroute flow.

Render routed sequence states

import type { SequenceView } from "@fanfare-io/fanfare-sdk-core/experiences";

function renderSequence(sequence: SequenceView) {
  if (sequence.phase === "scheduled") {
    showUpcoming({
      startsAt: sequence.startsAt,
    });
    // Waitlist is now a scheduled-phase attachment, not a mechanism or phase.
    // See [Journey State](/sdk/core/journey-state) for the full model.
    const waitlist = sequence.waitlist;
    if (waitlist) {
      showWaitlist({
        status: waitlist.status,
        onJoin: () => waitlist.join(),
        onLeave: () => waitlist.leave(),
      });
    }
    return;
  }

  if (sequence.phase === "enterable") {
    if ("enter" in sequence) {
      showEnterButton(() => sequence.enter());
    } else if (sequence.mechanism === "auction") {
      showBidForm((amount) => sequence.bid(amount));
    } else if (sequence.mechanism === "appointment") {
      showAppointmentPicker((slotId, locationId) => sequence.book(slotId, locationId));
    }
    return;
  }

  if (sequence.phase === "participating") {
    // Live participating data is read from the `display$` atom, not the view itself.
    const unsubscribeDisplay = sequence.display$.listen((display) => {
      showLiveParticipation({ type: sequence.mechanism, display });
    });

    if (sequence.mechanism === "appointment") {
      showAppointmentControls({
        onReschedule: (slotId, locationId) => sequence.reschedule(slotId, locationId),
        onCancel: (reason) => sequence.cancel(reason),
      });
    } else {
      // queue, draw, and timed_release can leave; auction can leave and also raise its bid.
      showParticipationControls({
        onLeave: "leave" in sequence ? () => sequence.leave() : undefined,
        onBid: sequence.mechanism === "auction" ? (amount) => sequence.bid(amount) : undefined,
      });
    }

    return unsubscribeDisplay;
  }

  if (sequence.phase === "granted") {
    // claim() starts checkout and stops the client-side grant-expiry countdown.
    const grant = sequence.claim();
    goToCheckout(grant.token);
    return;
  }

  if (sequence.phase === "ended") {
    showEnded({ reason: sequence.outcome.type });
  }
}

Resume existing journeys

If the consumer returns to your app with an existing session, resume known journeys before rendering your main experience surface.
await sdk.journeys.resumeAll();
Resume tells the SDK to reconstruct public journey state. Your UI should still render from the latest view$ value after resume completes.

Next steps