Email Universal Link Setup

Prev Next

A universal link, also known as an email link that opens your app directly, is a special URL type that works in web browsers and mobile apps. It ensures a smooth experience no matter what device users use. With universal links, businesses can make it easy for users to open Android and iOS apps directly from emails and access content on any device with one link that works everywhere.

Universal links work as follows:

  • Opens app or web: If a user has the app, the link opens the app. If not, it opens the web. 

  • Offers better user experience: Users get to the right content quickly and easily.

This article explains how the universal link setup process works, provides SSL setup examples for multiple providers such as AWS, Google Cloud, etc., and the following steps to complete the setup process:

1. DNS record setup
2. SSL setup for your link branding domain
3. Verification
4. Handling asset files
5. Verification
6. Application
7. Link resolution

If you want to complete the universal link setup, you need to create a ticket for Insider One's team.

1. DNS record setup

Insider deliverability team will share two CNAME records with you to add under your domain. 

Add the respective CNAME records in your domain hosting provider settings and inform the deliverability team.

Type

Host

Value

CNAME

url{rand-id-1}.{your-domain}.com

sendgrid.net

CNAME

{rand-id-2}.{your-domain}.com

sendgrid.net

Or

Type

Host

Value

CNAME

{subdomain}.{your-domain}.com

sendgrid.net

CNAME

{rand-id-2}.{your-domain}.com

sendgrid.net

Replace the placeholders with your own specific URLs. E.g., url123.acme.com, 12334.acme.com.

Below you can see DNS record setup tutorials for some providers:

To set up SSL for your link branding domain, proceed to 2.1 or 2.2. If you are using one of the providers listed in 2.1 (e.g., AWS Route53, GoDaddy, Azure, etc.), proceed to follow section 2.1. If you're using a CDN, proceed to follow section 2.2.

2.1. Follow these steps if you are using one of the providers listed below (e.g., AWS Route53, GoDaddy, etc.).

You must set up SSL for your link branding domain (e.g., url123.yourdomain.com) as you would for the other domains.

SSL setup will vary depending on the service provider and the available certificate. You need a certificate valid for the CNAME record shared with you. If you already have a wildcard (*.yourdomain.com) SSL that covers any subdomains, you can easily set it up. However, if you choose to use a double subdomain (url123.sub.yourdomain.com) as your link branding and you do not have a double wildcard certificate (*.*.yourdomain.com) or your SSL certificate is exclusive to your main domain (yourdomain.com), you will need to purchase a certificate to complete the setup.

Below you can see SSL setup tutorials for some providers:

2.2. Follow these steps if you are using CDN.

Forward all the requests coming to url123.yourdomain.com to sendgrid.net. You can forward the request using your own CDN or Proxy.

Below you can see the CDN/Proxies documents for some providers:

At this point, your DNS records should look like the following example table:

Type

Host

Value

CNAME

url123.yourdomain.com

URL to your CDN or Proxy or API Gateway

CNAME

123456.yourdomain.com

sendgrid.net

Your title goes here

Do not change the CNAME for 123456.yourdomain.com; it should still point to sendgrid.net. 123456 is an example, and it can vary in your implementation.

3. Verification

Verification consists of two steps: verification on your end and verification on Insider. Once you complete the previous steps, you can verify that everything is set up correctly on your end. To verify on your end, you will need to run the following command:

dig ns url{rand-id}.{yourdomain}.com
// or
dig ns {subdomain}.{yourdomain}.com

Verify Certificate

To verify the certificate, run the following command:

openssl s_client -showcerts -connect url{rand-id}.{yourdomain}.com:443
// or
openssl s_client -showcerts -connect {subdomain}.{yourdomain}.com:443

You will receive a public certificate as a response with the following response:

Once you've verified everything on your end, contact the Insider One team to verify your setup. Once Insider verifies the setup, your next email will include your custom domain in the links instead of sendgrid.net.

4. Handling asset files

For universal links to work, include your application-specific asset files under the subdomain you set up in the DNS record setup step. Your setup must include route prioritization to re-route the incoming requests to assetlinks.json (Android) and apple-app-site-association (iOS) to your file server (or cloud storage) and everything else to sendgrid.net.

Adding the asset files under the subdomain will ensure that your app is opened instead of the web page every time it is clicked. For example, if your links are branded as https://links.acme.com, your asset files should be available as https://links.acme.com/.well-known/assetlinks.json, https://links.acme.com/.well-known/apple-app-site-association, and https://links.acme.com/apple-app-site-association (without the JSON extension).

The sample case above would look as follows:

Precedence

Path

Origin

Protocol

0

apple-site-association

File Server

HTTPS Only

1

.well-known/apple-site-association

File Server

HTTPS Only

2

.well-known/assetlinks.json

File Server

HTTPS Only

3

*

sendgrid.net

HTTP and HTTPS

Below are examples of different providers. 

NGINX

server {
  listen 80;
  listen 443 ssl;
  server_name 'links.acme.com';
  ssl_certificate '/etc/pki/tls/certs/links.acme.com.crt';
  ssl_certificate_key '/etc/pki/tls/private/links.acme.com.key';
  location = /apple-app-site-association {
    root '/var/www/links.acme.com';
    default_type 'application/json';
  }
  location = /.well-known/apple-app-site-association {
    root '/var/www/links.acme.com';
    default_type 'application/json';
  }
  location = /.well-known/assetlinks.json {
    root '/var/www/links.acme.com';
    default_type 'application/json';
  }
  location / {
    proxy_pass 'https://sendgrid.net';
    proxy_set_header 'Host' 'links.example.com';
  }
}

AWS Route53

Refer to the Universal Link Setup page to set up universal links using Route53, S3, and CloudFront.

GCP

For Google Cloud, you can utilize routes or use URL maps on your load balancer to handle the traffic.

5. Verification

If you set up your asset configuration internally, you can use a bastion server. If it's a public setup, you can directly test your asset links and routing. In either case, testing will be the same.

[must return JSON response]
curl http://internal.domain/.well-known/assetlinks.json
curl http://internal.domain/.well-known/apple-app-site-association
curl http://internal.domain/apple-app-site-association

[must return 404 response from sendgrid.net]
curl http://internal.domain/test-404-response

6. Application

Once you’ve verified that the asset links are set up correctly, you can replace sendgrid.net in your DNS setup with your internal setup.

links.acme.com => sendgrid.com
// will be updated as
links.acme.com => example.internal.load-balancer.com
// or
links.acme.com => cloudfront.com/12345
// or
links.acme.com => 127.0.0.1 (NGINX instance IP)

Verify that the same files and routing work through your subdomain (links.acme.com). If routing fails and the URLs are broken, avoid sending emails until you fix the issue.

Now you can trigger your application directly from the email links. However, the links are encrypted and the destination is unavailable. Link resolution is the final step in the implementation so your application can redirect users to the correct pages.

To complete this action, follow these three steps:

  1. Send an HTTP request to the link the user clicks to resolve the destination link.

  2. Obtain the destination from the sent request.

  3. Redirect the user to the relevant page in your application.

This ensures the user is redirected to the destination page and the click log is delivered to the Insider server.

Below, you can see some examples:

@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();
    }
}
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
}
- (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;
}

If you’re using an external company to handle your universal links such as branch.io, you might want to consult with the respective vendors on how to set up the universal links. If you have a mixed setup where you use both your own links and an external company setup, you need to complete all the steps listed in this guide and contact your provider to set it up with them as well.