Tag Operations & Device Types

This page is part of Protocol & internals. Partner integrators should follow Managed BLE Operations instead.

The SDK is built around a simple idea: an electronic baggage tag (EBT) offers a set of high-level operations — write a ticket to the display, blank the display, read the battery, read the firmware. Those operations are described in vendor-neutral, transport-agnostic terms. How a tag is reached (Bluetooth Low Energy today; potentially NFC, or others in the future) and which vendor built it are details the SDK layers on top — they do not change the core operations.

For partner apps the practical model is: you hold a type of device, and the SDK offers you the set of operations that the device supports. Everything you call goes through the single BagIdSdk façade; the layering below explains what that façade exposes and why, not extra types you have to wire up yourself.

The layered model

Diagram
Core tag operations (vendor-neutral)

The base capability contract for any electronic baggage tag, independent of vendor or transport: write a ticket to the display, blank the display, read the battery level, read the firmware version. This is the layer that makes the SDK vendor-neutral — the same operations are meaningful whether a tag is reached over BLE, NFC, or any future transport. In the codebase this contract is the IEbtTag interface.

BLE specialization

Tags reached over Bluetooth Low Energy add a device lifecycle on top of the core operations: scanning for nearby tags, connecting to one, and disconnecting. Transport that is not BLE would expose the core operations without this lifecycle layer. In the codebase this is the IEbtBleTag specialization of IEbtTag.

BagID over BLE (the default implementation)

The concrete, shipping implementation is BagID electronic baggage tags over BLE — currently the only implementation of the BLE specialization. On top of scan/connect/disconnect and the core operations, it adds BagID-specific extras: writing a personal nametag image, toggling the display layer (hide / restore), and a connection/error state stream. It also handles BagID GATT generation differences (v2.0 vs. v2.1, including the v2.1 security handshake) internally. In the codebase this is the IBagIdBleTag interface and its implementation.

Choosing a device type

A partner app does not pick a transport or a GATT stack in code. Instead, it works with the device identity the SDK reports from BLE discovery, and the SDK routes to the right internals:

  • EbtTagIdentity — the vendor and GATT generation the SDK inferred for a discovered tag (BagId for GATT v2.0, BagIdBleV21 for GATT v2.1, OtherVendor for non-BagID advertisements). Use it to label a device in your scan list; the SDK uses it to route internally.

  • EbtType — selects vendor-specific BLE extras that sit outside the core operations (e.g. EbtType.BagId for nametag). This is how you reach the BagID-implementation layer deliberately when you need an extra it provides.

Today every supported device is a BagID tag over BLE, so in practice the device type is fixed and all supported BagID hardware lines (BAGID_GO, BAGID_2FM, BAGID_2ST) use the same operations — generation differences are internal. The layering exists so additional vendors or transports can be added later without changing the operations your app already calls.

Where each operation lives on the façade

All of these are methods on BagIdSdk. They are grouped here by the layer they belong to, so the model above maps directly to the API you call.

Layer Façade methods Notes

Core tag operations (vendor-neutral)

writeEbtTicket, clearEbtDisplay, readEbtBattery, readEbtFirmware

The transport-agnostic operations every EBT exposes. Require an active connection.

BLE specialization (device lifecycle)

scanForEbtTags, notifyDiscoveredModel, connectEbt, disconnectEbt

Discovery and connection management for tags reached over BLE.

BagID implementation (vendor extras)

sendNametagTag / ebtBleExtras(EbtType.BagId), hideEbtTicketDisplay, restoreEbtTicketDisplay, ebtBleDeviceState

BagID-specific features beyond the core operations.

Orchestrated flows — full-screen managed

Android BagIdAndroidHost / iOS BagIdIosHost

Same JSON payload and result contract; SDK owns scan, custody UI, connect, and backend steps. See Managed BLE Operations.

Orchestrated flows — headless

runBleOperation

Same operation strings and JSON contract as full-screen managed, but no SDK UI — your app supplies data.uniqueDeviceId. See Headless managed operations (runBleOperation).

Orchestrated flows — programmatic

transferTag, clearTag, sendNametagTag

Typed Result APIs on an already-connected device; host owns scan/connect and custody UI. See Transfer & Clear operation details.

Headless managed operations (runBleOperation)

Headless mode runs the same managed operation pipeline as the full-screen flow (ManagedBleFlowRunner) but without ManagedBleFlowActivity or BagIdSDKUI. Use it when:

  • Your app already has BLE UX (or a device registry) and knows which tag to target.

  • You want the managed JSON in/out contract without handing the screen to the SDK.

  • You integrate from a non-coroutine host (e.g. React Native via BagIdRnFacade.runBleOperationBlocking).

Surface Who picks the tag Entry point

Full-screen managed

SDK scan list (or auto-select when uniqueDeviceId is in the payload)

BagIdAndroidHost / BagIdIosHostManaged BLE Operations

Headless (runBleOperation)

Host app — data.uniqueDeviceId is required in the payload

BagIdSdk.runBleOperation(operation, payloadJson) after configure + initialize

Programmatic

Host app — connect first, then call typed APIs

scanForEbtTagsconnectEbttransferTag / clearTagTransfer & Clear

The SDK scans BLE, connects to peripherals whose GUID matches uniqueDeviceId, runs the operation (lock lookup, custody when required, authorize, BLE I/O, backend attach/unlock as applicable), returns result JSON, and disconnects. Operation strings, payload envelope, and result shapes match Managed BLE Operations.

Steps

  1. Call BagIdSdk.configure(…​) and BagIdSdk.initialize() first.

  2. Build payload JSON with data.uniqueDeviceId set to the tag’s backend GUID (stable device identity — not the BLE deviceId / iOS peripheral UUID).

  3. Call BagIdSdk.runBleOperation(operation, payloadJson).

  4. Parse the returned JSON string (same status / code / message contract as the full-screen flow).

suspend fun BagIdSdk.runBleOperation(
    operation: String,
    payloadJson: String,
    custodyProofCollector: CustodyProofCollector? = null,
): String

// After configure + initialize:
val resultJson = BagIdSdk.runBleOperation(
    operation = "READ_BATTERY",
    payloadJson = """
        {
          "schemaVersion": 1,
          "data": { "uniqueDeviceId": "$tagGuid" }
        }
    """.trimIndent(),
)

On Android, BagIdRnFacade.runBleOperationBlocking wraps the same API for JVM hosts that cannot use coroutines (blocks the calling thread — invoke from a background thread).

Headless mode does not show custody UI. For TRANSFER or SEND_NAMETAG when custody proof is required, pass a CustodyProofCollector to runBleOperation, or use the full-screen managed path.

Custody helpers for headless flows

Wire these when your app renders custody proof UI while runBleOperation is suspended:

fun BagIdSdk.managedBleCustodyProofCollector(): CustodyProofCollector
val BagIdSdk.managedBleCustodyPrompt: StateFlow<ManagedBleCustodyHost.Prompt?>
fun BagIdSdk.submitManagedBleCustodyProof(recordLocator: String, surname: String)
fun BagIdSdk.cancelManagedBleCustodyProof()
fun BagIdSdk.resetManagedBleCustodyHost()

Headless-specific result codes

These codes from the managed result JSON contract apply most often to headless entry:

Code Typical cause

MISSING_DEVICE_ID

runBleOperation called without data.uniqueDeviceId in the payload.

DEVICE_NOT_FOUND

Targeted scan timed out or no nearby tag matched the GUID.

Partner apps should use the full-screen managed path. Reach for headless or programmatic APIs only when you own the surrounding BLE UX or need RN/JVM blocking entry points.

What’s next