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.
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 duringinitialize(). -
CustodyProofRequiredException— transfer needs PNR and surname; retrytransferTagwithrecordLocatorandcustodyProofSurname. -
BLE connect / write errors — surfaced on
Result.failureorBagIdState.bleLastError. -
Attachment after successful BLE write —
TransferResult.pendingRetrywhen 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 |
BLE connect |
Surfaces error to host; host may retry |
Custody proof (PNR / surname) |
Host-driven — supply fields on |
Device attachment after successful BLE write |
|