Audience: engineers maintaining a cross-platform AppsFlyer plugin (Flutter, React Native, Unity, Cordova, Capacitor, Xamarin, …) who are replacing an existing per-platform native binding with AppsFlyerRPC.
Purpose: every RPC method is a thin wrapper over one specific AppsFlyerLib API (occasionally AppsFlyerCrossPromotionHelper / AppsFlyerShareInviteHelper). If your plugin's iOS native layer currently calls AppsFlyerLib.shared().foo directly, this document tells you exactly which RPC JSON call replaces that call site, so you can delete the native binding code entirely and go through the RPC bridge instead.
This is a companion to README.md, which has full JSON request/response examples, parameter tables, and the plugin integration guide. This document adds the one thing the README doesn't spell out: which native SDK symbol each RPC method resolves to, verified directly against the adapter source (AppsFlyerSDKAdapter.swift), not against documentation that can drift from code.
Verification note: Every row below was traced through
AFRPCParser.swift→AFRPCHandlerRouter.swift→ the domainHandler→ the domainSDKprotocol →AppsFlyerSDKAdapter.swift(the single, literal pass-through toAppsFlyerLib.shared()). File references are given per section so you can re-verify after future changes.
Before (per-plugin native binding):
Plugin native layer → AppsFlyerLib.shared().customerUserID = "user-123"
After (via RPC):
Plugin native layer → AppsFlyerRPCBridge.executeJson(
{"method":"setCustomerUserId","params":{"customerId":"user-123"}}
)
→ AppsFlyerRPC internally calls AppsFlyerLib.shared().customerUserID = "user-123"
Your plugin's native bridge shrinks to two responsibilities: serialize a JSON request / deserialize the JSON response, and forward RPC events to your JS/Dart/C# event system. All SDK method names, property names, and validation logic move into AppsFlyerRPC — you no longer need per-platform native code that calls AppsFlyerLib directly.
initis NOT a working alias forinitialize.README.mddocumentsinitas a "legacy alias," but the RPC dispatch table (AFRPCParser.swift,methodParsers) only registersAFRPCInitRequest.methodName = "initialize"(AppsFlyerRPC/AppsFlyerRPC/Core/AFRPCTypedRequests.swift:35). There is no"init"entry anywhere in that dictionary. Sending{"method":"init", ...}today throwsunknownMethod("init")(404). Useinitialize. This is a real bug in the current README that should be tracked and fixed separately — do not build plugin code againstinit.docs/schemas/rpc-request-envelope.schema.jsonis stale relative to the implementation: itsmethodenum lists"init"(not"initialize"), still lists the removedvalidateAndLogInAppPurchaseLegacy, and is missing ~15 methods that exist in code today (registerSessionReadyListener,unregisterSessionReadyListener,isSessionReady,handleLaunchOptions,setDisableIDFVCollection,setCurrentDeviceLanguage,logEvent, and others). Do not validate outgoing requests against this schema file as-is — treat the Swift source (AFRPCTypedRequests.swift/AFRPCParser.swift) as the source of truth for supported methods until the schema is refreshed.
Source: Handler/Handlers/AFRPCCoreHandler.swift, Handler/Utilities/AppsFlyerSDKAdapter.swift (Core extension).
| RPC method | Native SDK API it wraps | Notes |
|---|---|---|
initialize |
AppsFlyerLib.shared().initialize(devKey:appId:) |
Only way to set appsFlyerDevKey/appleAppID — both are read-only properties in SDK7. Direct native setters no longer work; this must be your plugin's entry point. |
start |
AppsFlyerLib.shared().start(completionHandler:) |
With awaitResponse:true, RPC races the SDK completion against a timeout (SDKTimeoutHelper); without it, fire-and-forget. |
logEvent |
AppsFlyerLib.shared().logEvent(name:values:completionHandler:) (await) or .logEvent(_:withValues:) (fire-and-forget) |
Choice of underlying overload depends on awaitResponse. |
setPluginInfo |
AppsFlyerLib.shared().setPluginInfo(plugin:version:additionalParams:) |
plugin string is translated to the SDK's Plugin enum via a fixed mapping table (see Supported plugin values in the README) before being passed to the SDK. Call before start. |
registerSessionReadyListener |
AppsFlyerLib.shared().registerSessionReadyListener(_:) |
SDK7 API. Fires the onSessionReady RPC event when the SDK calls the listener closure. |
unregisterSessionReadyListener |
AppsFlyerLib.shared().unregisterSessionReadyListener() |
SDK7 API. |
isSessionReady |
AppsFlyerLib.shared().isSessionReady() |
SDK7 getter. Returned as result.data.isSessionReady. |
handleLaunchOptions |
AppsFlyerLib.shared().handleLaunchOptions(_:) |
Forward the host app's didFinishLaunchingWithOptions dictionary here instead of calling the SDK directly from your AppDelegate-equivalent. |
Source: Handler/Handlers/AFRPCSimpleConfigHandler.swift, AppsFlyerSDKAdapter.swift (Simple config extension). Every row here is a direct property get/set on AppsFlyerLib.shared() — no async, no completion handler.
| RPC method | Native SDK property |
|---|---|
isDebug |
AppsFlyerLib.shared().isDebug |
setCustomerUserId |
AppsFlyerLib.shared().customerUserID |
setAdditionalData |
AppsFlyerLib.shared().customData |
setCurrencyCode |
AppsFlyerLib.shared().currencyCode |
setDisableAdvertisingIdentifiers |
AppsFlyerLib.shared().disableAdvertisingIdentifier |
setDisableSKAdNetwork |
AppsFlyerLib.shared().disableSKAdNetwork |
setShouldCollectDeviceName |
AppsFlyerLib.shared().shouldCollectDeviceName |
setAppInviteOneLink |
AppsFlyerLib.shared().appInviteOneLinkID |
anonymizeUser |
AppsFlyerLib.shared().anonymizeUser |
setDisableCollectASA |
AppsFlyerLib.shared().disableCollectASA |
setDisableAppleAdsAttribution |
AppsFlyerLib.shared().disableAppleAdsAttribution |
setUseReceiptValidationSandbox |
AppsFlyerLib.shared().useReceiptValidationSandbox |
setUseUninstallSandbox |
AppsFlyerLib.shared().useUninstallSandbox |
setPhoneNumber |
AppsFlyerLib.shared().phoneNumber (setter) |
setDisableIDFVCollection |
AppsFlyerLib.shared().disableIDFVCollection |
setCurrentDeviceLanguage |
AppsFlyerLib.shared().currentDeviceLanguage |
stop |
AppsFlyerLib.shared().isStopped |
Source: Handler/Handlers/AFRPCComplexConfigHandler.swift, AppsFlyerSDKAdapter.swift (Complex config extension).
| RPC method | Native SDK API |
|---|---|
setResolveDeepLinkURLs |
AppsFlyerLib.shared().resolveDeepLinkURLs (property) |
setOneLinkCustomDomain |
AppsFlyerLib.shared().oneLinkCustomDomains (property) |
setHost |
AppsFlyerLib.shared().setHost(_:hostName:) — SDK7 breaking change: RPC params are hostPrefixName, hostName (renamed/reordered vs. the old host/hostPrefix naming). |
setMinTimeBetweenSessions |
AppsFlyerLib.shared().minTimeBetweenSessions (property, UInt seconds) |
setDeepLinkTimeout |
AppsFlyerLib.shared().deepLinkTimeout (property, UInt ms) |
setInstallId |
AppsFlyerLib.shared().setInstallId(_:) |
setSharingFilterForPartners |
AppsFlyerLib.shared().setSharingFilterForPartners(_:) |
setPartnerData |
AppsFlyerLib.shared().setPartnerData(partnerId:data:) |
Source: Handler/Handlers/AFRPCPrivacyHandler.swift, AppsFlyerSDKAdapter.swift (Privacy extension).
| RPC method | Native SDK API |
|---|---|
setConsentData |
AppsFlyerLib.shared().setConsentData(_:), constructing an AppsFlyerConsent(isUserSubjectToGDPR:hasConsentForDataUsage:hasConsentForAdsPersonalization:hasConsentForAdStorage:) from the RPC params (bridging Bool? → NSNumber?) |
enableTCFDataCollection |
AppsFlyerLib.shared().enableTCFDataCollection(_:) |
Source: Handler/Handlers/AFRPCDeepLinkHandler.swift, AppsFlyerSDKAdapter.swift (Deep link extension).
| RPC method | Native SDK API | Notes |
|---|---|---|
appendParametersToDeepLinkingURL |
AppsFlyerLib.shared().appendParametersToDeepLinkingURL(contains:parameters:) |
|
addPushNotificationDeepLinkPath |
AppsFlyerLib.shared().addPushNotificationDeepLinkPath(_:) |
|
enableFacebookDeferredApplinks |
AppsFlyerLib.shared().enableFacebookDeferredApplinks(with:) |
RPC resolves the FBSDKAppLinkUtility class via NSClassFromString when enable:true, passes nil when false. |
setFacebookDeferredAppLink |
AppsFlyerLib.shared().facebookDeferredAppLink (property) |
RPC validates the URL scheme (rejects dangerous schemes) before assigning. |
handleOpenURL / handleOpenUrl |
AppsFlyerLib.shared().handleOpen(_:options:) |
Both casing variants map to the same SDK call, for plugin naming-convention compatibility. Dispatched on MainActor. |
continueUserActivity |
AppsFlyerLib.shared().continue(_:restorationHandler:) |
RPC wraps the url param in an NSUserActivity (default type NSUserActivityTypeBrowsingWeb unless activityType given) before calling. Dispatched on MainActor. |
performOnAppAttributionWithURL |
AppsFlyerLib.shared().performOnAppAttribution(with:) |
Dispatched on MainActor. |
Source: Handler/Handlers/AFRPCMonetizationHandler.swift, AppsFlyerSDKAdapter.swift (Monetization extension). Note: two of these go through static helper classes, not AppsFlyerLib.shared() directly.
| RPC method | Native SDK API | Notes |
|---|---|---|
validateAndLogInAppPurchase |
AppsFlyerLib.shared().validateAndLogInAppPurchase(purchaseDetails:purchaseAdditionalDetails:completion:) |
Builds an AFSDKPurchaseDetails from productId + transactionId + purchaseType ("subscription" / "oneTimePurchase"). This is the SDK7 V2 path — the legacy 6-param validateAndLogInAppPurchaseLegacy API has no RPC exposure (see What's new / removed below). |
logAdRevenue |
AppsFlyerLib.shared().logAdRevenue(_:additionalParameters:) |
Builds an AFAdRevenueData from monetizationNetwork/mediationNetwork/currencyIso4217Code/revenue. |
logCrossPromoteImpression |
AppsFlyerCrossPromotionHelper.logCrossPromoteImpression(_:campaign:userParams:) |
Static helper class, not AppsFlyerLib.shared(). |
logAndOpenStore |
AppsFlyerCrossPromotionHelper.logAndOpenStore(_:campaign:userParams:openStore:) |
Static helper. RPC surfaces the resulting URL as result.data.clickURL; the plugin is responsible for opening it. |
generateInviteLink |
AppsFlyerShareInviteHelper.generateInviteLink(linkGenerator:completionHandler:) |
Static helper. RPC params map to AppsFlyerLinkGenerator setters (setChannel, setCampaign, setReferrerName, setReferrerImageUrl, setReferrerCustomerId, setBaseDeepLink, .brandDomain, addUserParams). SDK7 rename — was generateInviteUrl. |
logInvite |
AppsFlyerShareInviteHelper.logInvite(_:eventParameters:) |
Static helper. |
Source: Handler/Handlers/AFRPCNotificationHandler.swift, AppsFlyerSDKAdapter.swift (Notification extension).
| RPC method | Native SDK API |
|---|---|
handlePushNotification |
AppsFlyerLib.shared().handlePushNotification(_:) |
registerUninstall |
AppsFlyerLib.shared().registerUninstall(_:) (expects Data; RPC hex-decodes the JSON string param) |
Source: Handler/Handlers/AFRPCDataHandler.swift, AppsFlyerSDKAdapter.swift (Data extension).
| RPC method | Native SDK API |
|---|---|
getAppsFlyerUID |
AppsFlyerLib.shared().getAppsFlyerUID() |
getSdkVersion |
AppsFlyerLib.shared().getSdkVersion() |
setUserEmails |
AppsFlyerLib.shared().setUserEmails(_:with:) |
logLocation |
AppsFlyerLib.shared().logLocation(longitude:latitude:) |
Source: Handler/Handlers/AFRPCListenerHandler.swift, Handler/Delegates/AFRPCRequestHandlerDelegates.swift, AppsFlyerSDKAdapter.swift (Listener extension). These two RPC methods don't call an SDK method — they assign the RPC layer itself as the SDK delegate, then forward delegate callbacks back to the plugin as async RPC events (not request/response).
| RPC method | Native SDK API it wraps | What happens after registration |
|---|---|---|
registerConversionListener |
AppsFlyerLib.shared().delegate = <self> (sets AppsFlyerLibDelegate) |
AppsFlyerLibDelegate.onConversionDataSuccess(_:) → RPC event onConversionDataSuccess; .onConversionDataFail(_:) → RPC event onConversionDataFail. |
registerDeeplinkListener |
AppsFlyerLib.shared().deepLinkDelegate = <self> (sets AppsFlyerDeepLinkDelegate) |
AppsFlyerDeepLinkDelegate.didResolveDeepLink(_:) → RPC event onDeepLinkReceived, with data.status ∈ "found" / "failure" / "notFound". |
registerSessionReadyListener / unregisterSessionReadyListener / isSessionReady are functionally in this same "listener" family but are implemented in the Core handler — see the Core table above.
If your plugin's existing native code was written against a pre-SDK7 AppsFlyerLib, these are the RPC-relevant behavior changes to account for when you replace the direct binding with RPC calls — RPC does not paper over them:
| Change | Detail |
|---|---|
appsFlyerDevKey / appleAppID are read-only in SDK7 |
You can no longer set them via direct property assignment. initialize is the only entry point exposed through RPC (and the only one in the underlying SDK). |
| Session-ready gating is new | start must be issued from inside the callback delivered by registerSessionReadyListener (SDK7 concept, has no pre-SDK7 equivalent). Do consent/ATT work in that callback before calling start. |
setHost params renamed/reordered |
Old: host (name) with prefix second. New (as exposed by RPC): hostPrefixName then hostName. |
validateAndLogInAppPurchaseLegacy removed |
The old 6-param IAP validation path (no purchaseType) has no RPC method and no SDK7 replacement path other than V2. Use validateAndLogInAppPurchase. |
generateInviteUrl renamed to generateInviteLink |
Same params; completion now also carries an error field (SDK7 callback signature changed to include Error?). |
onAppOpenAttribution / onAppOpenAttributionFailure retired |
These delegate callbacks (and their would-be RPC events) don't exist in SDK7. All retargeting/deep-link outcomes — including what used to be "on-app attribution" — now arrive as a single onDeepLinkReceived event; branch on data.status. |
handleLaunchOptions is new |
No pre-SDK7 direct equivalent exposed through RPC; forward the host app's launch options dictionary through this RPC call instead of any ad hoc native code you had. |
Examples below follow the exact conventions of the existing suite in
AppsFlyerRPC/AppsFlyerRPCTests/: XCTest, @testable import AppsFlyerRPC, one hand-written mock
per domain SDK protocol (AFRPCConsolidatedHandlerTests.swift), and shared assertion helpers
(RPCTestHelpers.swift). Every SDK call is mocked — nothing hits AppsFlyerLib or the network.
Use these as templates for testing your own plugin bridge's integration with AppsFlyerRPC, or
for extending the RPC module's own test suite.
Each domain handler (AFRPCCoreHandler, AFRPCSimpleConfigHandler, …) depends on a narrow
protocol (AFRPCCoreSDK, AFRPCSimpleConfigSDK, …), never on AppsFlyerLib directly. Tests
conform a small mock class to the protocol slice the handler actually needs — no 40-member stub:
import XCTest
@testable import AppsFlyerRPC
final class MockSimpleConfigSDK: AFRPCSimpleConfigSDK {
var isDebug: Bool = false
var customerUserID: String?
var customData: [AnyHashable: Any]?
var currencyCode: String?
var disableAdvertisingIdentifier: Bool = false
var disableSKAdNetwork: Bool = false
var shouldCollectDeviceName: Bool = false
var appInviteOneLinkID: String?
var anonymizeUser: Bool = false
var disableCollectASA: Bool = false
var disableAppleAdsAttribution: Bool = false
var useReceiptValidationSandbox: Bool = false
var useUninstallSandbox: Bool = false
func setPhoneNumber(_ phoneNumber: String) {}
var disableIDFVCollection: Bool = false
var currentDeviceLanguage: String?
var isStopped: Bool = false
}Construct the handler directly with the mock, build a typed request, call handle(_:), and assert
against the mock's captured state — no JSON involved at this layer.
final class AFRPCSimpleConfigHandlerTests: XCTestCase {
func testSetCustomerUserId_setsValue() async throws {
// Given
let mock = MockSimpleConfigSDK()
let handler = AFRPCSimpleConfigHandler(sdk: mock)
let req = try AFRPCSetCustomerUserIdRequest(from: ["customerId": "user-42"])
// When
let result = await handler.handle(.setCustomerUserId(req))
// Then
XCTAssertEqual(mock.customerUserID, "user-42")
guard case .success = result else { XCTFail("Expected success"); return }
}
}This is the layer your plugin's own Objective-C/Swift bridge code actually calls
(AppsFlyerRPCBridge.executeJson wraps AFRPCClient.execute(jsonRequest:)). Testing at this level
exercises real JSON parsing + validation + routing, not just one handler in isolation. Passing
{ _ in } as the event emitter is enough when the test doesn't care about async events.
final class MyBridgeUsageTests: XCTestCase {
func testSetCustomerUserId_endToEndFromRawJSON() async {
// Given
let client = AFRPCClient { _ in }
let json = #"{"id":"req-1","method":"setCustomerUserId","params":{"customerId":"user-42"}}"#
// When
let response = await client.execute(jsonRequest: json)
// Then
RPCTestHelpers.assertSuccessEnvelope(response, expectedId: "req-1")
}
}start, logEvent, validateAndLogInAppPurchase, logAndOpenStore, and generateInviteLink
support awaitResponse: true, which routes through SDKTimeoutHelper.withSDKCompletionTimeout —
an async race between the SDK's completion handler and a timer, not a blocked thread + latch (the
Android bridge blocks a real thread for this; the iOS one is structured concurrency throughout).
Mocks that invoke their completion handler synchronously make this deterministic, no sleeping:
final class AFRPCCoreHandlerTests: XCTestCase {
func testStart_awaitResponseTrue_returnsSuccessOnceSDKCompletes() async throws {
// Given — MockCoreSDK.start(completionHandler:) invokes the handler immediately
let mock = MockCoreSDK()
let handler = AFRPCCoreHandler(sdk: mock, timeoutConfig: .test)
let req = try AFRPCStartRequest(from: ["awaitResponse": true])
// When
let result = await handler.handle(.start(req))
// Then
guard case .success = result else { XCTFail("Expected success"); return }
XCTAssertTrue(mock.startCalled)
}
}timeoutConfig: .test (defined in SDKTimeoutHelper.swift) shortens every timeout to 0.3s so
timeout-path tests stay fast; production code always uses .default (10–30s per operation).
SDKTimeoutHelperTests.swift covers this directly at the helper level, since it's the single
choke point every awaited domain call routes through:
final class SDKTimeoutHelperTests: XCTestCase {
func testTimeoutFiresWhenOperationNeverCompletes() async {
// Given / When — the SDK-call closure never invokes `completion`
let result = await SDKTimeoutHelper.withSDKCompletionTimeout(
timeoutSeconds: 0.5,
operationName: "neverComplete"
) { _ in }
// Then
guard case .failure(let failure) = result else { XCTFail("Expected failure"); return }
XCTAssertEqual(failure.errorType, "timeout")
XCTAssertTrue(failure.message.contains("timed out"))
}
}If your plugin bridge wraps this in its own async/await or Promise layer, mirror this shape: never invoke the mock's completion handler and assert your wrapper surfaces a timeout, not a hang.
Two independent failure points: an unrecognized method string (404), and a recognized method
missing a required param (422).
func testUnknownMethod_throwsUnknownMethodError() {
XCTAssertThrowsError(try AFRPCParser.validateMethod("notARealMethod")) { error in
guard case AFRPCClientError.unknownMethod(let method) = error else {
XCTFail("Expected unknownMethod, got \(error)"); return
}
XCTAssertEqual(method, "notARealMethod")
XCTAssertEqual(AFRPCClientError.unknownMethod(method).rpcError.code, 404)
}
}
func testInitialize_missingDevKey_throwsMissingParameter() {
XCTAssertThrowsError(try AFRPCInitRequest(from: ["appId": "id123456789"])) { error in
guard case AFRPCClientError.missingParameter(let param) = error else {
XCTFail("Expected missingParameter, got \(error)"); return
}
XCTAssertEqual(param, "devKey")
XCTAssertEqual(AFRPCClientError.missingParameter(param).rpcError.code, 422)
}
}Unlike the Android bridge (optString/optBoolean with silent defaults), the iOS Request(from:)
initializers throw on missing required params — there's no equivalent "loose validation" gotcha
here to defend against on the plugin side.
registerConversionListener / registerDeeplinkListener don't return data directly — they wire
the RPC layer as the SDK delegate, which later emits async events through the same emitter your
plugin's setEventHandler callback receives. Simulate the SDK invoking its delegate method and
assert on the emitted AFRPCEvent, exactly as AFRPCListenerLifecycleTests.swift does:
final class AFRPCListenerLifecycleTests: XCTestCase {
func testRegisterConversionListener_delegateCallback_emitsEvent() async throws {
// Given
var receivedEvents: [AFRPCEvent] = []
let eventExpectation = expectation(description: "onConversionDataSuccess emitted")
let handler = AFRPCRequestHandler { event in
receivedEvents.append(event)
if event.event == "onConversionDataSuccess" { eventExpectation.fulfill() }
}
let request = try AFRPCRegisterConversionListenerRequest(from: [:])
_ = await handler.handle(request: .registerConversionListenerRequest(request), requestId: nil)
// When — simulate the SDK firing its AppsFlyerLibDelegate callback
handler.onConversionDataSuccess(["af_status": "Non-organic", "media_source": "facebook"])
// Then
await fulfillment(of: [eventExpectation], timeout: 2.0)
XCTAssertEqual(receivedEvents.first?.event, "onConversionDataSuccess")
XCTAssertEqual(receivedEvents.first?.origin, "ios")
}
}For the full existing suite (mock-per-domain handler tests, router dispatch tests, concurrency tests, bridge hardening tests, characterization tests), see:
AppsFlyerRPC/AppsFlyerRPCTests/AFRPCConsolidatedHandlerTests.swift— one mock + handler test per domain, plus router dispatch testsAppsFlyerRPC/AppsFlyerRPCTests/AppsFlyerRPCiOSTests.swift— handler/event/bridge/client integration testsAppsFlyerRPC/AppsFlyerRPCTests/SDKTimeoutHelperTests.swift— timeout helper unit testsAppsFlyerRPC/AppsFlyerRPCTests/AppsFlyerRPCValidationTests.swift— required-param validation errors, one per methodAppsFlyerRPC/AppsFlyerRPCTests/AppsFlyerRPCRoutingTests.swift— unknown-method / error-code mappingAppsFlyerRPC/AppsFlyerRPCTests/RPCTestHelpers.swift— shared envelope assertion helpers used above
For full JSON payloads, parameter tables, and the plugin integration walkthrough (React Native / Flutter examples, step-by-step new-plugin guide, event schemas), see README.md.