> ## 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.

# Headless Integration

> Build custom UI from the core SDK journey view.

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

```bash theme={null}
pnpm add @fanfare-io/fanfare-sdk-core
```

## Initialize Once

Create the SDK in the browser area that owns the customer experience.

```ts theme={null}
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.

```ts theme={null}
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.

```ts theme={null}
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.

```ts theme={null}
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](/getting-started/testing) and [SDK Testing](/sdk/reference/testing).

## Next Steps

* [Core SDK Quickstart](/sdk/core/quickstart)
* [Core Headless Example](/sdk/examples/core-headless)
* [Journey State](/sdk/core/journey-state)
