CleverTap ZeroPii SDK provides a secure way to tokenize Personally Identifiable Information (PII) in your iOS applications. By replacing sensitive data with format-preserving tokens, you can minimize the exposure of sensitive information while maintaining data utility.
- Tokenization: Replace sensitive data with format-preserving tokens
- Batch Operations: Efficiently process multiple values in a single network call (up to 1,000 values)
- Encryption in Transit: Request and response payloads are encrypted while being transmitted over the network
- Bring-Your-Own Auth: No OAuth credentials in the SDK — your app supplies access tokens via the
AccessTokenProviderprotocol, keeping authentication logic under your control - Automatic Retry: Transient failures (network errors, 5xx, rate-limits) are retried automatically using a configurable
RetryPolicywith exponential back-off - ObjC / Swift / SwiftUI Compatible: All public APIs are annotated with
@objcand work across Objective-C, Swift, and SwiftUI apps
| iOS | 13.0+ |
| Swift | 5.0+ |
| Xcode | 15.0+ |
The SDK has no third-party dependencies.
In Xcode, go to File → Add Package Dependencies and enter the repository URL.
Or add it directly to your Package.swift:
dependencies: [
.package(url: "https://github.com/CleverTap/clevertap-ios-zeropii-sdk.git", from: "1.0.0")
],
targets: [
.target(
name: "YourTarget",
dependencies: ["CleverTapZeroPiiSDK"]
)
]Add to your Podfile:
pod 'CleverTapZeroPiiSDK', '~> 1.0'Then run:
pod installThe SDK does not handle authentication itself. You supply a fresh bearer token whenever the SDK needs one by implementing the AccessTokenProvider protocol.
Swift / SwiftUI
import CleverTapZeroPiiSDK
class MyTokenProvider: AccessTokenProvider {
func fetchToken(completion: @escaping (AccessTokenInfo?, Error?) -> Void) {
// Fetch a token from your auth system, then call completion exactly once.
MyAuthService.shared.getToken { token, expiresIn, error in
if let error = error {
completion(nil, error)
} else {
let info = AccessTokenInfo(token: token!, expiresInSeconds: expiresIn)
completion(info, nil)
}
}
}
}Objective-C
#import <CleverTapZeroPiiSDK/CleverTapZeroPiiSDK-Swift.h>
@interface MyTokenProvider : NSObject <AccessTokenProvider>
@end
@implementation MyTokenProvider
- (void)fetchTokenWithCompletion:(void (^)(AccessTokenInfo * _Nullable, NSError * _Nullable))completion {
[MyAuthService.shared getTokenWithCallback:^(NSString *token, NSInteger expiresIn, NSError *error) {
if (error) {
completion(nil, error);
} else {
AccessTokenInfo *info = [[AccessTokenInfo alloc] initWithToken:token expiresInSeconds:expiresIn];
completion(info, nil);
}
}];
}
@endImportant: You must call
completionexactly once — either with a validAccessTokenInfoon success, or with anErroron failure. Calling it zero times will cause the SDK to hang indefinitely; calling it more than once has undefined behaviour.
Initialize once, as early as possible — typically in AppDelegate or your SwiftUI App entry point.
Swift (AppDelegate)
import CleverTapZeroPiiSDK
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Minimal — default log level (.off) and default retry policy (1 retry, exponential back-off)
ZeroPiiSDK.initialize(
tokenProvider: MyTokenProvider(),
apiUrl: "https://your-api-base-url.com"
)
// With debug logging
ZeroPiiSDK.initialize(
tokenProvider: MyTokenProvider(),
apiUrl: "https://your-api-base-url.com",
logLevel: .debug
)
// With a custom retry policy
ZeroPiiSDK.initialize(
tokenProvider: MyTokenProvider(),
apiUrl: "https://your-api-base-url.com",
logLevel: .debug,
retryPolicy: MyRetryPolicy()
)
return true
}
}SwiftUI (App entry point)
import SwiftUI
import CleverTapZeroPiiSDK
@main
struct MyApp: App {
init() {
ZeroPiiSDK.initialize(
tokenProvider: MyTokenProvider(),
apiUrl: "https://your-api-base-url.com"
)
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}Objective-C (AppDelegate)
#import <CleverTapZeroPiiSDK/CleverTapZeroPiiSDK-Swift.h>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[ZeroPiiSDK initializeWithTokenProvider:[MyTokenProvider new]
apiUrl:@"https://your-api-base-url.com"];
return YES;
}
@endNote: You must call
initialize()before using any other SDK methods. CallinggetInstance()before initialization will cause a fatal error.
Swift
let sdk = ZeroPiiSDK.getInstance()
sdk.tokenizeString("[email protected]") { result in
if result.isSuccess {
let token = result.token! // the generated token
let wasExisting = result.exists // true if this value was already tokenized before
let isNew = result.newlyCreated // true if a new token was created
let dataType = result.dataType // "string" or "number"
print("Token: \(token)")
} else {
print("Tokenization failed: \(result.errorMessage ?? "unknown error")")
}
}
// Numeric types
sdk.tokenizeInt(42) { result in ... }
sdk.tokenizeLong(9876543210) { result in ... }
sdk.tokenizeFloat(3.14) { result in ... }
sdk.tokenizeDouble(2.71828) { result in ... }
sdk.tokenizeBool(true) { result in ... }SwiftUI
import SwiftUI
import CleverTapZeroPiiSDK
struct ContentView: View {
@State private var token: String = ""
var body: some View {
VStack {
Text(token.isEmpty ? "No token yet" : "Token: \(token)")
Button("Tokenize") {
ZeroPiiSDK.getInstance().tokenizeString("[email protected]") { result in
// Callback is delivered on the main thread — safe to update @State directly.
if result.isSuccess {
token = result.token ?? ""
}
}
}
}
}
}Objective-C
[[ZeroPiiSDK getInstance] tokenizeString:@"[email protected]"
callback:^(TokenizeResult *result) {
if (result.isSuccess) {
NSLog(@"Token: %@", result.token);
} else {
NSLog(@"Error: %@", result.errorMessage);
}
}];Process multiple values in a single API call. Maximum 1,000 values per batch.
Swift
let emails = ["[email protected]", "[email protected]", "[email protected]"]
ZeroPiiSDK.getInstance().batchTokenizeStringValues(emails) { result in
if result.isSuccess {
let summary = result.summary!
print("Processed : \(summary.processedCount)")
print("New tokens: \(summary.newlyCreatedCount)")
print("Existing : \(summary.existingCount)")
for item in result.results {
print("\(item.originalValue) → \(item.token)")
}
} else {
print("Batch failed: \(result.errorMessage ?? "unknown error")")
}
}
// Other supported types
ZeroPiiSDK.getInstance().batchTokenizeIntValues([1, 2, 3]) { result in ... }
ZeroPiiSDK.getInstance().batchTokenizeLongValues([10_000_000_000]) { result in ... }
ZeroPiiSDK.getInstance().batchTokenizeFloatValues([1.1, 2.2]) { result in ... }
ZeroPiiSDK.getInstance().batchTokenizeDoubleValues([1.111, 2.222]) { result in ... }
ZeroPiiSDK.getInstance().batchTokenizeBoolValues([true, false]) { result in ... }Objective-C
NSArray<NSString *> *emails = @[@"[email protected]", @"[email protected]"];
[[ZeroPiiSDK getInstance] batchTokenizeStringValues:emails
callback:^(BatchTokenizeResult *result) {
if (result.isSuccess) {
NSLog(@"Processed: %ld", result.summary.processedCount);
for (BatchTokenItem *item in result.results) {
NSLog(@"%@ → %@", item.originalValue, item.token);
}
} else {
NSLog(@"Batch error: %@", result.errorMessage);
}
}];Callbacks are always delivered on the main thread, so it is safe to update your UI directly from the callback without dispatching to DispatchQueue.main.
@discardableResult
public static func initialize(
tokenProvider: AccessTokenProvider,
apiUrl: String,
logLevel: ZeroPiiLogLevel,
retryPolicy: RetryPolicy
) -> ZeroPiiSDK| Parameter | Description |
|---|---|
tokenProvider |
Your implementation of AccessTokenProvider that supplies bearer tokens |
apiUrl |
Base URL for the tokenization API (include trailing slash) |
logLevel |
Logging verbosity |
retryPolicy |
Controls retry behaviour for failed requests |
@discardableResult
public static func initialize(
tokenProvider: AccessTokenProvider,
apiUrl: String,
logLevel: ZeroPiiLogLevel
) -> ZeroPiiSDKConvenience overload that uses the default RetryPolicy (1 retry, exponential back-off).
@discardableResult
public static func initialize(
tokenProvider: AccessTokenProvider,
apiUrl: String
) -> ZeroPiiSDKConvenience overload for Objective-C callers that uses logLevel: .off and the default RetryPolicy.
public static func getInstance() -> ZeroPiiSDKReturns the singleton. Crashes with a fatal error if called before initialize().
All methods follow the same pattern — execute on a background thread and deliver the callback on the main thread.
func tokenizeString(_ value: String, callback: @escaping (TokenizeResult) -> Void)
func tokenizeInt (_ value: Int, callback: @escaping (TokenizeResult) -> Void)
func tokenizeLong (_ value: Int64, callback: @escaping (TokenizeResult) -> Void)
func tokenizeFloat (_ value: Float, callback: @escaping (TokenizeResult) -> Void)
func tokenizeDouble(_ value: Double, callback: @escaping (TokenizeResult) -> Void)
func tokenizeBool (_ value: Bool, callback: @escaping (TokenizeResult) -> Void)func batchTokenizeStringValues(_ values: [String], callback: @escaping (BatchTokenizeResult) -> Void)
func batchTokenizeIntValues (_ values: [Int], callback: @escaping (BatchTokenizeResult) -> Void)
func batchTokenizeLongValues (_ values: [Int64], callback: @escaping (BatchTokenizeResult) -> Void)
func batchTokenizeFloatValues (_ values: [Float], callback: @escaping (BatchTokenizeResult) -> Void)
func batchTokenizeDoubleValues(_ values: [Double], callback: @escaping (BatchTokenizeResult) -> Void)
func batchTokenizeBoolValues (_ values: [Bool], callback: @escaping (BatchTokenizeResult) -> Void)Batch limits: Maximum 1,000 values per call. Exceeding this limit returns an error result immediately without making a network request. Empty arrays also return an error result immediately.
@objc public protocol AccessTokenProvider {
func fetchToken(completion: @escaping (AccessTokenInfo?, Error?) -> Void)
}Implement this protocol to bridge your app's authentication system to the SDK.
@objc public class AccessTokenInfo: NSObject {
public let token: String // The bearer access token
public let expiresInSeconds: Int // Seconds until expiry (from now)
}@objc public class TokenizeResult: NSObject {
public let isSuccess: Bool // true on success
public let token: String? // The generated token (nil on error)
public let exists: Bool // true if this value was already tokenized
public let newlyCreated: Bool // true if a new token was created
public let dataType: String? // "string" or "number" (nil on error)
public let errorMessage: String? // Human-readable error (nil on success)
public let httpStatusCode: NSNumber? // HTTP status code on server error, nil for SDK-level errors
}@objc public class BatchTokenizeResult: NSObject {
public let isSuccess: Bool // true on success
public let results: [BatchTokenItem] // Individual item results (empty on error)
public let summary: BatchTokenizeSummary?// Aggregate stats (nil on error)
public let errorMessage: String? // Human-readable error (nil on success)
public let httpStatusCode: NSNumber? // HTTP status code on server error, nil for SDK-level errors
}@objc public class BatchTokenItem: NSObject {
public let originalValue: String // The original input value
public let token: String // The generated token
public let exists: Bool // true if the token already existed
public let newlyCreated: Bool // true if a new token was created
public let dataType: String? // "string" or "number"
}@objc public class BatchTokenizeSummary: NSObject {
public let processedCount: Int // Total values processed
public let existingCount: Int // Values that already had a token
public let newlyCreatedCount: Int // New tokens created in this call
}@objc public protocol RetryPolicy: AnyObject {
/// Return `true` to retry the failed request.
///
/// - Parameters:
/// - attempt: Zero-based retry counter (0 = first retry after the initial failure).
/// - httpStatusCode: The HTTP status code from the server, or `nil` for network-level
/// errors (no HTTP response received). 401 responses are handled internally by the
/// SDK (token refresh) and are **never** passed to this method.
func shouldRetry(attempt: Int, httpStatusCode: NSNumber?) -> Bool
/// Milliseconds to wait before the next attempt.
///
/// - Parameter attempt: Zero-based retry counter, identical to the value passed to
/// `shouldRetry(attempt:httpStatusCode:)` for the same retry.
func retryDelayMs(attempt: Int) -> Int
}The SDK ships a built-in policy used by default — 1 retry with exponential back-off (2 s, 4 s, 8 s…). It retries on:
nilstatus code — network-level errors (no HTTP response)429Too Many Requests500Internal Server Error502Bad Gateway503Service Unavailable504Gateway Timeout
To customise retry behaviour, implement the protocol and pass your instance to initialize:
Swift
class MyRetryPolicy: NSObject, RetryPolicy {
func shouldRetry(attempt: Int, httpStatusCode: NSNumber?) -> Bool {
guard attempt < 3 else { return false }
guard let code = httpStatusCode else { return true } // network error → retry
return [429, 500, 502, 503, 504].contains(code.intValue)
}
func retryDelayMs(attempt: Int) -> Int {
return 1000 * (1 << (attempt + 1)) // 2 000 ms, 4 000 ms, 8 000 ms …
}
}
ZeroPiiSDK.initialize(
tokenProvider: MyTokenProvider(),
apiUrl: "https://your-api-base-url.com/",
logLevel: .debug,
retryPolicy: MyRetryPolicy()
)Objective-C
@interface MyRetryPolicy : NSObject <RetryPolicy>
@end
@implementation MyRetryPolicy
- (BOOL)shouldRetryWithAttempt:(NSInteger)attempt httpStatusCode:(NSNumber *)httpStatusCode {
if (attempt >= 3) return NO;
if (!httpStatusCode) return YES;
NSArray *retryable = @[@429, @500, @502, @503, @504];
return [retryable containsObject:httpStatusCode];
}
- (NSInteger)retryDelayMsWithAttempt:(NSInteger)attempt {
return 1000 * (1 << (attempt + 1));
}
@end
// Usage:
[ZeroPiiSDK initializeWithTokenProvider:[MyTokenProvider new]
apiUrl:@"https://your-api-base-url.com/"
logLevel:ZeroPiiLogLevelDebug
retryPolicy:[MyRetryPolicy new]];Note on 401 Unauthorized: The SDK handles 401 responses internally — it refreshes the access token via your
AccessTokenProviderand retries once immediately. This is never delegated to yourRetryPolicy.
@objc public enum ZeroPiiLogLevel: Int {
case off = 0 // No logging (default)
case error = 1 // Errors only
case info = 2 // Errors + informational messages
case debug = 3 // Errors + info + debug trace (use during development only)
}Logs are written via os_log and are visible in Xcode's Console and the macOS Console app under the CleverTap subsystem and CT-ZeroPiiSDK category.
All public classes, protocols, and enums are annotated with @objc and subclass NSObject, making them fully accessible from Objective-C.
Import the generated Swift header:
// CocoaPods (use_frameworks!) or SPM — module import (recommended)
@import CleverTapZeroPiiSDK;
// CocoaPods or manual — explicit header import
#import <CleverTapZeroPiiSDK/CleverTapZeroPiiSDK-Swift.h>Checking the result in ObjC — since Swift enums with associated values cannot be used in ObjC, all results are returned as NSObject subclasses with boolean flags:
[[ZeroPiiSDK getInstance] tokenizeString:@"[email protected]"
callback:^(TokenizeResult *result) {
if (result.isSuccess) {
NSLog(@"Token: %@, newly created: %d", result.token, result.newlyCreated);
} else {
NSLog(@"Failed: %@", result.errorMessage);
}
}];Two sample applications are included in the repository:
- Swift / SwiftUI —
ZeroPIISample/ZeroPIISample.xcworkspace - Objective-C / UIKit —
ZeroPIIObjCSample/ZeroPIIObjCSample.xcworkspace
Open the .xcworkspace (not the .xcodeproj) in Xcode to explore them.