iOS SDK Quickstart

The iOS Headless SDK provides a headless consent management API for iOS apps. The module is imported as TranscendHeadless, and all API calls will go through the imported TranscendAPI.

Requirements: iOS 15.0+, Swift 5.9+

Add to your `Podfile`:

pod 'TranscendHeadless'

Then run `pod install`.

In XCode, import your package using this URL:

https://github.com/transcend-io/consent-manager-headless-ios-library-spm

and then import TranscendHeadless in your code.

Create a TranscendConfig and call TranscendAPI.initialize(config:). Register a ready callback **before** calling initialize, and it will fire once the SDK has loaded settings and (optionally) completed its initial sync.

import TranscendHeadless

let config = try TranscendConfig(
    bundleId: "YOUR_BUNDLE_ID", // Required — Transcend bundle ID
    mobileAppId: "com.example.myapp", // Required — your app's mobile identifier pulled from https://app.transcend.io/consent-manager/native/applications?pageSize=10
    isTest: false, // Whether to use the test or production deployment
    cdnUrl: "https://transcend-cdn.com", // Optional — defaults to Transcend's CDN
    token: "AUTH_TOKEN", // Optional — auth token for backend sync. Configured using your preference store signing and encryption keys https://docs.transcend.io/docs/articles/preference-management/storing-consent-preferences#generate-a-user-token-on-your-backend-server
    settingsOverrides: [ // Optional — override remote settings at init
        "country": "EU",
        "regime": "GDPR"
    ]
)
TranscendAPI.ready { event in
    if case .ready(let success, let error) = event {
        if success {
            // SDK is ready — safe to call consent APIs
        } else {
            print("Init failed:", error?.localizedDescription ?? "unknown")
        }
    }
}
try TranscendAPI.initialize(config: config)

Use TranscendAPI.asyncInit(config:) when you prefer non-throwing initialization. Errors are delivered via the ready callback instead of being thrown.

TranscendAPI.asyncInit(config: config)

You can check for readiness in one of two ways: registering a variable or function that is then mutated or invoked on .ready callback success, or through polling. Ready callback registration is shown in the Initialization section, and polling can be with a periodic interval check calling the following function:

TranscendAPI.isReady() // returns Bool

Note: most API methods require the SDK to be ready and will throw TranscendError.notReady otherwise.

If your initialization has encountered an error and needs re-initialization, you can tear down the API with .destroy() and re-initialize. This should not be necessary during typical app usage, but may be helpful for debugging purposes.

try TranscendAPI.destroy()

Register listeners with addEventListener(type:callback:). Each call returns a ListenerHandle — store it and pass it to removeEventListener(_:) when done.

let consentHandle = TranscendAPI.addEventListener(type: .CONSENT_CHANGE) { event in
    if case .consentChange(let oldConsent, let newConsent, let changes) = event {
        print("Consent changed:", newConsent.purposes)
    }
}

let syncHandle = TranscendAPI.addEventListener(type: .SYNC) { event in
    if case .sync(let oldConsent, let newConsent, _) = event {
        print("Sync completed")
    }
}

// When no longer needed:
TranscendAPI.removeEventListener(consentHandle)
TranscendAPI.removeEventListener(syncHandle)
Listener TypeConsentEvent typeWhen it fires
.CONSENT_CHANGE.consentChangeLocal consent state changes (e.g. after setConsent).
.SYNC.syncBackend sync completes and consent may have been updated.
*(via ready)*.ready(success:error:)SDK initialization succeeds or fails.
let consent = try TranscendAPI.getConsent()
// consent.purposes       — [String: PurposeValue] (.True / .False / .Auto)
// consent.confirmed      — Bool
// consent.prompted         — Bool
// consent.timestamp        — ISO 8601 string
// consent.booleanPurposes() — [String: Bool] convenience helper, coalesces .Auto to true

Pass a map of purpose names to boolean values. Syncs to the backend by default.

let success = try TranscendAPI.setConsent(
    purposes: [
        "Analytics": true,
        "Functional": true,
        "Advertising": false
    ],
    autoSync: true,       // Sync to backend after setting (default: true)
    confirmed: true,      // Mark consent as confirmed (default: true)
    timestamp: nil,       // ISO timestamp; defaults to now
    updated: true,        // Mark consent as updated (default: true)
    metadata: .unchanged, // ConsentMetadata; see ConsentMetadata enum
    metadataTimestamp: nil
)

When autoSync is true, the SDK pushes consent to the backend using the token from TranscendConfig. Listen for .SYNC events to react to remote updates.

If you need to test specific regimes, turn on/off sync, or force on any specific configuration values, the TranscendConfig object allows you to pass those through for a runtime override.

Pass overrides in TranscendConfig.settingsOverrides:

let config = try TranscendConfig(
    bundleId: "YOUR_BUNDLE_ID",
    mobileAppId: "com.example.myapp",
    settingsOverrides: [
        "country": "DE",
        "countryRegion": "EU",
        "regime": "GDPR"
    ]
)

Some common override keys include: "partition", "country", "countryRegion", "regime", "defaultRegime".

**Backend sync requires an auth token.** Pass token when creating TranscendConfig at init — without it, sync is skipped and consent is not pushed to or pulled from the backend. You can generate an auth token by following the instructions here.

let config = try TranscendConfig(
    bundleId: "YOUR_BUNDLE_ID",
    mobileAppId: "com.example.myapp",
    token: "AUTH_TOKEN"  // Required for sync
)


The SDK syncs consent with the Transcend backend automatically in two cases:

  1. During initialization, using the token from the TranscendConfig.
  2. After setConsent, when autoSync is true (the default), using the same TranscendConfig token we initialized with.

There is no separate public sync() method. To trigger a sync, call setConsent with autoSync: true and ensure a valid token was provided at init.

To listen for sync completion:

TranscendAPI.addEventListener(type: .SYNC) { event in
    if case .sync(_, let newConsent, _) = event {
        // Backend sync finished; newConsent reflects any remote changes
    }
}

Sync is also skipped when SYNC_DISABLED is true in the loaded settings, or explicitly disabled using that field in an override.

Use these APIs to determine whether a third-party SDK (identified by a service ID) is allowed to run given the current consent state.

// Async/await
let status = try await TranscendAPI.getSdkConsentStatus(
    serviceId: "analytics_sdk",
    cacheTTL: nil  // Optional cache TTL in milliseconds
)

// Callback-based
TranscendAPI.getSdkConsentStatus(serviceId: "analytics_sdk") { result in
    switch result {
    case .success(let status):
        switch status {
        case .ALLOW:       /* SDK may run */
        case .BLOCK:       /* User denied required purpose(s) */
        case .NO_SDK_FOUND: /* Service ID not in purpose map */
        case .INTERNAL_ERROR:
            break
        }
    case .failure(let error):
        print(error.localizedDescription)
    }
}

Pass specific service IDs, or `nil`/empty to evaluate all known SDKs:

// Specific IDs
let statuses = try await TranscendAPI.batchGetSDKConsentStatuses(
    serviceIds: ["analytics_sdk", "ads_sdk"],
    cacheTTL: nil
)
// Returns [String: ConsentStatus], e.g. ["analytics_sdk": .ALLOW, "ads_sdk": .BLOCK]

// All SDKs in the purpose map
let allStatuses = try await TranscendAPI.batchGetSDKConsentStatuses(
    serviceIds: nil,
    cacheTTL: nil
)

Callback variant:

TranscendAPI.batchGetSDKConsentStatuses(serviceIds: ["analytics_sdk"]) { result in
    if case .success(let map) = result {
        for (serviceId, status) in map {
            print("\(serviceId): \(status)")
        }
    }
}
StatusMeaning
.ALLOWAll required purposes for this SDK are granted (or SDK is Essential).
.BLOCKAt least one required purpose was explicitly denied.
.NO_SDK_FOUNDThe service ID is not in the SDK purpose map.
.INTERNAL_ERRORReturned if consent resolution encountered an error while reconciling the service ID's purposes.

The SDK purpose map is loaded from settings and cached. Pass `cacheTTL` (milliseconds) to control how long the cache is reused before re-reading from settings.

// Active privacy regimes (e.g. "GDPR")
let regimes = try TranscendAPI.getRegimes()

// Purposes applicable under the current regime
let purposes = try TranscendAPI.getRegimePurposes()

// Purpose display metadata (name, description)
let details = try await TranscendAPI.getPurposeDetails()

// Raw loaded settings
let settings = try await TranscendAPI.getCurrentSettings()

All throwing APIs can produce `TranscendError`:

ErrorCause
.notInitialized`initialize` has not been called.
.notReadySDK is still loading; wait for the ready callback.
.alreadyInitializedinitialize called more than once without destroy().
.invalidConfiguration(String)Invalid config (e.g. empty bundleId).
.initFailed(String)Settings fetch or initialization failed.