A universal link is a special URL type that works on web browsers and mobile apps. It ensures users have a smooth experience no matter what device they use. With universal links, businesses can make it easy for their users to directly open Android and iOS apps from delivered emails and access their content on any device via only 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's deliverability 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 |
Below you can see DNS record setup tutorials for some providers:
2. SSL setup for your link branding domain
To set up SSL for your link branding domain, you can 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 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 that will be 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 as in 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 |
3. Verification
Verification consists of two steps: verification on your end and verification on Insider. Once the previous steps are completed, you can verify if everything has been 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(1).png)
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:443You will receive a public certificate as a response with the following response:(1).png)
Once you verify everything on your end, you can contact the Insider One team to verify your setup. Once the setup is verified on Insider's end, your next email will have your custom domain in the links instead of sendgrid.net.
4. Handling asset files
For universal links to work, you need to include your application-specific asset files under the subdomain you set up on 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 you can see some 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-response6. Application
Once you’ve verified that the asset links are set up correctly, you can replace sendrid.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)
7. Link resolution
Now you can trigger your application directly from the email links. However, the links are encrypted and the destination is unavailable. Link resolution is required as the last step of the implementation for your application to redirect users to the relevant pages correctly within your application.
To complete this action, follow these three steps:
- Send an HTTP request to the link the user clicks to resolve the destination link.
- Obtain the destination from the sent request.
- Redirect the user to the relevant page in your application.
This will ensure that the user will be redirected to the destination page and the click log is delivered to the Insider server.
Below you can see some examples:
Android link resolution example
@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();
}
}
iOS link resolution examples
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.