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

# Core SDK Quickstart

> Start an experience journey and render from the public journey view.

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

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

## Initialize the SDK

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

```ts theme={null}
const journey = sdk.journeys.get("exp_123");
```

The journey exposes four stores:

| Store          | Use 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`

```ts theme={null}
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](/sdk/core/gates-and-auth) for how this differs from the gated-stage reroute flow.

## Render routed sequence states

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

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

* Read [Journey State](/sdk/core/journey-state) to understand the full state model.
* Read [Gates and Auth](/sdk/core/gates-and-auth) before implementing authentication or access-code flows.
* Use [Core Headless Example](/sdk/examples/core-headless) for a more complete UI sketch.
