Skip to main content
Use a headless integration when your application owns the full UI and Fanfare supplies the public journey contract. This is the right path for custom storefronts, non-React frameworks, canvas experiences, or any app where the default widget is not the product experience you want to show. Headless integrations should render from JourneyView, call only actions exposed by the current view, and hand admitted customers to your trusted checkout flow without exposing grants in URLs, logs, or third-party analytics.

Install

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

Initialize Once

Create the SDK in the browser area that owns the customer experience.
import initFanfare from "@fanfare-io/fanfare-sdk-core";

const sdk = await initFanfare({
  organizationId: "org_123",
  publishableKey: "pk_live_123",
});

const journey = sdk.journeys.get("exp_123");
Call sdk.destroy() only when that application area unmounts permanently.

Render From The Public View

The view is the stable UI contract. It includes the public state and the actions that are valid for that state.
import type {
  JourneyView,
  SequenceView,
} from "@fanfare-io/fanfare-sdk-core/experiences";

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

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

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

  if (view.journeyStage === "gated") {
    mountGate({
      gates: view.gates,
      onAccessCode: (accessCode) => view.reroute({ accessCode }),
      onRetry: () => view.retry(),
    });
    return;
  }

  renderSequence(view.sequence);
}

function renderSequence(sequence: SequenceView) {
  switch (sequence.phase) {
    case "unavailable":
      mountUnavailable();
      return;
    case "scheduled":
      mountUpcoming({ startsAt: sequence.startsAt });
      return;
    case "enterable":
      if ("enter" in sequence) {
        mountEnterButton(() => sequence.enter());
      } else if ("bid" in sequence) {
        mountBidForm((amount) => sequence.bid(amount));
      } else if ("book" in sequence) {
        mountAppointmentPicker((slotId, locationId) =>
          sequence.book(slotId, locationId)
        );
      } else {
        mountUnavailable();
      }
      return;
    case "participating":
      mountParticipation({
        type: sequence.mechanism,
        onLeave: "leave" in sequence ? () => sequence.leave() : undefined,
      });
      return;
    case "settling":
      mountSettling(sequence);
      return;
    case "granted":
      mountCheckout(sequence.grant.token);
      return;
    case "ended":
      mountEnded({ reason: sequence.outcome.type });
  }
}
When your page no longer needs updates, call unsubscribe().

Framework Adapter Pattern

For Vue, Svelte, or vanilla state stores, keep the SDK wrapper small and pass the current view into your application renderer.
export function connectExperience(experienceId: string, onView: (view: JourneyView) => void) {
  const journey = sdk.journeys.get(experienceId);
  const unsubscribe = journey.view$.listen(onView);

  return {
    disconnect: unsubscribe,
  };
}
Your UI layer should still branch on the current view before showing actions.

Admission Handoff

When the view reaches an admitted sequence, send the admission grant to your own backend or trusted checkout boundary.
async function handleAdmission(admissionGrant: string) {
  await fetch("/api/fanfare/admission", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ admissionGrant }),
  });

  window.location.assign("/checkout");
}
Do not put the grant in query strings, screenshots, debug logs, or analytics payloads.

What To Test

  • Every public state your experience can render.
  • The presence or absence of buttons based on the current view.
  • Gated flows with generic customer-safe copy.
  • Admission handoff through your app boundary.
  • Cleanup when a route or embedded surface unmounts.
Avoid undocumented SDK mock helpers. For recommended app-boundary testing patterns, see Testing Your Integration and SDK Testing.

Next Steps