---
title: "Native app SDKs"
description: "Compare SeatLayer SDKs for Flutter, React Native, Swift and Kotlin. Choose a complete native buyer UI, custom controls, composed layout or raw seat map."
---

SeatLayer has four first-party native app SDKs. They share inventory, selection,
hold, recovery, and checkout concepts. Flutter is `0.8.0`; React Native, iOS
and Android are independently verified at `0.3.4` in the SDK catalog.

The native event picker accepts one published Event per picker. Performance
Groups and Fixed Renewable Seasons are browser/API/server products and are not
native SDK configurations.

Use this page to choose the platform package and the amount of buyer UI your
application owns. Every platform supports the same four-level product ladder:
drop in the complete picker, customise it, compose your own layout from public
parts, or use only the raw map.

## SDK capability matrix

| Platform | Install | Complete picker | Host-owned layout | Raw map |
| --- | --- | --- | --- | --- |
| Flutter | `seatlayer: 0.8.0` | `SeatLayerPicker` | `SeatLayerPickerScope` + Flutter widgets | `SeatLayerView` + controller |
| React Native | `@seatlayer/react-native@0.3.4` | `SeatLayerPicker` or `SeatLayerPickerModal` | `SeatLayerPickerScope` + React Native components | `SeatLayerView` + controller |
| iOS | Swift package `0.3.4` | `SeatLayerPicker` or `SeatLayerPickerViewController` | SwiftUI scope/components or UIKit map/controller | `SeatLayerView` |
| Android | Core + Compose `0.3.4` | Compose `SeatLayerPicker` or View/XML `SeatLayerPickerView` | Compose scope/components or headless Android Views | `SeatLayerView` |

<Aside type="note" title="Machine-readable SDK catalog">
  Check the [SDK catalog](/sdk-catalog.json) before an automated install
  or upgrade. It records the verified installable version, primary surface,
  integration levels, and registry for every SDK. The four mobile
  records carry individual verification dates and registry/source links. Do not
  infer that every package shares a release number.
</Aside>

## Choose an integration path

<CardGrid>
  <Card title="Ship one cross-platform screen" icon="ph:device-mobile">
    Choose [Flutter](/buyer-sdk/flutter) or
    [React Native](/buyer-sdk/react-native). Both packages provide the
    complete picker and public components for a custom layout.
  </Card>
  <Card title="Use the platform-native stack" icon="ph:brackets-curly">
    Choose [iOS](/buyer-sdk/ios) for SwiftUI/UIKit or
    [Android](/buyer-sdk/android) for Compose/View/XML. The buyer chrome is
    native and every layer remains replaceable.
  </Card>
  <Card title="Build a completely custom experience" icon="ph:squares-four">
    Use the platform scope/state holder, one map, the typed controller, and
    public components. You own hierarchy and expression without rebuilding
    inventory, holds, recovery, or checkout state.
  </Card>
  <Card title="Keep only the venue surface" icon="ph:map-trifold">
    Use the preserved raw map when your application intentionally owns every
    surrounding control, confirmation, cart, timer, recovery path, and hold
    transition.
  </Card>
</CardGrid>

## What “native picker” means

The SDKs divide responsibility at a deliberate seam:

| Native application UI owns | SeatLayer SDK owns |
| --- | --- |
| Header, event identity, filters, navigation, confirmation, quantities, cart, hold status, checkout, loading and errors | Seats, labels, section geometry, map hit testing, pan and pinch, venue camera, authored 3D, and panorama pixels |
| Platform layout, accessibility semantics, back navigation, safe areas, app lifecycle and haptics | Authoritative inventory projection and map capabilities |

The native picker is therefore not a screenshot or generic hosted checkout. Its
buyer chrome is made from Flutter widgets, React Native components, SwiftUI or
UIKit, or Compose/Android Views. SeatLayer keeps venue geometry, inventory, and
map behavior consistent behind the public native APIs. The map itself uses the
shared WebView renderer; native UI refers to the controls and layout around it.

## The customization ladder

All four SDKs follow the same product model:

1. **Drop in the complete flow.** Supply an event configuration and a checkout
   callback.
2. **Keep the flow, change its expression.** Apply theme, strings, options,
   style slots, or replace one complete part through a builder.
3. **Own the hierarchy.** Place the SDK scope/state holder, map, controller,
   and public components in a layout your app controls.
4. **Use only the raw map.** Own every piece of buyer UI and hold orchestration
   yourself.

Moving down the ladder does not move booking into the app. Every path ends at
an opaque `holdId` that goes to your trusted backend.

## Install the SDKs

<Tabs syncKey="seatlayer-mobile-sdk">
  <TabItem label="Flutter 0.8.0">
    ```yaml title="pubspec.yaml"
    dependencies:
      seatlayer: 0.8.0
    ```

    ```dart title="ticket_screen.dart"
    import 'package:seatlayer/seatlayer.dart';

    SeatLayerPicker(
      configuration: SeatLayerConfiguration(
        event: 'ev_your_event_key',
        publicKey: 'pk_test_your_public_key',
      ),
      themeMode: SeatLayerThemeMode.auto,
      onCheckout: (handoff) async {
        await checkoutBackend.begin(holdId: handoff.holdId);
      },
    );
    ```

    Continue with the [Flutter guide](/buyer-sdk/flutter),
    [customisation](/buyer-sdk/flutter/customise), or a
    [custom layout](/buyer-sdk/flutter/custom-layout).
  </TabItem>

  <TabItem label="React Native 0.3.4">
    ```bash
    npm install @seatlayer/react-native@0.3.4 react-native-webview
    ```

    ```tsx title="TicketPickerScreen.tsx"
    import React, { useMemo } from 'react';
    import {
      SeatLayerPicker,
      type SeatLayerConfiguration,
    } from '@seatlayer/react-native';

    export function TicketPickerScreen() {
      const configuration = useMemo<SeatLayerConfiguration>(
        () => ({
          event: 'ev_your_event_key',
          publicKey: 'pk_test_your_public_key',
          currency: 'USD',
        }),
        [],
      );

      return (
        <SeatLayerPicker
          configuration={configuration}
          themeMode="auto"
          onCheckout={(handoff) =>
            checkoutBackend.begin({ holdId: handoff.holdId })
          }
        />
      );
    }
    ```

    Continue with the [React Native guide](/buyer-sdk/react-native),
    [native-picker composition](/buyer-sdk/react-native/native-picker), or
    [raw map](/buyer-sdk/react-native/raw-map).
  </TabItem>

  <TabItem label="iOS 0.3.4">
    ```swift title="Package.swift"
    dependencies: [
        .package(
            url: "https://github.com/seatlayer/seatlayer-ios.git",
            exact: "0.3.4"
        )
    ]
    ```

    ```swift title="TicketPicker.swift"
    import SeatLayer
    import SwiftUI

    struct TicketPicker: View {
        var body: some View {
            SeatLayerPicker(
                configuration: SeatLayerConfiguration(
                    event: "ev_your_event_key",
                    publicKey: "pk_test_your_public_key",
                    currency: "USD"
                ),
                onCheckout: { handoff in
                    try await checkoutBackend.begin(holdId: handoff.holdId)
                }
            )
        }
    }
    ```

    Continue with the [iOS guide](/buyer-sdk/ios),
    [SwiftUI/UIKit composition](/buyer-sdk/ios/native-picker), or
    [raw map](/buyer-sdk/ios/raw-map).
  </TabItem>

  <TabItem label="Android 0.3.4">
    ```kotlin title="app/build.gradle.kts"
    dependencies {
        implementation("io.seatlayer:seatlayer-android:0.3.4")
        implementation("io.seatlayer:seatlayer-android-compose:0.3.4")
    }
    ```

    ```kotlin title="CheckoutActivity.kt"
    class CheckoutActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                SeatLayerPicker(
                    configuration = SeatLayerConfiguration(
                        event = "ev_your_event_key",
                        publicKey = "pk_test_your_public_key",
                        currency = "USD",
                    ),
                    modifier = Modifier.fillMaxSize(),
                    onCheckout = { handoff ->
                        checkoutBackend.begin(holdId = handoff.holdId)
                    },
                    onError = ::reportPickerError,
                    onClose = ::finish,
                )
            }
        }
    }
    ```

    Continue with the [Android guide](/buyer-sdk/android),
    [Compose/View composition](/buyer-sdk/android/native-picker), or
    [raw map](/buyer-sdk/android/raw-map).
  </TabItem>
</Tabs>

The examples use a publishable key for Public inventory. For login-gated,
presale, partner, channel, or other private inventory, omit `publicKey` and use
the platform's renewable `buyerAccessTokenProvider`. Implement that provider
with
[`POST /v1/events/:key/buyer-access-sessions`](/server-api/buyer-access-sessions)
on your authenticated backend. Mint a native buyer-access session for the
Event, return only the short-lived token to the provider, and treat the SDK's
access context as opaque. Keep the token in memory and never expose a SeatLayer
secret to the application.

## Shared security boundary

The mobile app may select inventory and create or resume a temporary hold. It
must not decide the amount to charge or book inventory with a secret key.

1. Send the opaque `holdId` and your own order context to your backend.
2. Inspect the hold and calculate the price from trusted server data.
3. Charge through your payment provider.
4. Reuse the same `bookingRef` so retries remain idempotent.
5. Treat expiry and inventory conflicts as recoverable selection states.

Read [holds and checkout](/buyer-sdk/holds-and-checkout) before connecting a
production order flow.

## Troubleshooting

Start with the typed SDK error or event; do not infer failure from an empty
screen or a paused countdown.

| Symptom or signal | Likely boundary | Safe response |
| --- | --- | --- |
| Load fails before ready | No network, temporary service failure, or a zero-sized/unmounted view | Give the view a definite mounted size, verify connectivity, and retry through the SDK's public reload path. Do not issue commands until ready. |
| `accessUnavailable`, `origin_mismatch`, or an access-provider failure | The native buyer-access session is invalid, expired, revoked, or the provider did not return a token | Mint again from your backend through [buyer access sessions](/server-api/buyer-access-sessions), return only the token to the provider, and keep it in memory. Do not fall back from private to Public inventory. |
| `sl_incompatible` or the platform's compatibility error | The installed app SDK is no longer compatible with the service contract | This is not retryable in place. Upgrade to a current SDK version, validate it, and ship an app update. |
| `sl_timeout` | A command did not receive its correlated response before the configured deadline | Show a retryable state. For inventory-changing commands, inspect `getCurrentHold()` or resume the known hold before retrying so a late success is not duplicated. |
| `destroyed` or a command after unmount | The view/controller lifecycle ended | Stop sending commands and create one new view/controller pair for the next session. |
| `holdExpired` or a null hold after foreground/restoration | Server ownership ended while the app timer was paused | Clear checkout state, return to selection, and let the buyer choose again. Never extend or book from a local countdown alone. |
| `selectedObjectsUnavailable`, conflict details, or `sold_out` | Inventory changed concurrently | Remove only the reported unavailable objects, explain the change, and let the buyer reselect. Do not silently recreate a charge. |
| Test inventory appears live | The host ignored `ReadyInfo.mode` | Render an unmistakable Test Mode disclosure; test Events do not create live bookings. |
| Pan/pinch is unreliable or controls cannot be reached | The venue map is nested in another scrolling/gesture surface or covered by an intercepting overlay | Let the map own its gestures, keep host overlays accessible, and use typed camera/navigation commands rather than raw touch forwarding. |

Platform guides add the exact error types, event names, lifecycle hooks, and
tagged source references for their package.

## Product scope

The documented mobile SDK contracts select inventory for one published Event.
The browser SDK separately provides
[PerformanceGroupPicker](/buyer-sdk/performance-groups) and
[SeasonPicker](/buyer-sdk/seasons). Do not assume those browser products can be
passed to a native SDK's `event` configuration until a platform release
documents a versioned native contract for them.

## Production checklist

- Pin the exact package version or release tag you validated.
- Give the map a definite size; do not place it inside another pan/zoom surface.
- Keep buyer tokens in memory and secrets on the server.
- Preserve an open `holdId` only when your checkout promises restoration.
- Test background/foreground reconciliation, rotation, safe areas, back
  navigation, expiry, release, conflict, and retry behavior.
- Verify test/live mode disclosure and any required SeatLayer attribution.
- Smoke-test supported physical iOS and Android devices before rollout.
- Confirm the backend derives price from the inspected hold, not app input.

## Platform guides

- [Flutter SDK](/buyer-sdk/flutter)
- [React Native SDK](/buyer-sdk/react-native)
- [iOS SDK](/buyer-sdk/ios)
- [Android SDK](/buyer-sdk/android)