Managed BLE Operations

This page is the integration contract for partner apps that use the SDK’s managed BLE path. The same operation strings, payload JSON envelope, and result JSON apply on Android and iOS.

Use this path when you want the SDK to own BLE permission prompts, discovery UI, custody proof UI, connect/reconnect, and backend orchestration.

Discovery UI is how the passenger finds and selects their BagID tag. The phone scans for nearby tags over Bluetooth and shows a list so the user can tap the correct device (or the SDK connects automatically when you already know the tag’s uniqueDeviceId).

On the full-screen managed path, the SDK provides this UI—you do not need to call scanForEbtTags, connectEbt, or build your own device picker.

Custody proof UI is a short identity check before writing baggage to a tag. The BagID backend may require proof that the passenger owns the tag (typically booking reference / PNR and surname) so baggage data is not sent to the wrong device.

On the full-screen managed path, the SDK shows this form, submits proof to the API, and handles retries—you do not need CustodyProofCollector. Optional recordLocator and custodyProofSurname in the payload JSON can pre-fill the first attempt.

For headless orchestration without SDK UI (runBleOperation), see Tag Operations — headless mode.

Platform entry points

Platform Entry

Android

ManagedBleFlowActivity via BagIdAndroidHost.startManagedBleFlowForResult or Intent extras.

iOS

BagIdIosHost.presentManagedBleFlow from the BagIdSDKUI package.

Operations

All operation names are uppercase strings. The managed flow normalizes case internally.

Operation Purpose Payload data required?

TRANSFER

Write a baggage tag to the EBT and attach the device on the backend (lock lookup, custody when required, authorize, BLE write, attachment).

Yes — baggageId, journeyId, and either journey or displayTicket.

CLEAR

Clear the tag display and remove custody lock on the backend.

Optional — uniqueDeviceId for auto-select; otherwise the user picks from the scan list.

READ_BATTERY

Read battery level (0–100) over BLE.

Optional — uniqueDeviceId for auto-select.

READ_FIRMWARE

Read firmware version string over BLE.

Optional — uniqueDeviceId for auto-select.

SEND_NAMETAG

Write a personal image (nametag) to the tag display.

Yes — imageBase64 (see SEND_NAMETAG data fields).

HIDE_TICKET

Hide the bag-tag layer so the personal image is visible on the tag.

Optional — uniqueDeviceId for auto-select.

RESTORE_TICKET

Restore the bag-tag layer after a prior hide.

Optional — uniqueDeviceId for auto-select.

HIDE_TICKET and RESTORE_TICKET change what the tag displays; they do not clear backend attachment. Your app may track display preference in session state until firmware exposes a read for current display mode.

Launch parameters (all platforms)

Every full-screen launch supplies the same logical inputs:

Parameter Type Description

API base URL

String

BagID API endpoint for this environment (e.g. https://api.bagid.com). Issued by BagID during onboarding.

Source app key

String

X-Source-App-Key value for your partner app. Issued by BagID during onboarding.

Federated token

String

Federated JWT from your auth layer. Passed on launch so the managed surface can authenticate if initialize() cannot reuse or refresh an existing SDK session. Your app does not need to fetch a new token before every launch—provide a tokenProvider at configure time and ensure the launch value is valid when the SDK does call for federated login.

Operation

String

One of the operations in the table above.

Payload JSON

String

Envelope described in Payload envelope. Use "{}" when no data fields are needed (e.g. manual CLEAR).

Output: a single JSON string. Parse status (SUCCESS, FAILURE, or CANCELLED). See Result JSON.

Payload envelope

The payload is a JSON object with three top-level keys:

{
  "schemaVersion": 1,
  "selection": {
    "deviceId": "ble-peripheral-id",
    "autoSelectTimeoutMs": 12000
  },
  "data": {
    "uniqueDeviceId": "tag-guid-from-your-device-table",
    "baggageId": 12345,
    "journeyId": 67890
  }
}
Field Type Description

schemaVersion

int

Currently 1. Reserved for forward-compatible schema changes.

selection

object, optional

BLE peripheral hint for faster connect when the user already picked a row from a recent scan. deviceId is the platform BLE id (MAC-derived on Android, CoreBluetooth UUID on iOS). autoSelectTimeoutMs defaults to 12000, minimum 3000.

data

object

Operation-specific fields. uniqueDeviceId is the tag’s stable GUID (from your registered-device table or from BLE after first pairing). When set, the managed flow auto-selects that tag by scanning and verifying GUID (see Auto-select by GUID).

Unknown keys in the envelope are ignored.

TRANSFER data fields

Required:

  • baggageId (int) — from create baggage (Step 4) or your PSS.

  • journeyId (int) — from loadJourney.

  • Either journey (full journey object) or displayTicket (EBT ticket payload).

Optional:

  • recordLocator (string) — pre-fills custody proof.

  • custodyProofSurname (string) — pre-fills custody proof.

  • uniqueDeviceId (string) — auto-select registered tag by GUID.

journey vs displayTicket — choosing the right field

Field When to use Shape

journey (recommended for most partners)

You have passenger/flight data from DCS or your own PSS — typically obtained via loadJourney + createBaggageTag. The SDK builds the EBT ticket internally from this structured data.

Full Journey JSON object: journeyId, optional recordLocator, passengers[], flights[], baggage[]. See example below.

displayTicket

You already hold a pre-built EbtTicket byte representation (e.g. your own ticketing system serialises it directly). Bypasses SDK ticket construction.

EbtTicket JSON — an opaque SDK-internal format. Only use this path if BagID has specifically provided you with this payload; do not construct it manually.

In almost all partner integrations the journey path is the right choice. Supply displayTicket only when BagID explicitly advises it during onboarding.

The journey object mirrors Journey JSON: journeyId, optional recordLocator, passengers[], flights[], baggage[], optional baggageSlots[].

{
  "schemaVersion": 1,
  "data": {
    "baggageId": 1001,
    "journeyId": 42,
    "recordLocator": "ABC123",
    "journey": {
      "journeyId": 42,
      "recordLocator": "ABC123",
      "passengers": [
        { "passengerId": 1, "surname": "DOE", "givenName": "JOHN", "title": "MR" }
      ],
      "flights": [
        {
          "flightNumber": "WF123",
          "departureAirport": "BGO",
          "destinationAirport": "OSL",
          "departureAt": "2026-06-11T10:00:00Z"
        }
      ],
      "baggage": [
        {
          "baggageId": 1001,
          "baggageTagNumber": "0701913714",
          "airline": "WF",
          "destinationAirport": "OSL"
        }
      ]
    }
  }
}

SEND_NAMETAG data fields

Required:

  • imageBase64 — Base64-encoded image bytes. After decode, the image must be exactly 48000 bytes (NAMETAG_IMAGE_BYTES).

Optional:

  • nametagHeader — PGM header string. Default: "P5 240 400 16 " (note the trailing space; the firmware checks for it).

  • uniqueDeviceId — auto-select by GUID.

{
  "schemaVersion": 1,
  "data": {
    "uniqueDeviceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "imageBase64": "<48000-byte-image-as-base64>"
  }
}

CLEAR, READ_*, HIDE_TICKET, RESTORE_TICKET data

For manual device pick inside the SDK UI, use an empty data object or omit fields:

{ "schemaVersion": 1, "data": {} }

To skip the list and target a registered tag:

{
  "schemaVersion": 1,
  "data": {
    "uniqueDeviceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "autoSelectTimeoutMs": 15000
  }
}

autoSelectTimeoutMs in data applies when selection is omitted (same default and minimum as selection.autoSelectTimeoutMs).

Auto-select by GUID

When data.uniqueDeviceId is set:

  1. The managed flow scans for nearby BagID tags (up to the timeout).

  2. For each candidate, it connects briefly and compares the tag’s BLE-reported UUID to your GUID.

  3. On match, it keeps the connection and runs the operation (no redundant reconnect).

Use this for registered devices in your app’s device table. Store the GUID from the first successful transfer or from your backend device registry.

selection.deviceId is optional acceleration when you already know the BLE peripheral id from a recent scan.

Result JSON

Every managed flow finishes with one JSON string.

SUCCESS

Top-level fields: status = "SUCCESS", operation, data.

Operation data shape

TRANSFER

bleWriteSuccess, attachmentSuccess, pendingRetry, device (deviceId, optional serialNumber, batteryLevel, firmwareVersion)

CLEAR

success, unlocked, resolvedBleDeviceId

READ_BATTERY

batteryLevel, resolvedBleDeviceId

READ_FIRMWARE

firmware, resolvedBleDeviceId

SEND_NAMETAG

success, resolvedBleDeviceId

HIDE_TICKET, RESTORE_TICKET

success, resolvedBleDeviceId

pendingRetry on transfer means the BLE write succeeded but HTTP attachment did not—your app should offer retry or backend reconciliation (see FAQ).

Example transfer success:

{
  "status": "SUCCESS",
  "operation": "TRANSFER",
  "data": {
    "bleWriteSuccess": true,
    "attachmentSuccess": true,
    "pendingRetry": false,
    "device": {
      "deviceId": "AA:BB:CC:DD:EE:FF",
      "batteryLevel": 87,
      "firmwareVersion": "2.1.0"
    }
  }
}

FAILURE

{
  "status": "FAILURE",
  "code": "TRANSFER_FAILED",
  "message": "Human-readable detail"
}

Error codes

Code Typical cause

INVALID_OPERATION

Blank operation string.

MISSING_DEVICE_ID

Headless runBleOperation without data.uniqueDeviceId (see headless mode).

DEVICE_NOT_FOUND

Auto-select scan timed out or GUID did not match any nearby tag.

CONNECT_FAILED

BLE connect failed or connected state not ready.

INVALID_PAYLOAD

Missing or malformed data for the operation (e.g. TRANSFER without journey/displayTicket).

TRANSFER_FAILED

transferTag returned failure.

CLEAR_FAILED

clearTag returned failure.

READ_BATTERY_FAILED / READ_FIRMWARE_FAILED

BLE read failed.

SEND_NAMETAG_FAILED

Nametag write or backend authorize failed.

HIDE_TICKET_FAILED / RESTORE_TICKET_FAILED

Display-layer toggle failed over BLE.

UNKNOWN_OPERATION

Unsupported operation string.

CANCELLED

User dismissed the flow before completion:

{ "status": "CANCELLED", "message": "cancelled" }

Android: launch full-screen flow

The SDK merges com.bagid.sdk.ui.ManagedBleFlowActivity into your app manifest (see Installation).

import android.app.Activity
import com.bagid.sdk.host.BagIdAndroidHost

fun launchTransfer(activity: Activity, federatedToken: String, payloadJson: String) {
    BagIdAndroidHost.startManagedBleFlowForResult(
        activity = activity,
        apiBaseUrl = "https://api.bagid.com",
        sourceAppKey = "your-issued-client-key",
        federatedToken = federatedToken,
        operation = "TRANSFER",
        payloadJson = payloadJson,
    )
}

// In onActivityResult or Activity Result API callback:
val resultJson = BagIdAndroidHost.managedFlowResultJson(data)

BagIdRnFacade exposes the same methods for JVM hosts that prefer that entry name.

Intent extras (advanced)

Extra constant Value

ManagedBleFlowActivity.EXTRA_API_BASE_URL

API base URL

ManagedBleFlowActivity.EXTRA_SOURCE_APP_KEY

Source app key

ManagedBleFlowActivity.EXTRA_FEDERATED_TOKEN

Federated token

ManagedBleFlowActivity.EXTRA_OPERATION

Operation string

ManagedBleFlowActivity.EXTRA_PAYLOAD_JSON

Payload envelope JSON

ManagedBleFlowActivity.EXTRA_RESULT_JSON

Result JSON on the returned Intent

iOS: launch full-screen flow

Link both SPM products from the published package:

  • BagIdSDK — KMP core (BagIdSdk, models, BLE façade)

  • BagIdSDKUI — SwiftUI managed flow (BagIdIosHost)

import BagIdSDK
import BagIdSDKUI

// From a UIViewController (e.g. when user taps "Transfer to tag"):
BagIdIosHost.presentManagedBleFlow(
    from: self,
    apiBaseUrl: "https://api.bagid.com",
    sourceAppKey: "your-issued-client-key",
    federatedToken: federatedToken,
    operation: "TRANSFER",
    payloadJson: transferPayloadJson
) { resultJson in
    // Called on the main thread — parse JSON and update your UI
    print(resultJson)
}

The completion handler receives the same result contract as Android’s EXTRA_RESULT_JSON.

End-to-end flows for partner apps

Transfer workflow

Diagram
  1. Configure once at app startup with your issued sourceAppKey and a tokenProvider that can return a federated token from your IdP when the SDK asks (silently, without user interaction).

  2. Initialize before journey calls. The SDK reuses stored access and refresh tokens while they are still valid; it calls tokenProvider only when refresh is missing or has expired.

  3. Load journey and create baggage via BagIdSdk (or supply your own journey data in the TRANSFER payload).

  4. Build payload JSON with baggageId, journeyId, and journey/ticket data; add uniqueDeviceId if the tag is already registered.

  5. Launch the managed flow with the launch parameters in Launch parameters (all platforms). The SDK runs initialize() inside the surface and reuses the session when possible.

  6. Parse result JSON and drive your UI; handle pendingRetry on transfer.

Other operations

CLEAR, READ_BATTERY, READ_FIRMWARE, SEND_NAMETAG, HIDE_TICKET, and RESTORE_TICKET do not require journey load or baggage creation.

Diagram
  1. Configure and initialize as for transfer (session reuse and on-demand tokenProvider apply the same way).

  2. Build payload JSON with the fields required for the operation (see the operations table above). Include uniqueDeviceId when the tag is already registered.

  3. Launch the managed flow with the launch parameters in Launch parameters (all platforms).

  4. Parse result JSON and drive your UI.