Getting Started

This guide walks through the complete SDK workflow: configure, initialize, load a journey, create baggage, scan for EBTs with scanForEbtTags, connect, transfer a tag, and clear a tag.

Step 1: Configure

Call configure once at app startup. This sets the API endpoint, client key, token provider, and optional theming and localization.

  • Kotlin

  • Swift

BagIdSdk.configure(BagIdConfig(
    apiBaseUrl = "https://api.bagid.com",
    sourceAppKey = "your-issued-client-key",
    tokenProvider = { myAuthService.getFederatedToken() },
    theme = BagIdTheme(
        primaryColor = "#003366",
        borderRadius = 12,
    ),
    localization = BagIdLocalization(
        locale = "nb-NO",
    ),
))
import BagIdSDK

enum AuthBridge {
    static func federatedToken() async throws -> String {
        // Host app obtains a federated token from its IdP/session.
        fatalError("Implement token retrieval")
    }
}

BagIdSdk.shared.configure(config: BagIdConfig(
    apiBaseUrl: "https://api.bagid.com",
    sourceAppKey: "your-issued-client-key",
    tokenProvider: AuthBridge.federatedToken
))

The tokenProvider is a suspend function that returns a federated identity token. In the initial SDK version, federated token authentication is the only supported auth mode.

Step 2: Initialize

Call initialize each time SDK features are needed (e.g., on screen entry). The SDK performs setup (session + certificate) and ensures the SDK is ready.

  • Kotlin

  • Swift

val result = BagIdSdk.initialize()

when (result) {
    is InitResult.Authenticated -> { /* ready */ }
    is InitResult.Unavailable -> {
        Log.e("BagID", "SDK init failed: ${result.reason}")
    }
}
import BagIdSDK

let result = await BagIdSdk.shared.initialize()

switch onEnum(of: result) {
case .authenticated:
    break // ready
case .unavailable(let unavailable):
    print("SDK init failed: \(unavailable.reason)")
}

On subsequent calls, initialize reuses stored tokens and certificates when still valid. If both access and refresh tokens have expired, the SDK calls the tokenProvider to obtain a fresh federated token.

For setup details, see Setup.

Step 3: Load journey

  • Kotlin

  • Swift

// From BCBP barcode (primary flow)
val journey = BagIdSdk.loadJourney(
    LoadJourneyRequest.Bcbp(bcbp = "M1DOE/JOHN  E...")
).getOrThrow()
import BagIdSDK

let journey = try await BagIdSdk.shared.loadJourneyOrThrow(
    request: LoadJourneyRequest.Bcbp(bcbp: "M1DOE/JOHN  E...")
)

Returns a Journey containing the journey ID, passengers, flights, and any existing baggage records.

The BCBP string should represent the passenger and journey the user has pre-loaded into the host app.

The SDK does not provide UI for journey management. The host app must present the journey to the user, including options to proceed (e.g. create baggage tags) and clear messaging for limitations or errors returned from the backend.

Step 4: Create baggage

  • Kotlin

  • Swift

// With DCS
val updated = BagIdSdk.createBaggageTag(
    CreateBaggageTagRequest(
        journeyId = journey.journeyId,
        passengerList = listOf(...),
    )
).getOrThrow()
import BagIdSDK

// let paxList: [Passenger] = ... // host app builds passenger selection
let updated = try await BagIdSdk.shared.createBaggageTagOrThrow(
    request: CreateBaggageTagRequest(
        journeyId: journey.journeyId,
        passengerList: paxList
    )
)

Returns the updated Journey with a populated baggage list including baggageId for each tag.

Step 5: Scan and connect

Collect BagIdScanEvent from scanForEbtTags(). When the user selects a device, call notifyDiscoveredModel(device) then connectEbt(device.deviceId).

  • Kotlin

  • Swift

val discovered = mutableListOf<DiscoveredEbtBleDevice>()

val scanJob = coroutineScope.launch {
    BagIdSdk.scanForEbtTags().collect { event ->
        when (event) {
            is BagIdScanEvent.DeviceFound -> discovered.add(event.device)
            is BagIdScanEvent.ScanError -> Log.e("BLE", "Scan error: ${event.message}")
        }
    }
}

// User selects `selected: DiscoveredEbtBleDevice`, then stop scanning and connect:
scanJob.cancel()
BagIdSdk.notifyDiscoveredModel(selected)
BagIdSdk.connectEbt(selected.deviceId).getOrThrow() // suspend — call from a coroutine
import BagIdSDK

var discoveredDevices: [DiscoveredEbtBleDevice] = []

let scanTask = Task {
    for await event in BagIdSdk.shared.scanForEbtTags() {
        switch onEnum(of: event) {
        case .deviceFound(let e):
            discoveredDevices.append(e.device)
        case .scanError(let e):
            print("Scan error: \(e.message)")
        }
    }
}

// User picks `selected`, cancel scan, notify, connect:
scanTask.cancel()
BagIdSdk.shared.notifyDiscoveredModel(device: selected)
_ = await BagIdSdk.shared.connectEbt(deviceId: selected.deviceId)

DiscoveredEbtBleDevice carries deviceId, signalStrength (RSSI), and identity (vendor / model class). For API calls you also need the tag uuid from ConnectedEbtDevice after connect (BagIdSdk.ebtBleDeviceState).

The SDK emits scan events only—the host provides selection UI.

Step 6: Transfer tag

transferTag connects if needed, runs lock / custody / authorize, writes the ticket over BLE, then attaches the device. Supply TransferTagRequest including uniqueDeviceId (connected tag uuid), displayTicket or journey + baggageId, and optional recordLocator / custodyProofSurname when custody proof is required.

  • Kotlin

  • Swift

val connected = BagIdSdk.ebtBleDeviceState.value.connectedDevice
    ?: error("Connect first")

val result = BagIdSdk.transferTag(
    TransferTagRequest(
        deviceId = connected.deviceId,
        uniqueDeviceId = connected.uuid,
        baggageId = selectedBaggage.baggageId,
        journeyId = journey.journeyId,
        journey = journey,
    ),
)

result.onSuccess { transfer ->
    Log.d("BagID", "BLE write=${transfer.bleWriteSuccess} attach=${transfer.attachmentSuccess}")
}.onFailure { error ->
    when (error) {
        is CustodyProofRequiredException -> { /* collect PNR + surname; retry with recordLocator + custodyProofSurname */ }
        else -> Log.e("BagID", "Transfer failed: ${error.message}")
    }
}
import BagIdSDK

// After connect, read ConnectedEbtDevice from ebtBleDeviceState (pattern depends on your app).
let request = TransferTagRequest(
    deviceId: connected.deviceId,
    uniqueDeviceId: connected.uuid,
    baggageId: selectedBaggage.baggageId,
    journeyId: journey.journeyId,
    journey: journey
)
do {
    let transfer = try await BagIdSdk.shared.transferTagOrThrow(request: request)
    print("BLE write=\(transfer.bleWriteSuccess) attach=\(transfer.attachmentSuccess)")
} catch {
    print("Transfer failed: \(error.localizedDescription)")
}

If the backend requires custody proof and those fields are omitted, the SDK fails with CustodyProofRequiredException and hints—collect PNR and surname in your UI and call transferTag again with recordLocator and custodyProofSurname.

Step 7: Clear tag

Provide BLE deviceId and backend uniqueDeviceId (tag uuid).

  • Kotlin

  • Swift

val connected = BagIdSdk.ebtBleDeviceState.value.connectedDevice
    ?: error("Connect first")

val result = BagIdSdk.clearTag(
    ClearTagRequest(
        deviceId = connected.deviceId,
        uniqueDeviceId = connected.uuid,
    ),
)

result.onSuccess {
    Log.d("BagID", "Clear success unlocked=${it.unlocked}")
}.onFailure { error ->
    Log.e("BagID", "Clear failed: ${error.message}")
}
import BagIdSDK

do {
    let clear = try await BagIdSdk.shared.clearTagOrThrow(
        request: ClearTagRequest(
            deviceId: connected.deviceId,
            uniqueDeviceId: connected.uuid
        )
    )
    print("Clear success unlocked=\(clear.unlocked)")
} catch {
    print("Clear failed: \(error.localizedDescription)")
}

Complete example

  • Kotlin

  • Swift

// 1. Configure (once at app startup)
BagIdSdk.configure(BagIdConfig(
    apiBaseUrl = "https://api.bagid.com",
    sourceAppKey = "wf-app-key",
    tokenProvider = { myAuthService.getFederatedToken() },
))

// 2. Initialize (silent auth + certificate)
BagIdSdk.initialize()

// 3. Load journey
val journey = BagIdSdk.loadJourney(
    LoadJourneyRequest.Bcbp(scanData = scannedBcbp)
).getOrThrow()

// 4. Create baggage
val updated = BagIdSdk.createBaggageTag(
    CreateBaggageTagRequest(journeyId = journey.journeyId, passengerList = paxList)
).getOrThrow()

// 5. Scan, select, notify, connect (suspend connect from your coroutine)
// var selected: DiscoveredEbtBleDevice = ...
BagIdSdk.notifyDiscoveredModel(selected)
BagIdSdk.connectEbt(selected.deviceId).getOrThrow()
val connected = BagIdSdk.ebtBleDeviceState.value.connectedDevice!!

// 6. Transfer tag
BagIdSdk.transferTag(
    TransferTagRequest(
        deviceId = connected.deviceId,
        uniqueDeviceId = connected.uuid,
        baggageId = selectedBaggage.baggageId,
        journeyId = journey.journeyId,
        journey = updated,
    ),
).getOrThrow()

// 7. Clear tag (later)
BagIdSdk.clearTag(
    ClearTagRequest(deviceId = connected.deviceId, uniqueDeviceId = connected.uuid),
).getOrThrow()
import BagIdSDK

// 1. Configure (once at app startup)
BagIdSdk.shared.configure(config: BagIdConfig(
    apiBaseUrl: "https://api.bagid.com",
    sourceAppKey: "wf-app-key",
    tokenProvider: AuthBridge.federatedToken
))

// 2. Initialize (silent auth + certificate)
_ = await BagIdSdk.shared.initialize()

// 3. Load journey
// let scannedBcbp: String = ...
let journey = try await BagIdSdk.shared.loadJourneyOrThrow(
    request: LoadJourneyRequest.Bcbp(scanData: scannedBcbp, airline: nil)
)

// 4. Create baggage
// let paxList: [Passenger] = ...
let updated = try await BagIdSdk.shared.createBaggageTagOrThrow(
    request: CreateBaggageTagRequest(journeyId: journey.journeyId, passengerList: paxList)
)

// 5. Scan / connect — omitting full scan loop; after selection:
// let selected: DiscoveredEbtBleDevice = ...
BagIdSdk.shared.notifyDiscoveredModel(device: selected)
_ = try await BagIdSdk.shared.connectEbt(deviceId: selected.deviceId)
// obtain `connected` from ebtBleDeviceState in your app

// 6. Transfer — use connected.uuid from ConnectedEbtDevice
_ = try await BagIdSdk.shared.transferTagOrThrow(
    request: TransferTagRequest(
        deviceId: connected.deviceId,
        uniqueDeviceId: connected.uuid,
        baggageId: selectedBaggage.baggageId,
        journeyId: journey.journeyId,
        journey: updated
    )
)

// 7. Clear
_ = try await BagIdSdk.shared.clearTagOrThrow(
    request: ClearTagRequest(deviceId: connected.deviceId, uniqueDeviceId: connected.uuid)
)

Alternative flow (without DCS)

Partners that manage their own journey and baggage data can use airline-supplied payloads instead of the BCBP/DCS flow (LoadJourneyRequest.AirlinePayload, createAirlineBaggage).

Load journey from airline-supplied payload

  • Kotlin

  • Swift

val journey = BagIdSdk.loadJourney(
    LoadJourneyRequest.AirlinePayload(passengers = listOf(...))
).getOrThrow()
import BagIdSDK

// let passengers: [AirlinePassenger] = ...
let journey = try await BagIdSdk.shared.loadJourneyOrThrow(
    request: LoadJourneyRequest.AirlinePayload(passengers: passengers)
)

Register airline-issued baggage tag

  • Kotlin

  • Swift

val updated = BagIdSdk.createAirlineBaggage(
    CreateAirlineBaggageRequest(
        journeyId = journey.journeyId,
        baggageTagNumber = "0701913714",
        airline = "WF",
        destinationAirport = "OSL",
    )
).getOrThrow()
import BagIdSDK

let updated = try await BagIdSdk.shared.createAirlineBaggageOrThrow(
    request: CreateAirlineBaggageRequest(
        journeyId: journey.journeyId,
        baggageTagNumber: "0701913714",
        airline: "WF",
        destinationAirport: "OSL"
    )
)

Integration guidance

Architecture

The SDK handles BLE and HTTP orchestration internally. The host app provides a thin coordination layer between its UI and BagIdSdk.

Diagram

The coordinator:

  • Calls BagIdSdk.configure() at startup with the token provider.

  • Calls BagIdSdk.initialize() when SDK features are needed.

  • Translates user actions (scan, select device, transfer) into SDK calls.

  • Handles results and errors, then surfaces them to the UI.

Token provider behavior

The SDK uses the token provider passed in configure(). It should return a token without requiring user interaction during SDK operations.

The SDK calls the token provider:

  • On first initialize() when no stored session exists.

  • When both access and refresh tokens have expired.

  • During background token refresh if the refresh token is rejected.

  • Mid-operation if an HTTP call returns 401 (transparent retry).

Initialize lifecycle

initialize() is safe to call on every screen entry. Its behavior adapts to session state:

  • If the SDK is already ready, it returns quickly.

  • If setup is needed, it performs the required background initialization automatically.

  • If user re-authentication is required and cannot be completed silently, operations return SessionExpired.

Session expiry during use

If the session expires while the user is actively using the SDK (for example, after app backgrounding), the SDK attempts silent re-authentication via the token provider. If that fails, operations return SessionExpired.

Permissions

Android

On Android 12+ (API 31), request BLUETOOTH_SCAN and BLUETOOTH_CONNECT. On older versions, request ACCESS_FINE_LOCATION.

iOS

Add NSBluetoothAlwaysUsageDescription to Info.plist. The system prompts automatically when the SDK initiates a BLE scan.

  • Kotlin

  • Swift

val blePermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    arrayOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
} else {
    arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
}
import CoreBluetooth

// iOS Bluetooth permission is driven by Info.plist usage descriptions.
// Ensure `NSBluetoothAlwaysUsageDescription` is set (see xref:install.adoc[]).

// Optionally, you can proactively trigger the permission prompt.
// Keep `CBCentralManager` alive for the duration of your app/session (static storage is fine).
enum BluetoothPermissionWarmup {
    static let central = CBCentralManager(
        delegate: nil,
        queue: nil,
        options: [CBCentralManagerOptionShowPowerAlertKey: true]
    )
}

State observation

The SDK exposes a StateFlow<BagIdState> for runtime status observation:

  • Kotlin

  • Swift

val state by BagIdSdk.state.collectAsState()

when {
    state.connectedDevice != null -> ShowConnectedUI(state.connectedDevice!!)
    state.lastError != null -> ShowError(state.lastError!!)
    else -> ShowDefaultUI()
}
import BagIdSDK
import Combine

@MainActor
final class BagIdCoordinator: ObservableObject {
    @Published private(set) var connectedDevice: DeviceInfo?
    @Published private(set) var lastError: String?

    private var stateTask: Task<Void, Never>?

    func startObservingSdkState() {
        stateTask?.cancel()
        stateTask = Task {
            for await state in BagIdSdk.shared.state {
                connectedDevice = state.connectedDevice
                lastError = state.lastError
            }
        }
    }

    deinit {
        stateTask?.cancel()
    }
}

Error handling

On Android, most operations return Result<T>. Use .onSuccess { }, .onFailure { }, or .getOrThrow().

On iOS, SKIE typically generates OrThrow variants for suspend functions returning Result<T>. Use do/catch with try await …​OrThrow(…​) (see Versioning & FAQ).

Common failure scenarios:

  • InitResult.Unavailable — token provider, federation, or network during initialize().

  • CustodyProofRequiredException — transfer needs PNR and surname; retry transferTag with recordLocator and custodyProofSurname.

  • BLE connect / write errors — surfaced on Result.failure or BagIdState.bleLastError.

  • Attachment after successful BLE write — TransferResult.pendingRetry when HTTP attachment fails.

Lifecycle management

  • Call configure() once at app startup.

  • Call initialize() each time SDK features are needed.

  • Cancel scan collectors when the scan UI is dismissed.

  • BLE operations are serialized internally.

Retry behavior

Some HTTP paths may retry transient failures; behavior can vary by SDK version. Prefer observing Result / BagIdState and consulting release notes.

Operation Notes

HTTP calls (lookups, authorizations, attachments, unlocks)

May include limited retries; verify in your SDK build

Session renewal

Call initialize() when the token provider can supply a fresh federated token

BLE connect

Surfaces error to host; host may retry

Custody proof (PNR / surname)

Host-driven — supply fields on TransferTagRequest or handle CustodyProofRequiredException

Device attachment after successful BLE write

TransferResult.pendingRetry when BLE write succeeded but attachment did not