# Embed on the web (https://www.getfounderhq.com/docs/journeys/embed-on-the-web)

Render a published Journey inside your React app. About 5 minutes.

No code? Use the [public link, the embed, or the phone preview](/journeys/share)
from the **Share** button in the editor instead.

<Callout type="info" title="Before you start">
You need a published Journey in FounderHQ, its Journey ID, and a
publishable key (`fhq_pk_XXXX`) from the **Journey embed** preset. Open
Journeys in FounderHQ to publish one and to copy its ID. See
[Journeys](/journeys) for how keys and allowed origins work.
</Callout>

## Install the package

```npm
npm install @founderhq/journeys
```

## Render the Journey

Import the component and the stylesheet. The stylesheet also loads the
bundled Inter and Lora fonts, so the Journey looks the same in every
browser without a font request to a third party.

```tsx
import { Journey } from "@founderhq/journeys";
import "@founderhq/journeys/styles.css";

export function Onboarding() {
  return (
    <Journey
      apiKey="fhq_pk_XXXX"
      journeyId="journey_123"
      storageKey="onboarding"
    />
  );
}
```

`apiKey` and `journeyId` are always required. With nothing else passed, the
SDK fetches the published config from FounderHQ and renders it.

`storageKey` is where the SDK keeps answers and the current screen in the
browser. Give each Journey its own key so a person who reloads the page
lands back where they left off.

Next.js App Router? Add `"use client"` at the top of the file. The Journey
is an interactive client component.

## What the SDK does on render

Everything goes to `https://app.getfounderhq.com`. You do not configure the
host.

1. **Check access.** `POST /api/v1/journeys/{journeyId}/validate` with the
   key in an `Authorization: Bearer` header. FounderHQ checks the key, the
   request origin, the brand, the Journeys entitlement, and that the
   Journey is published with an active version. A failure renders the error
   state and nothing else is sent.
2. **Fetch the config.** `GET /api/v1/journeys/{journeyId}` returns the
   published config, its `revisionId`, its `version`, and the Journey's
   contact-linking mode. Pass a `config` prop to skip this call — step 1
   still runs, so a local config never bypasses access control.
3. **Render and capture.** Step events queue in the browser and go out as
   `POST /api/v1/journeys/{journeyId}/capture`, ten per batch by default.
4. **Finish.** The `complete` event flushes at once. The SDK also tries an
   exit-safe flush on `visibilitychange` and `pagehide`.

The queue survives a reload. It holds up to 100 events for 24 hours, keeps
the session, revision, and context each event was created with, and retries
a failed batch later. Every event carries an ID, so a duplicate delivery is
counted once. A `429` is obeyed: the SDK waits out `Retry-After` before it
tries again.

`baseUrl` exists for local testing only. The web SDK honors a localhost
value and falls back to `https://app.getfounderhq.com` for anything else.

## Tell FounderHQ who the person is

If your app already knows the person, pass `identity`. FounderHQ uses it to
link the response to a contact.

```tsx
<Journey
  apiKey="fhq_pk_XXXX"
  journeyId="journey_123"
  identity={{
    externalId: "usr_1042",
    email: "jane@acme.com",
    phone: "+1 212 555 0123",
  }}
/>
```

Any non-empty `identity` field wins over an email, phone, or ID the person
typed into the Journey. Their typed answers are still saved in the
response. Pass only the fields you actually know.

## React when the Journey finishes

`onEvent` fires as the person moves through the flow. Use the `complete`
event to send them onward.

```tsx
<Journey
  apiKey="fhq_pk_XXXX"
  journeyId="journey_123"
  onEvent={(event) => {
    if (event.type === "complete") {
      window.location.href = "/dashboard";
    }
  }}
/>
```

Event types: `session_start`, `step_view`, `step_submit`, `navigate`,
`complete`, and `purchase_intent`. Each event carries `answers` (what the
person entered) and `computedVariables` (values derived from those
answers), kept separate.

## Verify

1. Load the page in your app. The first screen of your Journey renders.
2. Answer a screen and continue. Reload the page — you come back to the
   same screen with your answer still filled in.
3. Finish the flow, then open the Journey in FounderHQ. The response is
   listed, and it carries the contact you passed in `identity`.

## Props

| Prop | What it does |
| --- | --- |
| `apiKey` | Your publishable key. Required. |
| `journeyId` | The Journey to render. Required. |
| `config` | Render a config you already have instead of fetching it. Access is still checked with FounderHQ. |
| `storageKey` | Where answers and progress are saved in the browser. |
| `identity` | `externalId`, `email`, `phone` for contact linking. |
| `initialAnswers` | Seed answers at first render, for data only your app knows (live pricing plans, for example). |
| `initialOptions` | Replace an option list at render time, keyed by the answer variable. |
| `onEvent` | Called for every Journey event. |
| `onOpenURL` | Handle link buttons yourself instead of letting the browser navigate. |
| `onDiscountCodeApply` | Validate a discount code and return new pricing. Your app owns the rules. |
| `capture` | `false` turns off sending events to FounderHQ. An object adds custom context. |
| `theme` | CSS class(es) for the Journey root, overriding the theme set in FounderHQ. |
| `className` | Extra class on the Journey root. |
| `presentation` | `"web"` (default) or `"native"` for an edge-to-edge app WebView. |
| `loadingComponent` | What to show while the config loads. |
| `errorComponent` | What to show when the config cannot load. |
| `baseUrl` | Local testing override. Only a localhost value is honored. |

## Injecting data your app owns

Some screens need values that live in your app, not in the Journey config —
live prices from StoreKit or your own API, or a country list. Pass them in
`initialAnswers` and reference them from the Journey config with a
template.

```tsx
<Journey
  apiKey="fhq_pk_XXXX"
  journeyId="journey_123"
  initialAnswers={{
    pricingPlans: [
      {
        id: "pro_monthly",
        name: "Pro",
        price: {
          amount: 29,
          currency: "USD",
          period: "month",
          trial: { days: 7 },
        },
      },
    ],
  }}
/>
```

Keys you declared as computed variables are ignored here. Their formulas
always win.

## Good practice

- **One key per brand.** A Journey embed key only reads Journeys in the
  brand it was created for. Do not carry one key to a second brand.
- **List every origin you serve from.** Staging and preview domains are
  separate origins. A missing one fails with `403`.
- **Test against localhost.** Run FounderHQ locally and pass
  `baseUrl="http://localhost:3000"`. Any other override is ignored.
- **Give each Journey its own `storageKey`.** Two Journeys sharing one key
  restore each other's answers.
- **Leave capture on.** Turning it off with `capture={false}` also removes
  responses and funnel analytics for that embed.

See [Limits and reliability](/journeys#limits-and-reliability) for the
batch, body, and retry rules the SDK already follows for you.

## Troubleshoot

### The Journey shows "This Journey is unavailable"

The access check or the config fetch failed. Check that the Journey is
published, that the Journey ID matches, that the publishable key belongs to
the same brand as the Journey, and that the page's origin is on the key's
allowed-origin list.

### The Journey renders with the wrong fonts

Import `@founderhq/journeys/styles.css`. Without it the Journey inherits
your page's fonts and loses its own layout rules.

### Answers come back with no contact attached

Pass `identity` with at least one of `externalId`, `email`, or `phone`.
Without it, FounderHQ can only link a contact when the person types an
email or phone into the Journey itself.
