Table of Contents

What are universal links?

Mobile devices are increasingly becoming the preferred method of receiving, reading, and engaging with email. If you send an email containing a link to your website, but you also have a corresponding mobile application, it is possible to ensure that any recipients who click the link on their mobile device are taken directly to your app instead of their web browsers.

This is accomplished by using universal links. A universal link is a unique URL that can be configured to open a window in either the recipient’s web browser, mobile browser, or mobile application depending on the device the recipient is using. SendGrid enables you to simply tag individual links that you would like to be converted to universal links, without sacrificing the ability to track clicks on those links.

These links are sometimes referred to as “deep links” in the context of Google’s Android OS. Apple uses the term “universal links”.

Regardless of the OS you are configuring your links for, we will use the term “universal links”.

When setting up universal links for your app, it is important to ensure that you maintain the ability to track when users click those links. After configuring your universal links, we will explain how to ensure that your universal links are tracked.

Marketing Campaigns does not support universal links by default! If you would like to include universal links in your campaign, you must ensure that you edit the HTML of your template to appropriately flag your links as universal.

Requirements

There are several requirements that you must complete before you can begin using universal links in your email:

  • Universal links for iOS require an “apple-app-site-association” JSON file.
  • Universal links for Android require that you set up an “digital asset links” JSON file, along with configuring intent filters in your Android app’s manifest file.
  • Your apple-app-site-association and digital asset links files must be hosted on an HTTPS web server or content delivery network (CDN).
  • To ensure that your universal links register click tracking events, and to ensure that your recipient is taken to the correct page within your app, you must properly resolve your links.
  • You must complete the link whitelabeling process for your account. When whitelabeling your links, you must use the same domain that will be used for your universal links. (e.g. links.example.com)
  • On iOS, you must include your email link whitelabel subdomain in the “Associated Domains” for your app. Using the example above, you’d need to add an entry for “applinks:links.example.com” like this:

What are “apple-app-site-association” and “digital asset links” files?

To keep your app secure, Google and Apple want to restrict which resources or websites are allowed to link directly to different pages within your app. This prevents bad actors from using universal links to gain access to sensitive information within your app.

Your “apple-app-site-association” and “digital asset links” files serve as secure means of authenticating your universal links; they verify that your website is allowed to open up a page within your app.

You must create your own digital asset links and apple-app-site-association files, and you must upload these files to a secure server.

Both “apple-app-site-association” and “digital asset links” files are comprised of a series of JSON key/value pairs that associate external URLs with pages within your application.

For detailed instructions on how to configure an iOS “apple-app-site-association” file, please see Apple’s Developer Documentation.

For detailed instructions on how to configure an Android “digital asset links” file, please visit Google’s Developer Documentation.

Example apple-app-site-association file:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "[YOUR APP ID HERE]",
        "paths": [
          "/uni/*"
        ]
      }
    ]
  }
}

Do not append the .json file extension to your apple-app-site-association file!

Example assetlinks.json file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
  {
    "target": {
      "namespace": "android_app",
      "package_name": "[YOUR APP’S PACKAGE NAME]",
      "sha256_cert_fingerprints": [
        "[YOUR APP FINGERPRINT HERE]"
      ]
    },
    "relation": [
      "delegate_permission/common.handle_all_urls"
    ]
  }
]

When configuring your universal links in iOS, you specify which paths you want to be handled by the app by using the paths argument in the apple-app-site-association file. By specifing only the path ["/uni/*"], and using the universal=true attribute on your links as documented below, only appropriate links will be handled by the app, and others will be opened in the phone’s browser.

Android requires that you specify these paths inside your app, rather than the assetlinks.json file. This is accomplished by adding intent filters for specific hosts and paths. Please visit Google’s Developer Documentation to learn how to add an intent filter to your app manifest that can handle your universal links.

Once you have created and configured your Android and iOS configuration files, you will have to host them on a secure HTTPS server. Keep reading below to learn how you can host these files on either Amazon CloudFront or NGINX.

Setting Up Universal Links Using CloudFront

After creating your iOS “apple-app-site-association” file and/or your Android “digital asset links” file, you need to host them on a secure content delivery network. The following instructions will guide you through setting up Amazon’s CloudFront to host these files.

  1. Navigate to Amazon CloudFront. Once you have created an account or are logged into your existing account, create a new S3 bucket and give it a unique name (e.g. links-example-com)

  2. Upload your “apple-app-site-association” file into the root of the new S3 bucket

  3. Under Permissions on the uploaded file, add a permission for Everyone to Open/Download (or Read in the new S3 UI), then hit Save

  4. Under Metadata on the uploaded file, change the Content-Type value to application/json, then hit Save

  5. Create a new folder in the bucket called “.well-known”

  6. Inside of the “.well-known” folder, upload the same “apple-app-site-association” file as in step 2

  7. As above, add a permission for Everyone to Open/Download (or Read in the new S3 UI) and change the Content-Type to “application/json”

  8. Inside of the “.well-known” folder, upload your “assetlinks.json”

  9. Repeat step 7 for your “assetlinks.json” file: add a permission for Everyone to Open/Download (or Read in the new S3 UI) and change the Content-Type to “application/json”

  10. Navigate to the AWS Certificate Manager

  11. Request a new certificate for the domain your link whitelabel is configured for (e.g. links.example.com)

  12. AWS will send an email to the appropriate domain owners, requesting them to approve the certificate

  13. Ensure that the certificate is approved and issued

  14. Navigate to AWS CloudFront

  15. Create a new Distribution that is a Web delivery method

  16. Under the Origin Settings section, set the fields as follows:

    • Origin Domain Name: sendgrid.net
    • Origin ID: sendgrid.net
    • Origin SSL Protocols: only TLSv1.2
    • Origin Protocol Policy: HTTPS Only
  17. Under the Default Cache Behavior Settings section, set the fields as follows:

    • Forward Headers: Yes
    • Forward Query Strings: Forward all, cache based on all
  18. Under the Distribution Settings section, set the fields as follows:

    • Alternate Domain Names: links.example.com
    • SSL Certificate: Custom SSL Certificate, pointing to the appropriate ACM certificate
  19. Hit Create Distribution

  20. Once the distribution is created, click into Distribution Settings

  21. Under the Origins tab, create a new origin with the following details

    • Origin Domain Name: links-example-com.s3.amazonaws.com
    • Origin ID: s3
  22. Click Create

  23. Under the Behaviors tab, create a new behavior with the following details

    • Path Pattern: apple-app-site-association
    • Origin: s3
    • Viewer Protocol Policy: HTTPS Only
  24. Click Create

  25. Create another behavior with the following details

    • Path Pattern: .well-known/apple-app-site-association
    • Origin: s3
    • Viewer Protocol Policy: HTTPS Only
  26. Click Create

  27. Create a third behavior with the following details

    • Path Pattern: .well-known/assetlinks.json
    • Origin: s3
    • Viewer Protocol Policy: HTTPS Only
  28. Hit Create

  29. Ensure that the Behaviors are sorted so that the Default is the last onNewIntent

  30. Wait for the distribution to deploy

  31. Verify that the distribution serves up the expected files (do this without changing the real DNS to avoid causing any issues with existing links)

    • https://links.example.com/apple-app-site-association
    • https://links.example.com/.well-known/apple-app-site-association
    • https://links.example.com/.well-known/assetlinks.json
    • https://links.example.com/wf/click?upn=
  32. Verify behavior using https://branch.io/resources/universal-links/

Setting Up Universal Links Using NGINX

After creating your iOS “apple-app-site-association” file and/or your Android “digital asset links” file, you need to host them on a secure content delivery network. The following instructions will guide you through setting up NGINX to host these files.

  1. Request a new certificate for the domain your link whitelabel is configured for (e.g. links.example.com)

  2. Place the certificate chain into the file named /etc/pki/tls/certs/links.example.com.crt

  3. Place the private key into the file named /etc/pki/tls/private/links.example.com.key

  4. Create the following directory /var/www/links.example.com

  5. Create the file /var/www/links.example.com/apple-app-site-association, with the appropriate content for your apple-app-site-association file, as explained in Apple’s Developer Documentation.

  6. Create the directory /var/www/links.example.com/.well-known

  7. Create the file /var/www/links.example.com/.well-known/apple-app-site-association, with the appropriate content for your apple-app-site-association file

  8. Create the file /var/www/links.example.com/.well-known/assetlinks.json, with the appropriate content for your digital asset links file, as explained in Google’s Developer Documentation.

  9. Create the file /etc/nginx/conf.d/links.example.com.conf, with the following content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
server {
  listen 80;
  listen 443 ssl;
  server_name 'links.example.com';
  ssl_certificate '/etc/pki/tls/certs/links.example.com.crt';
  ssl_certificate_key '/etc/pki/tls/private/links.example.com.key';
  location = /apple-app-site-association {
    root '/var/www/links.example.com';
    default_type 'application/json';
  }
  location = /.well-known/apple-app-site-association {
    root '/var/www/links.example.com';
    default_type 'application/json';
  }
  location = /.well-known/assetlinks.json {
    root '/var/www/links.example.com';
    default_type 'application/json';
  }
  location / {
    proxy_pass 'https://sendgrid.net';
    proxy_set_header 'Host' 'links.example.com';
  }
}

Flagging Your Universal Links

It is important to make sure that only the links within your email that point to your app are flagged as universal links.

It is not unusual to include links to pages outside of your app alongside links to your app in the same email. Not all of these links should be treated as universal links. For example, if you have Facebook or Twitter links tagged as universal links, users will be taken to your app when they click those links instead of being taken to your Facebook and Twitter pages.

To flag links to your app as universal links, simply include the attribute universal="true" within the HTML link of your email.

For example:

<a href="links.example.com" universal="true">Link to your app!</a>

This way, as long as your association file has the paths restricted to /uni/* as we recommend above, only the links that you want to be handled by your app will be.

If you exclude the universal="true" attribute, your links will still function, but they will take your recipient to their mobile browser. If you exclude the /uni/* path in your apple-app-site-association, the all links for your Whitelabeled domain will be forwarded for your app to handle, which may cause issues.

Resolving SendGrid Click Tracking Links

Handling Click Tracking Links In Your App

Now that you’ve successfully set up your app to open SendGrid click tracking links, you’ll want to ensure that your app handles them properly. The link your app receives will be the SendGrid encoded link, so you’ll want to resolve the link in order to:

  1. Trigger the “click” event in your SendGrid account for statistics.
  2. Find the original URL to determine which part of your app the user should be taken to.

The following code examples help to illustrate what logic should be included within your own app to guarantee that your links are resolved, and tracked by SendGrid.

Resolving Links in iOS

If you have written your app for iOS, you can use NSURLSession resolve the link.

For example:

Using Swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
    if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
        guard let encodedURL = userActivity.webpageURL else {
            print("Unable to handle user activity: No URL provided")
            return false
        }
        let task = URLSession.shared.dataTask(with: encodedURL, completionHandler: { (data, response, error) in
            guard let resolvedURL = response?.url else {
                print("Unable to handle URL: \(encodedURL.absoluteString)")
                return
            }
            // Now you have the resolved URL that you can
            // use to navigate somewhere in the app.
            print(resolvedURL)
        })
        task.resume()
    }
    return true
}

Using Objective-C

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler {
    if (userActivity.activityType == NSUserActivityTypeBrowsingWeb) {
        NSURL *encodedURL = userActivity.webpageURL;
        if (encodedURL == nil) {
            NSLog(@"Unable to handle user activity: No URL provided");
            return false;
        }
        NSURLSession *session = [NSURLSession sharedSession];
        NSURLSessionDataTask *task = [session dataTaskWithURL:encodedURL completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            if (response == nil || [response URL] == nil) {
                NSLog(@"Unable to handle URL: %@", encodedURL.absoluteString);
                return;
            }
            // Now you have the resolved URL that you can
            // use to navigate somewhere in the app.
            NSURL *resolvedURL = [response URL];
            NSLog(@"Original URL: %@", resolvedURL.absoluteString);
        }];
        [task resume];
    }
    return YES;
}

Resolving Links in Android

If you have written your app for Android, you can use HttpURLConnection to resolve the URL by setting setInstanceFollowRedirects to false.

For example:

Using Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    onNewIntent(getIntent());
}

protected void onNewIntent(Intent intent) {
    String action = intent.getAction();
    final String encodedURL = intent.getDataString();
    if (Intent.ACTION_VIEW.equals(action) && encodedURL != null) {
        Log.d("App Link", encodedURL);
        new Thread(new Runnable() {
            public void run() {
                try {
                    URL originalURL = new URL(encodedURL);
                    HttpURLConnection ucon = (HttpURLConnection) originalURL.openConnection();
                    ucon.setInstanceFollowRedirects(false);
                    URL resolvedURL = new URL(ucon.getHeaderField("Location"));
                    Log.d("App Link", resolvedURL.toString());
                }
                catch (MalformedURLException ex) {
                    Log.e("App Link",Log.getStackTraceString(ex));
                }
                catch (IOException ex) {
                    Log.e("App Link",Log.getStackTraceString(ex));
                }
            }
        }).start();
    }
}