How to add Apple Search Ads attribution to your iOS app
Tags: ios, swift, analytics
Published: 2021-06-06 19:52:25

**Do you run Apple Search Ads for your iOS app, and would like to see which users came from which ads? This post describes the necessary steps to fetch this attribution data (which Apple provides, natively)!**

I'm part of a small startup building [an iOS app that teaches coding](https://imagi.app.link/YwXtxICrTgb), and a few months ago ago we decided that we needed to ramp up our attribution efforts, in order to see which of our marketing endeavours were paying off. 

We started out using [Branch](https://branch.io) links for our non-Apple Search Ads campaigns. It's a service that allows you to create links leading to your app, and then figure out which campaigns users came from with some minimal app-level changes. We realized that for Apple Search Ads we would need a different solution, as the ads are presented inside the App Store, and therefore we would not have the ability to link the user to a third-party service which would do the attribution magic.

The [Apple Search Ads console](https://searchads.apple.com/) provides information about clicks and installs, however we wanted to be able to also follow users beyond their install. Thankfully, Apple provides a native way to get campaign data, allowing developers to fetch this data after users either install or open the app from an Apple Search ad.

This tutorial was created using Xcode Version 12.5, using Swift 5, on iOS 14.5, and I'll be updating it if need be after this week's WWDC!

### Fetching and sending attribution data for Apple Search Ads

We live in a post iOS 14.3 world, which was the iOS version where Apple replaced its iAd framework with a framework called AdServices. Therefore, as long as you support versions prior to iOS 14.3, you will need to implement two ways of fetching campaign data, using the two frameworks.

Let's start by adding the necessary frameworks to our app. In order to do so, head to your app's General settings, and under *Frameworks, Libraries, and Embedded Content*, add the following two frameworks: *AdServices.framework* and *iAd.framework*.

![Screenshot of Frameworks, Libraries and Embedded Content section with added frameworks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ozz1p7odqquqonknkvqu.png)

#### AdServices implementation, valid for versions 14.3 and up.

The first step is to check for the user's attribution token. This token is generated regardless of whether the user actually opened the app from a campaign or not.

```swift
if #available(iOS 14.3, *) {
    if let attributionToken = try? AAAttribution.attributionToken() {
    }
}
```

The next step is to request the attribution data, using the user's attribution token.
```swift
if #available(iOS 14.3, *) {
    if let attributionToken = try? AAAttribution.attributionToken() {
        let request = NSMutableURLRequest(url: URL(string:"https://api-adservices.apple.com/api/v1/")!)
        request.httpMethod = "POST"
        request.setValue("text/plain", forHTTPHeaderField: "Content-Type")
        request.httpBody = Data(attributionToken.utf8)
    }
}
```

You can now find the campaign info you're looking for! You can see the full list of provided info in the attribution payload [here](https://developer.apple.com/documentation/adservices/aaattribution/3697093-attributiontoken#3697458). For the sake of this tutorial I'll be using just the campaign ID.

The values of the various attributes, like the campaign ID, are set to a mock Int value of *1234567890* in the case in which the user has not actually come from a campaign.

I then proceed to send the entire result object to [Amplitude](https://analytics.amplitude.com/), which is the analytics tool that we use. If you don't already use some sort of analytics tool, I highly recommend Amplitude, with their free tier being very comprehensive. You could however also store this data as a user attribute in your backend if you'd prefer, and only store one of the attribution attributes as opposed to all of them. I've left that part of the implementation blank, assuming you already have some sort of tracking system in place.

```swift
if #available(iOS 14.3, *) {
    if let attributionToken = try? AAAttribution.attributionToken() {
        let request = NSMutableURLRequest(url: URL(string:"https://api-adservices.apple.com/api/v1/")!)
        request.httpMethod = "POST"
        request.setValue("text/plain", forHTTPHeaderField: "Content-Type")
        request.httpBody = Data(attributionToken.utf8)
        let task = URLSession.shared.dataTask(with: request as URLRequest) { (data, _, error) in
            if let error = error {
                print(error)
                return
            }
            do {
               let result = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! [String:Any]
                print("Search Ads attribution info:", result)
                if let campaignId = result["campaignId"] as? Int {
                // Only send data to Amplitude if it is not mock data, in which case the campaign id would be the integer below
                    if campaignId != 1234567890 {                                  
                        // Send data to your tracking tool, we use Amplitude, with the line of code below.
                        // Amplitude.instance().logEvent("open_app_from_apple_search_ad, with EventProperties: result)
                    }
                }
            } catch {
               print(error)
            }
        }
    task.resume()
    }
}
```
And that's it for iOS 14.3+!

#### iAd framework implementation (deprecated, used in versions 14.2 and under)

The iAd framework has a class called ADClient, through which you can simply request attribution details. You then receive a result object which differs slightly from the one fetched with AdServices. It contains two items: the Search Ads version and an attribution dictionary similar to the one returned above, and you can see the full payload [here](https://developer.apple.com/documentation/iad/setting_up_apple_search_ads_attribution/). 
```swift
ADClient.shared().requestAttributionDetails({ (attributionDetails, error) in
    guard let attributionDetails = attributionDetails else {
        print("Search Ads error: \(error?.localizedDescription ?? "")")
        return
    }
    for (version, adDictionary) in attributionDetails {
        print("Search Ads version:", version)
            if var attributionInfo = adDictionary as? Dictionary<String, Any> {
                print("Search Ads attribution info:", attributionInfo)
            }
        }
    }
})
```

In order to be able to consistently segment our users using Amplitude, I've added an extra attribute to the dictionary called *campaignId*, which is what it is called in AdServices, as opposed to *iad-campaign-id* in the iAd framework. Depending on your tracking system, you could do so as well, or potentially rename the attributes in the result.

It's also important to note that the mock data is in String format as opposed to Int format.

```swift
ADClient.shared().requestAttributionDetails({ (attributionDetails, error) in
    guard let attributionDetails = attributionDetails else {
        print("Search Ads error: \(error?.localizedDescription ?? "")")
        return
    }
    for (version, adDictionary) in attributionDetails {
        print("Search Ads version:", version)
            if var attributionInfo = adDictionary as? Dictionary<String, Any> {
                print("Search Ads attribution info:", attributionInfo)
                if let campaignId = attributionInfo["iad-campaign-id"] as? String {
                // Only send data to Amplitude if it is not mock data, in which case the campaign id would be the string of numbers below
                if campaignId != "1234567890" {
                    // Add campaignID attribute in order to have consistent property which which to segment users on all iOS versions
                    attributionInfo["campaignId"] = campaignId
                    // Send data to your tracking tool, we use Amplitude, with the line of code below.                                 
                    // Amplitude.instance().logEvent("open_app_from_apple_search_ad, with EventProperties: attributionInfo)
                }
            }
        }
    }
})
```

You can now package the whole implementation into a function:

```swift 
func requestAndSendAttributionData() {
    if #available(iOS 14.3, *) {
       ​if let attributionToken = try? AAAttribution.attributionToken() {
           ​let request = NSMutableURLRequest(url: URL(string:"https://api-adservices.apple.com/api/v1/")!)
           ​request.httpMethod = "POST"
           ​request.setValue("text/plain", forHTTPHeaderField: "Content-Type")
           ​request.httpBody = Data(attributionToken.utf8)
           ​let task = URLSession.shared.dataTask(with: request as URLRequest) { (data, _, error) in
               ​if let error = error {
                   ​print(error)
                   ​return
               ​}
               ​do {
                  ​let result = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! [String:Any]
                   ​print("Search Ads attribution info:", result)
                   ​if let campaignId = result["campaignId"] as? Int {
                   ​// Only send data to Amplitude if it is not mock data, in which case the campaign id would be the integer below
                       ​if campaignId != 1234567890 {                                  
                           ​// Send data to your tracking tool, we use Amplitude, with the line of code below.
                           ​// Amplitude.instance().logEvent("open_app_from_apple_search_ad, with EventProperties: result)
                       ​}
                   ​}
               ​} catch {
                  ​print(error)
               ​}
           ​}
       ​task.resume()
       ​}
    } else {
       ​ADClient.shared().requestAttributionDetails({ (attributionDetails, error) in
           ​guard let attributionDetails = attributionDetails else {
               ​print("Search Ads error: \(error?.localizedDescription ?? "")")
               ​return
           ​}
           ​for (version, adDictionary) in attributionDetails {
               ​print("Search Ads version:", version)
                   ​if var attributionInfo = adDictionary as? Dictionary<String, Any> {
                       ​print("Search Ads attribution info:", attributionInfo)
                       ​if let campaignId = attributionInfo["iad-campaign-id"] as? String {
                       ​// Only send data to Amplitude if it is not mock data, in which case the campaign id would be the string of numbers below
                       ​if campaignId != "1234567890" {
                           ​// Add campaignID attribute in order to have consistent property which which to segment users on all iOS versions
                           ​attributionInfo["campaignId"] = campaignId
                           ​// Send data to your tracking tool, we use Amplitude, with the line of code below.                                 
                           ​// Amplitude.instance().logEvent("open_app_from_apple_search_ad, with EventProperties: attributionInfo)
                       ​}
                   ​}
               ​}
           ​}
       ​})
    }
}
```

You can then call *requestAndSendAttributionData()* whenever the app is opened, and this will fetch and send the user's campaign information both after a fresh install, and if they already had the app installed.

If you'd like to check out Apple's official documentation for the frameworks above, you can find it here:
* [AdServices framework] (https://developer.apple.com/documentation/adservices)
* [iAd framework] (https://developer.apple.com/documentation/iad/setting_up_apple_search_ads_attribution/)

Happy attributing! I hope this helps with your marketing efforts, and let me know if you'd like me to go more in-depth into how we use Branch, or how we measure our analytics as a whole.