> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getflowpilot.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting

> Symptoms, causes, and fixes for the most common FlowPilot iOS SDK integration problems.

When a flow does not show up or does not look right, the cause is usually one of a handful of things: the SDK is not configured, the placement has no flow for this user, the network is slow with nothing cached, or an asset was not uploaded. This page walks the common symptoms. For the error type and codes themselves, see [Error handling](/ios-sdk/error-handling).

<Tip>
  Before anything else, raise the log level. Set `logLevel: .debug` (or `.verbose`) in your `FlowPilotConfiguration`, or call `FlowPilot.shared?.enableDebugOverlay()` at runtime. The SDK logs each resolve, the delivery source (cache, network, stale cache, or bundled), and why a flow was rejected. Most issues below are obvious once logging is on. See [Configuration](/ios-sdk/configuration).
</Tip>

## Common problems

<AccordionGroup>
  <Accordion title="Nothing shows when I present a placement">
    A present call that returns without showing anything almost always means FlowPilot had no flow to show, or the SDK was never set up. Work through these in order:

    1. **The SDK is not configured.** If `FlowPilot.configure(...)` never ran (or ran with a key that does not start with `fp_`), `FlowPilot.shared` is `nil` and `FlowPilot.shared?.presentPlacement(...)` silently does nothing. Check the log for `FlowPilot SDK initialized - v1.0.0`. If you see `Invalid API key format` instead, the key is wrong. See [Configuration](/ios-sdk/configuration).
    2. **Wrong App ID.** The flow, placement, and key must all belong to the same app. A mismatched `appId` resolves against the wrong app and finds nothing.
    3. **The placement has no flow for this user.** The placement may be paused, have no default flow attached, or exclude this user by audience targeting or frequency rules. The resolve returns "no flow" and you get a `flowNotFound` (or `targetingNotMet` / `frequencyLimitReached`) error. This is expected, not a bug. Check the placement in the dashboard. See [Placements](/dashboard/placements) and [Audience targeting](/dashboard/audience-targeting).
    4. **The flow is a draft, or is not attached.** Only a published flow that is attached to the placement is served. Publishing alone does not attach it. See [Publishing](/editor/publishing).
    5. **The platform excludes iOS.** A placement gates which platforms it serves. If iOS is not included, the resolve returns no flow for an iOS device.
    6. **Offline with a cold cache.** With no network, no cached flow, and no bundled default, there is nothing to present and the resolve throws.

    **Fix:** turn on `.debug` logging and read which step fails. For anything user-facing, use the never-throwing overload so the user always sees something:

    ```swift theme={null}
    await FlowPilot.shared?.presentPlacement(
        "onboarding",
        from: self,
        fallback: { MyNativeOnboardingViewController() }
    )
    ```
  </Accordion>

  <Accordion title="The flow never appears and the SDK seems uninitialized (`sdk_not_initialized`)">
    `FlowPilot.shared` is `nil`. Causes:

    * `FlowPilot.configure(...)` was never called.
    * It was called with an API key that does not start with `fp_`. The SDK logs `Invalid API key format` and leaves `shared` as `nil`.
    * A present or resolve call ran before `configure(...)` finished (for example, from a view that loads earlier than your app's launch code).

    **Fix:** call `FlowPilot.configure(...)` exactly once, as early as possible (the SwiftUI `App` initializer or `application(_:didFinishLaunchingWithOptions:)`), before any present or resolve. See [Configuration](/ios-sdk/configuration) and [Quickstart](/ios-sdk/quickstart).
  </Accordion>

  <Accordion title="`invalid_api_key` when resolving">
    The server rejected the key with a `401` during a resolve. Common causes:

    * **Wrong key type.** You must use a workspace **SDK** key, not a dashboard or secret key. See [API keys](/dashboard/api-keys).
    * **Wrong workspace or app.** The key belongs to a different workspace than the flow and placement.
    * **Wrong environment.** A key minted for one environment will not work against another. Match `environment` in `FlowPilotConfiguration` to where the key and flows live.

    **Fix:** copy the SDK key again from the dashboard, confirm the `appId` matches, and confirm `environment` points at the right backend. Note that a key with the wrong prefix never gets this far: it fails the `fp_` check at `configure(...)` time instead.
  </Accordion>

  <Accordion title="`timeout` or very slow first present on poor networks">
    The live resolve did not finish within `resolveTimeout` (default `4.0` seconds) and there was nothing cached or bundled to fall back to, so the call threw `timeout`.

    **Fixes, in order of impact:**

    * **Ship a bundled default flow** so the first present always has something offline. See [Offline and bundled flows](/ios-sdk/offline-bundled-flows).
    * **Prefetch at launch** so the placement is warm before the user reaches it. Declare it once in config with `prefetchOnLaunch: ["onboarding"]`, or call `await FlowPilot.shared?.prefetch(["onboarding"])` (add `warmMedia: true` to also warm first-screen images). See [Prefetching](/ios-sdk/prefetching).
    * **Use the never-throwing present overload** (`fallback:`) so a timeout falls through to cache, then bundled, then your own UI instead of throwing.
    * **Raise `resolveTimeout`** only if your audience is on consistently slow networks. It bounds the entire resolve, so a higher value means a longer possible wait. The minimum is `0.5`.

    See [Caching](/ios-sdk/caching) for how the cache and timeout interact.
  </Accordion>

  <Accordion title="A custom component renders blank (`custom_component_not_found`)">
    The flow references a custom component the SDK could not find, so it shows a placeholder instead of your view. Causes:

    * **Not registered.** You never called `registerCustomComponent(key:version:definition:)`, or you registered it after presenting. Register before you present.
    * **Key mismatch.** The `key` you registered does not match the component key in the flow exactly (it is case-sensitive).
    * **Version mismatch.** The SDK matches on `key + version`. If the flow uses version `2` and you registered version `1`, it will not match (a version-`1` registration also answers the bare, unversioned key, but not other versions).

    **Fix:** register the exact key and version the flow uses, before presenting. The editor's component contract shows both. See [Custom components](/ios-sdk/custom-components).
  </Accordion>

  <Accordion title="Fonts look wrong or fall back to the system font">
    A flow renders with the wrong typeface when the SDK cannot resolve the font:

    * **The font was not uploaded.** Custom fonts must be uploaded to the workspace in the dashboard so the resolve response includes them. See [Custom fonts](/dashboard/custom-fonts).
    * **Family, weight, or style mismatch.** The flow references a family/weight/style combination that does not exist among the uploaded variants, so it falls back to the system font.
    * **Offline with no bundled font assets.** A bundled (offline) flow only has its fonts when you ship them in the asset bundle. Without a `fonts` entry in the `.flowassets` manifest, an offline render uses the system font.

    **Fix:** upload the exact families and weights the flow uses, and for offline support include the fonts in the bundled asset manifest. See [Offline and bundled flows](/ios-sdk/offline-bundled-flows).
  </Accordion>

  <Accordion title="Images do not load when offline">
    Online, images stream from the CDN and get cached. Offline, an image only appears if its bytes are already in the cache or shipped with the flow.

    * **No bundled assets.** A bundled default flow with no image assets shows blank images offline. Ship the images in the `.flowassets` folder and declare them in `manifest.json` so the SDK seeds the image cache before first paint.
    * **Cold cache.** If the user has never seen the flow online, nothing is cached yet. Preload or prefetch while the network is available.

    **Fix:** for guaranteed offline rendering, register the bundled flow with its assets. See [Offline and bundled flows](/ios-sdk/offline-bundled-flows) and [Media preloading](/ios-sdk/media-preloading).
  </Accordion>

  <Accordion title="I republished the flow but the app still shows the old version">
    Two things can keep an old flow on screen:

    * **Cache TTL.** The SDK serves a fresh cached flow without hitting the network until it goes stale. The freshness window comes from the server's `cache_ttl_seconds` (the SDK defaults to 300 seconds when the server omits it). Until then, the cached copy is used. Call `FlowPilot.shared?.clearCache()` to force a re-resolve during testing.
    * **The placement still points at the old version.** Publishing a new flow version does **not** automatically repoint a placement. Until you attach the new version to the placement, the resolve keeps returning the old one. This is the most common cause. See [Publishing](/editor/publishing).

    **Fix:** attach the new version to the placement in the dashboard, then clear the cache (or wait out the TTL) to see it on device.
  </Accordion>

  <Accordion title="An experiment user keeps switching variants">
    Variant assignment is deterministic and sticky: the backend buckets a user by their user id, so the same user gets the same variant every time. If a user keeps flipping variants, their **user id is not stable**.

    The SDK generates a stable per-install user id and stores it in the Keychain, so it normally survives app restarts. Switching usually means that id is being reset: a fresh install on a wiped simulator, a cleared Keychain, or testing across multiple devices. There is **no public API to set a custom user id** in v1.0.0, so you cannot pin it yourself yet.

    **Fix:** test variant stickiness on a real device or a persistent simulator without resetting it between launches. See [Variables and SDK context](/ios-sdk/variables-and-context) and [Reading experiment results](/dashboard/reading-results).
  </Accordion>

  <Accordion title="How do I get more diagnostic output?">
    Raise the log level. The SDK logs the resolve path, the delivery source, font and image loading, and validation failures.

    ```swift theme={null}
    FlowPilot.configure(
        FlowPilotConfiguration(
            apiKey: "fp_live_...",
            appId: "your-app-id",
            environment: .production,
            logLevel: .debug   // or .verbose for the most detail
        )
    )
    ```

    At runtime you can also call `FlowPilot.shared?.enableDebugOverlay()` (raises the level to `.verbose`) and `FlowPilot.shared?.logState()` to print the current user id, session id, environment, and registered component counts. Set `FlowPilot.debugBordersEnabled = true` to draw borders around rendered components when debugging layout.
  </Accordion>
</AccordionGroup>

## When you cannot reproduce it

* **Test the resilient path.** Turn on Airplane Mode and present. With the never-throwing overload and a bundled flow, the user should still see something. If they see a blank screen, you are missing a bundled default or its assets.
* **Confirm the delivery source.** The `delivery_source` on analytics events (and the debug logs) tells you whether a render came from the network, cache, stale cache, or bundled default. A surprising source often explains a surprising flow.
* **Check the dashboard first.** Most "nothing shows" cases are a paused placement, an unattached flow, or a targeting rule, not an SDK bug.

## Related pages

* [Error handling](/ios-sdk/error-handling)
* [Configuration](/ios-sdk/configuration)
* [Offline and bundled flows](/ios-sdk/offline-bundled-flows)
* [Caching](/ios-sdk/caching)
* [Prefetching](/ios-sdk/prefetching)
* [Placements](/dashboard/placements)
* [API reference](/ios-sdk/api-reference)
