iOS SDK
Installation
The minimum iOS version is iOS 12
.
The SDK can be integrated into iOS projects using several methods:
The latest SDK version can be found here
Swift Package Manager (SPM)
Go to your project → Package Dependencies tab
Click the + sign to add a new package
Add the following SPM dependency URL
CODEhttps://github.com/Utiq-tech/UTIQ-iOS-SDK
CocoaPods
If this is your first time adding a CocoaPod to your project, please follow the official CocoaPods installation tutorial.
Add the UTIQ pod to your project’s Podfile:
pod 'UTIQ'
, or specify an explicit version:pod 'UTIQ', 'LATEST_VERSION'
, ReplaceLATEST_VERSION
with the version number.Install the pod by running:
CODEcd {PATH_TO_YOUR_PROJECT} pod install
If you encounter an error stating that the latest version is not found, try updating the SDK pod using the following command pod update UTIQ
. Ensure that you are using the latest stable version.
Binary Framework
Head to this Repo.
Download this file
Utiq.xcframework
.Drag and drop it to your project.
Make sure you always have the latest stable update of the framework.
Initialize the SDK.
Please make sure that you have the latest stable version.
Initializing the SDK
Once added to your App, you can initialize the SDK in the AppDelegate
or the main class of your iOS application.
Please email us for a new token and a config file specific to your mobile App.
Basic initializing
Utiq.shared.initialize(sdkToken: SDK_TOKEN)
Initializing with custom options
UtiqOptions()
is an optional parameter for SDK initialization. You can initialize the SDK without it, but if you want to enable or disable specific options, configure them in UtiqOptions
and pass it to the initializer.
let fallbackConfigJson : String = "{ /* fallbackConfigJson */ }"
let options = UtiqOptions().enableLogging().setFallBackConfigJson(json: configJsonString)
Utiq.shared.initialize(sdkToken: "test123", options: options)
Currently, the only available options are enabling debugging and using the fallback configuration.
Check if the SDK is initialized
At some point, you will need to check if the SDK is initialized before doing anything else.
Utiq.shared.isInitialized()
didInitializeWithResult Closure
You should use this if you need to interact with the SDK very early in the App (for example, on the splash screen) and want to ensure the SDK is initialized before calling any of its functions. However, if you are using SDK functions in a flow that naturally involves multiple steps—such as a cart page in an e-commerce app—you likely won’t need this check, since the SDK will already be initialized by the time the user reaches that page or class. It is up to you to decide when this check is necessary and when it can be skipped.
Utiq.shared.didInitializeWithResult { [weak self] in
// Success Action
} failure: { [weak self] error in
// Failure Actio
}
Basic usage
Once the SDK is initialized, all the functions can be called by calling Utiq.shared.
to access all the SDK functions.
Integration
Fetch Utiq data
try? Utiq.shared.fetchIdConnectData(dataCallback: { [weak self] idcData in
}, errorCallback: { [weak self] error in
})
You can use a stub token to test the Utiq service if you don’t have an eligible SIM card from one of the supported Telcos.
Please email us to generate a new stub token for your App.
try? Utiq.shared.fetchIdConnectData(stubToken: STUB_TOKEN, dataCallback: { [weak self] idcData in
}, errorCallback: { [weak self] error in
})
App Tracking Transparency (ATT)
You must use the AppTrackingTransparency framework if you intend to use the AdTechPass and share it with other companies for cross-app and cross-website tracking purposes.
The AppTrackingTransparency framework displays a tracking authorization request to the user and provides the tracking authorization status.
For more details, see here.
Request ATT
Display the ATT pop-up. If the user accepts, proceed to the next step. If the user rejects, you can still continue, but only the MarTechPass will be returned.
Display the Utiq consent pop-up. If the user accepts, proceed to the next step; if the user rejects, stop the flow.
ATT Validity Check - User rejects the ATT
Apple allows users to change or withdraw ATT permissions from the App settings, even when the app is not open.
To do this, go to Settings > Privacy & Security > Tracking to see a list of Apps that have requested tracking permissions. You can toggle tracking on or off for a specific App.
In some cases, the ATT might be accepted but later withdrawn, so you should periodically check the ATT status (for example, on each App start). If the status has changed to rejected, you must also reject the Utiq consent and remove the AdTechPass if it was previously set.

Utiq Consent
Dedicated guidelines for Utiq Privacy Requirements and Consent Experience on Mobile Apps can be found at this page Consent Experience on Mobile App
Utiq services require explicit user consent before data can be fetched. Therefore, it is essential to display a consent prompt to the user. Without the user’s acceptance, Utiq data will not be retrieved.
The SDK does not provide a built-in consent dialog. You must implement a custom pop-up that aligns with the look and feel of your app.
This consent pop-up should be displayed before calling the fetchIdConnectData()
function (and only if the consent prompt has not already been shown). The user’s choice must then be passed to the SDK.
While the pop-up may match the look and feel of your app, the consent description must follow our guidelines, which can be found here.
Is consent accepted
This function returns a Boolean value that indicates the user’s consent status (accepted or rejected).
try? Utiq.shared.isConsentAccepted()
You can use this function to verify the user’s consent status before displaying a consent dialog.
Accept consent
try? Utiq.shared.acceptConsent()
Reject consent
try? Utiq.shared.rejectConsent()
This function can be used to reject the user’s consent if he changed his mind, or to reset the consent status.
Since this is an API call, you may want to handle what happens when the request succeeds or fails. For this, you can use the same function as above, but with success and failure closures.
try? Utiq.shared.rejectConsent(successCallback: { [weak self] in
}, errorCallback: { [weak self] error in
})
It is mandatory to notify the user that their consent has been successfully rejected if it was previously accepted, and the prompt must follow the text provided in the guidelines.
consenthub URL
If you need to access the ConsentHub URL from within the mobile app (for example, in a web view), you can do so by calling the function that returns the ConsentHub URL.
Utiq.shared.consentHubUrl()
During implementation, you may need to pass the stub token to the consentHubUrl(stubToken: STUB_TOKEN)
function if you are not using an eligible SIM.
User Eligibility
Before requesting user consent and fetching IdConnect data, it is recommended to first check whether the user is on a supported Telco by using the following function. This helps avoid unnecessary API calls and consent popups. In all cases, Utiq will not return any data if the Telco is not supported.
Utiq.shared.checkMNOEligibility {
// Success
} errorCallback: {
print("Error \($0)")
}
The eligibility check function also accepts a stub token so you can test freely without a supported SIM.
Clear cached data
Locally cached data and cookies can be cleared easily by using the following functions.
try? Utiq.shared.clearData()
This does not delete data and cookies from our back-end, but only from the mobile device’s local storage. To delete the user’s data, you should use the function that rejects the user’s consent.
Error handling
All functions with callbacks described in the previous sections include both success and failure callbacks (or closures). The failure callback returns a custom error that is specific to the function. You can use this error to take a defined action based on the error type.
All of the following errors are of type UtiqError
that inherits from NSError
.
Displaying errors from the SDK directly to the end user is not recommended. Instead, errors are returned so that developers can handle them appropriately and take the necessary actions based on the error code.
Error | Code | Description |
---|---|---|
| HTTP Codes | Represents an HTTP exception that includes an error message and a status code. |
| -1982 | Indicates that an invalid token was used to initialize the SDK. |
| -1983 | This error is thrown when attempting to initialize the SDK after it has already been initialized. |
| -1985 | Indicates that the SDK config was not found. |
| -1986 | This error is thrown when an empty token is passed to the SDK’s initializer. |
| -1987 | This error is thrown if you attempt to call any SDK function before the SDK has been initialized. |
| -1988 | This error is thrown when the SDK fails to fetch configuration data from the server and has no local configuration file to fall back on. This situation occurs if no fallback config file was provided during initialization or if there are no cached configurations from a previous session, preventing the SDK from initializing correctly. |
| -1959 | This error is thrown when the user’s previously saved consent has expired. |
| -1960 | This error is thrown when the |
| -1961 | This error is thrown when the consent version is invalid. |
| -1962 | When starting the Utiq identification flow, the user status must be one of the following: |
| -1963 | Unknown connection type |
| -1964 | Indicates that the |
| -1965 | Indicates that an invalid stub token was used to initialize the Utiq SDK. |
| -1966 | SIM card operator is not supported by Utiq. |
| -1967 | User has not provided consent or has not been prompted to accept or reject it. |
| -1968 | The user has deleted his data from ConsentHub. In this case, the SDK clears any cached data, and you should prompt the user to provide consent again, and finally fetch the IdConnect data to ensure the app has the latest information. |
| -1969 | Indicates that the SIM operator belongs to a use case that is unknown to Utiq. |
| -1970 | Indicates that the mobile MNO URL was not found. |
| -1971 | Indicates that the data value was not found. |
| -1972 | Indicates that the data domain was not found. |
| -1973 | Indicates a Telco with a token use case that is not Utiq-enabled. |
| -1974 | Indicates a SAML use-case Telco with an invalid session ID. |
| -1975 | Indicates a SAML use-case Telco with an invalid location URL. |
| -1976 | Indicates a SAML use-case Telco with an invalid authentication URL. |
| -1977 | Indicates that the |
| -1978 | Indicates that the ID Connect API host was not found. |
| -1979 | Indicates that ID Connect data was not found. |
| -1981 | Indicates an error that does not match any of the other errors listed in this table. |
| -1990 | Indicates an unknown error. |
Example CMP agnostic integration (Didomi)
The following is a sample integration. Feel free to organize the code in the way that best suits your project, but ensure you follow the general guidelines.
Create a class that encapsulates all required Didomi functions. This can be a singleton, or an interface with an implementation that you inject using any DI framework or service locator.
SWIFTimport OSLog import Didomi class DidomiSdk { private var didomiEventListener: EventListener? private var isUtiqVendorEnabled = false private var isUtiqPurposeEnabled = false private let didomi = Didomi.shared // static let shared = DidomiSdk() private init() { self.didomi.setLogLevel(minLevel: OSLogType.default.rawValue) } func initialize() { /* The SDK will automatically use the remote configuration hosted by Didomi and cache it locally. The cached version is refreshed every 60 minutes. Config file example { "app": { "name": "My App Name", "privacyPolicyURL": "http://www.website.com/privacy", "vendors": { "iab": { "all": true } }, "gdprAppliesGlobally": true, "gdprAppliesWhenUnknown": true } } */ let initializeParameters = DidomiInitializeParameters( apiKey: "5252a0d1-edad-4c60-8103-82aa49bd5432", localConfigurationPath: nil, remoteConfigurationURL: nil, providerID: nil, disableDidomiRemoteConfig: false, languageCode: nil, noticeID: "3RDba8E8" ) self.didomi.initialize(initializeParameters) self.didInitialize { let currentUserStatus = self.didomi.getCurrentUserStatus() self.isUtiqVendorEnabled = currentUserStatus.vendors.first(where: { $0.key.lowercased().contains("utiq") })?.value.enabled ?? false self.isUtiqPurposeEnabled = currentUserStatus.purposes.first(where: { $0.key.lowercased().contains("utiq") })?.value.enabled ?? false } self.onError { os_log("Error while initializing Didomi SDK", log: .default, type: .error, $0) } } func startIfNeeded(viewController: UIViewController, forceStart: Bool) { self.didInitialize { self.didomi.setupUI(containerController: viewController) if (forceStart) { self.didomi.forceShowNotice() } } } func startedBefore() -> Bool { !self.didomi.shouldUserStatusBeCollected() } func isUtiqEnabled() -> Bool { self.isUtiqVendorEnabled && self.isUtiqPurposeEnabled } func reset() { self.didomi.reset() } func resetUtiq() { self.didInitialize { let currentUserStatus = self.didomi.getCurrentUserStatus() /* If you want to hardcode the ID, The vendor ID can also be found in the Didomi's console in the Data Manager section, select the VENDORS tab, then search for the vendor you want to enable or disable, and the APP ID is the vendor ID. */ let utiqVendorId = currentUserStatus.vendors.keys.first { $0.lowercased().contains("utiq") } /* If you want to hardcode the ID, The purpose ID can also be found in the Didomi's console in the Data Manager section, select the Purposes tab, then search for the purpose you want to enable or disable and the APP ID is the purpose ID. */ let utiqPurposeId = currentUserStatus.purposes.keys.first { $0.lowercased().contains("utiq") } if(utiqVendorId != nil && utiqPurposeId != nil) { _ = self.didomi.openCurrentUserStatusTransaction() .disableVendor(utiqVendorId!) .disablePurpose(utiqPurposeId!) .commit() } } } func consentStatusDidChange(action: @escaping (_ enabled: Bool) -> ()) { /* Listen for changes on the user status linked to a specific vendor. We always need to listen for changes from Didomi as the user might open the screen from another place and reject his consent that he granted before, in this case we need to keep the synchronization between Didomi and Utiq */ self.didInitialize { if (self.didomiEventListener == nil) { self.didomiEventListener = self.createDidomiEventListener(action: action) } else { self.didomi.removeEventListener(listener: self.didomiEventListener!) } self.didomi.addEventListener(listener: self.didomiEventListener!) } } func onError(errorAction: @escaping(_ errorMessage: String) -> ()) { let eventListener = EventListener() eventListener.onError = { errorAction($0.localizedDescription) } self.didomi.addEventListener(listener: eventListener) } func didInitialize(action: @escaping () -> ()) { self.didomi.onReady(callback: action) } private func createDidomiEventListener(action: @escaping (_ enabled: Bool) -> ()) -> EventListener { let didomiEventListener = EventListener() didomiEventListener.onPreferencesClickVendorAgree = { _, vendorId in if (vendorId?.lowercased().contains("utiq") ?? false) { self.isUtiqVendorEnabled = true } } didomiEventListener.onPreferencesClickVendorDisagree = { _, vendorId in if (vendorId?.lowercased().contains("utiq") ?? false) { self.isUtiqVendorEnabled = false } } didomiEventListener.onPreferencesClickAgreeToAllVendors = { _ in self.isUtiqVendorEnabled = true } didomiEventListener.onPreferencesClickDisagreeToAllVendors = { _ in self.isUtiqVendorEnabled = false } // This will be called when the agree selector switch one of the options of the second layer is selected didomiEventListener.onPreferencesClickPurposeAgree = { _, purposeId in if (purposeId?.lowercased().contains("utiq") ?? false) { self.isUtiqPurposeEnabled = true } } // This will be called when the disagree selector switch one of the options of the second layer is selected didomiEventListener.onPreferencesClickPurposeDisagree = { _, purposeId in if (purposeId?.lowercased().contains("utiq") ?? false) { self.isUtiqPurposeEnabled = false } } // This will be called when the agree all selector switch one of the options of the second layer is selected didomiEventListener.onPreferencesClickAgreeToAllPurposes = { _ in self.isUtiqPurposeEnabled = true } // This will be called when the disagree all selector switch one of the options of the second layer is selected didomiEventListener.onPreferencesClickDisagreeToAllPurposes = { _ in self.isUtiqPurposeEnabled = false } didomiEventListener.onPreferencesClickSaveChoices = { event in action(self.isUtiqEnabled()) } // This will be called when Agree of the first layer is clicked didomiEventListener.onNoticeClickAgree = { _ in self.isUtiqVendorEnabled = true self.isUtiqPurposeEnabled = true action(self.isUtiqEnabled()) } // This will be called when Disagree of the first layer is clicked didomiEventListener.onNoticeClickDisagree = { _ in self.isUtiqVendorEnabled = false self.isUtiqPurposeEnabled = false action(self.isUtiqEnabled()) } return didomiEventListener } }
Create a function to start and observe Didomi's status
SWIFTprivate func startDidomiAndObserveConsentStatus(forceStart: Bool) { DidomiSdk.shared.startIfNeeded(viewController: self, forceStart: forceStart) DidomiSdk.shared.consentStatusDidChange { accepted in if (accepted) { try? Utiq.shared.clearData() // Only use stub if you are testing and do not have an eligible SIM card self.stubToken = YOUR_STUB_TOKEN_GOES_HERE try? Utiq.shared.acceptConsent() self.fetchUtiqIds() } else { self.rejectUtiqConsent() // Do any further actions if needed } } }
To synchronize Didomi's status with Utiq, you can do the following
SWIFTDidomiSdk.shared.didInitialize { if (DidomiSdk.shared.startedBefore()) { if (DidomiSdk.shared.isUtiqEnabled()) { // Only use stub if you are testing and do not have an eligible SIM card self.stubToken = YOUR_STUB_TOKEN_GOES_HERE try? Utiq.shared.acceptConsent() self.fetchUtiqIds() } else { // Didomi Consent was rejected before, Utiq will not start! } } else { self.startDidomiAndObserveConsentStatus(forceStart: false) } }
Then fetch Utiq IDs
SWIFT// Call the function that accepts the stub token if the token is not nil, else call the other function the does not accept the stub token if let token = self.stubToken { Utiq.shared.fetchIdConnectData(stubToken: token, dataCallback: { [weak self] idcData in /* Do Whatever you want with the IDs AttrPass => idcData.attrid AdTechPass => idcData.atid MarTechPass => idcData.mtid */ }, errorCallback: { let errorCode = ($0 as NSError).code let userOptedOutFromUtiqErrorCode = UserOptedOutFromUtiqException().code let userFrozenUtiqForOneYearErrorCode = UserFrozenUtiqForOneYearException().code if errorCode == userOptedOutFromUtiqErrorCode || errorCode == userFrozenUtiqForOneYearErrorCode { DidomiSdk.shared.resetUtiq() } // Handle the error }) } else { Utiq.shared.fetchIdConnectData(dataCallback: { [weak self] idcData in /* Do Whatever you want with the IDs AttrPass => idcData.attrid AdTechPass => idcData.atid MarTechPass => idcData.mtid */ }, errorCallback: { if $0 is UserOptedOutFromUtiqException || $0 is UserFrozenUtiqForOneYearException { DidomiSdk.shared.resetUtiq() } // Handle the error }) }
Synchronize Didomi when Utiq is manually rejected from within the App (for example, from the Manage Utiq page).
SWIFTprivate func rejectUtiqConsent(postAction: @escaping () -> () = {}) { Utiq.shared.rejectConsent { try? Utiq.shared.clearData() postAction() } errorCallback: { // Handle the error } }
To update Didomi’s status, call the following function.
SWIFTself.startDidomiAndObserveConsentStatus(forceStart: true)
Support and bug reporting
If you have suggestions, want to report a bug, or have any other inquiries, you can contact the Utiq Customer Success team at csm@utiq.com