ATS API Implementation Guide for Mobile Publishers
This article outlines the steps to implement the ATS Envelope API to retrieve and manage consented envelopes for mobile use cases.
Prerequisites for LiveRamp Secure Signals Adapter UsersIf you are leveraging the secure signals adapter with
LRAtsSDKorLRAtsSDKMediationAdapterto pass envelopes to Google's AdMob, make sure to perform the following tasks before proceeding:
- Remove ATS Mobile SDK and the Secure Signals adapter from applications.
- Remove any external calls to the SDK or
LRAtsMediationAdapter.configure().To implement a custom adapter to pass envelope signals from ATS API, see "Secure Signals for Mobile Publishers".
1. Create an ATS Placement in Console
Create an ATS placement in LiveRamp Console to obtain a Placement ID that will be used in your ATS requests. During this process, you must include the bundle IDs and the countries/regions where you want to run ATS. Note that your ATS placement must be in an 'Approved' state to retrieve valid envelopes.
To learn how you can obtain a Placement ID, see "Create an ATS Placement".
2. Collect Valid Consent Strings
Based on the user's location, ATS Envelope API may require a standardized consent string to retrieve envelopes. Make sure you have a proper Consent Management Platform (CMP) in place that can provide a valid consent string.
See the consent requirement for retrieving envelopes below:
- For EU: Positive consent required and must be TCF v2.3 compatible.
- For US: Consent not required, but recommended in conjunction with the Global Privacy Platform (GPP).
- Other countries: Consent signal not required.
- For iOS devices: ATT consent required.
For more information, see "Provide a Valid Consent String".
3. Prepare the Identifiers
To improve match rate to audience data, we recommend pre-processing your data to ensure valid identifiers are provided when calling the ATS Envelope API. ATS supports the following identifier types, which you must indicate correctly via the it parameter in your call:
- Hashed email (
it=4)- SHA256 (recommended if you are only able to hash once)
- SHA1
- MD5
- Hashed phone (
it=11)- SHA1
- Custom ID (
it=15)
Before hashing, pre-process your identifiers to ensure valid identifiers are being sent. For more information on data normalization, see "Prepare the Identifiers".
Email Normalization and Hashing (iOS)
func normalizeEmail(_ email: String) -> String {
var cleaned = email.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
let parts = cleaned.split(separator: "@", omittingEmptySubsequences: false)
guard parts.count == 2 else { return cleaned }
var username = String(parts[0].split(separator: "+").first ?? parts[0])
let domain = String(parts[1])
if domain == "gmail.com" {
username = username.replacingOccurrences(of: ".", with: "")
}
return "\(username)@\(domain)"
}
// Usage: hash normalized email with SHA1, SHA256, MD5
let normalized = normalizeEmail("[email protected]")
// → "[email protected]"
let sha1 = sha1Hash(normalized)
let sha256 = sha256Hash(normalized)
let md5 = md5Hash(normalized)
Phone Number Formatting and Hashing (iOS)
func formatPhone(_ phone: String) -> String {
var digits = phone.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: "[^0-9]", with: "", options: .regularExpression)
digits = digits.replacingOccurrences(of: "^0+(?!$)", with: "", options: .regularExpression)
return digits
}
// Usage: hash formatted phone with SHA1
let formatted = formatPhone("+1 (555) 123-4567")
// → "15551234567"
let sha1 = sha1Hash(formatted)
4. Call the ATS Envelope API
Call the v2 ATS Envelope API endpoint pattern with the required request parameters, keeping the following in mind:
- Include a bundle ID tied to your ATS placement in the origin header of every request. Prefix your bundle ID with
https://in the origin header (for example,https://com.example.app). - For EU users, only call the API with a positive consent string.
See "Call the ATS Envelope API" for more details on each required parameter.
Android
val client = OkHttpClient()
val url = "https://api.rlcdn.com/api/identity/v2/envelope?pid=14&it=4&iv=YOUR_HASHED_IDENTIFIER&atype=2&ct=4&cv=YOUR_TCF_STRING&gpp_sid=7&gpp=YOUR_GPP_STRING"
val request = Request.Builder()
.url(url)
.get()
.addHeader("origin", "https://com.example.app")
.addHeader("accept", "application/json")
.build()
val response = client.newCall(request).execute()
val body = response.body?.string()
OkHttpClient client = new OkHttpClient();
String url = "https://api.rlcdn.com/api/identity/v2/envelope?pid=14&it=4&iv=YOUR_HASHED_ID&atype=2&ct=4&cv=YOUR_TCF_STRING";
Request request = new Request.Builder()
.url(url)
.get()
.addHeader("origin", "https://com.example.app")
.addHeader("accept", "application/json")
.build();
Response response = client.newCall(request).execute();
String body = response.body() != null ? response.body().string() : null;
iOS
import Foundation
let baseURL = "https://api.rlcdn.com/api/identity/v2/envelope"
let pid = 14
let it = 4 // identifier type: 4=email, 11=phone, 15=custom id
let iv = "sha256_or_sha1_hex_of_email"
let atype = 2 // 2 = ATS API
var components = URLComponents(string: baseURL)!
components.queryItems = [
URLQueryItem(name: "pid", value: "\(pid)"),
URLQueryItem(name: "it", value: "\(it)"),
URLQueryItem(name: "iv", value: iv),
URLQueryItem(name: "atype", value: "\(atype)")
]
// Add consent if needed: ct, cv, gpp_sid, gpp
var request = URLRequest(url: components.url!)
request.httpMethod = "GET"
request.setValue("https://com.example.app", forHTTPHeaderField: "origin")
request.setValue("application/json", forHTTPHeaderField: "accept")
URLSession.shared.dataTask(with: request) { data, response, error in
let body = data.flatMap { String(data: $0, encoding: .utf8) }
// Handle response
}.resume()
#import <Foundation/Foundation.h>
NSString *baseURL = @"https://api.rlcdn.com/api/identity/v2/envelope";
NSInteger pid = 14;
NSInteger it = 4; // 4=email, 11=phone, 15=custom id
NSString *iv = @"YOUR_HASHED_ID";
NSInteger atype = 2;
NSString *urlString = [NSString stringWithFormat:@"%@?pid=%ld&it=%ld&iv=%@&atype=%ld",
baseURL, (long)pid, (long)it, [iv stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet], (long)atype];
NSURL *url = [NSURL URLWithString:urlString];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"GET";
[request setValue:@"https://com.example.app" forHTTPHeaderField:@"origin"];
[request setValue:@"application/json" forHTTPHeaderField:@"accept"];
[[NSURLSession.sharedSession dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSString *body = data ? [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] : nil;
}] resume];
Server-to-Server IntegrationsWhen retrieving or refreshing the ATS Envelopes, publishers integrating server-to-server are required to include the
X-Forwarded-Forheader to ensure that the client-side IP address is within the approved countries tied to the ATS Placement.For example:
curl --request GET \ --url 'https://api.rlcdn.com/api/identity/v2/envelope?pid=14&it=4&iv=38CFDA7F9B55B7F74B6D3D143CBB7661DA9BFEB4683473A5813B220A571A1E37&ct=4&cv=CPUCbs9PUDXghADABCENCBCoAP_AAEJAAAAADGwBAAGABPADCAY0BjYAgADAAngBhAMaAAA.YAAAAAAAA4AA' \ --header 'Origin: https://example.com' \ --header 'X-Forwarded-For: 203.0.113.195' \ --header 'accept: application/json'
Responses
For opted-in users, a successful ATS response returns HTTP 200 with one or more envelopes (depending on your integration) in JSON format.
{"envelopes":[{"type":19,"source":"envelopeLiveramp","value":"Aqs3wtOuqIRic78_s4Nqilcr0MGn0cDvKzvpEhSGVjwV8Ci0Jy73B1bkfpv03GwK2uyKbME4kSAtVFn_5sCLWstyHTnEWYurbjLXLpgtf9MfLP1AeDTUQ_0ESnD3x1pr6gYfBOOUH8BPhbfscxdaGUA-_sacPab3nzc05koLhbqrJIQC7JBraEbpOJPDAHp9f44DXIXm","err":null},{"type":25,"source":"pairIds","value":"WyJBeVhiNUF0dmsvVS8xQ1d2ejJuRVk5aFl4T1g3TVFPUTJVQk1BMFdiV1ZFbSJd","err":null}]}For opted-out users, the call will return a 204 response with no content.
For more information, see "ATS Envelope API Responses".
5. Store the Envelope with a Timestamp
Publishers can store envelopes retrieved from mobile devices using a local database or other storage mechanisms, such as UserDefaults, property lists (plist), or the Keychain on iOS, and SharedPreferences on Android.
When managing envelopes, note the following key points:
- Expiry: An envelope should be considered expired if the current time (
now) minus thecreatedAttimestamp exceeds the pre-defined Time-To-Live (TTL), e.g., 15 days (in milliseconds). - Refresh: An envelope is due for a refresh if the current time (
now) minus thelastRefreshTimeexceeds the designated refresh interval, e.g., 15 minutes (in milliseconds).
The table below lists the minimum required structure for storing envelope data. Depending on your application design, it may hold a single row or one row per user.
| Column | Type | Description |
|---|---|---|
| id | Long | Primary key (auto-increment) |
| envelope | String | The LiveRamp primary ATS envelope (type 19). |
| envelope24 | String | The Meta envelope (type 24). |
| envelope25 | String | The PAIR envelope (type 25). |
| envelope26 | String | The ATS Direct envelope (type 26). |
| envelope27 | String | The Google SSP envelope (type 27). |
| lastRefreshTime | Long | Timestamp (in milliseconds) of the most recent envelope refresh. |
| createdAt | Long | Timestamp (in milliseconds) when the envelope was first generated. |
| userId (optional) | Identifier needed if you store multiple envelopes, one for each user. |
Returning Stored EnvelopesFor troubleshooting purposes and as required by vendors, such as Prebid.js, you must implement a mock function that returns envelope to the caller from the application.
6. Implement Envelope Refresh
Refreshing envelopes is an important step to keep the match rate as high as possible for your users. Calling the Refresh Envelope endpoint will reset the envelope expiration to the maximum allowed value, after which you need to store the newly obtained envelope and update the timestamp in storage.
Android
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://api.rlcdn.com/api/identity/v2/envelope/refresh?pid=14&it=19&iv=ApN4iB_v5QsHRLDYDvEPmdhbdaknzxNA3-4Al2d_JFnP6IlH9cApY45f3ohEbriGAzRGVmnizG1iE8bV9MK66Dde9wCektrTCHCD8wKmRkimgOJQyfzwWRfMljKUlcN3TE0k&ct=4&gpp_sid=7&cv=CPUCbs9P...&gpp=DBABLA~...&atype=2")
.get()
.addHeader("origin", "com.example.app")
.addHeader("accept", "application/json")
.build();
Response response = client.newCall(request).execute();
String body = response.body() != null ? response.body().string() : null;
val client = OkHttpClient()
val request = Request.Builder()
.url("https://api.rlcdn.com/api/identity/v2/envelope/refresh?pid=14&it=19&iv=ApN4iB_v5QsHRLDYDvEPmdhbdaknzxNA3-4Al2d_JFnP6IlH9cApY45f3ohEbriGAzRGVmnizG1iE8bV9MK66Dde9wCektrTCHCD8wKmRkimgOJQyfzwWRfMljKUlcN3TE0k&ct=4&gpp_sid=7&cv=CPUCbs9PUDXghADABCENCBCoAP_AAEJAAAAADGwBAAGABPADCAY0BjYAgADAAngBhAMaAAA.YAAAAAAAA4AA&gpp=DBABLA~BVQqAAAAAAA&atype=2")
.get()
.addHeader("origin", "com.example.app")
.addHeader("accept", "application/json")
.build()
val response = client.newCall(request).execute()
val body = response.body?.string()
iOS
let refreshURL = "https://api.rlcdn.com/api/identity/v2/envelope/refresh"
var components = URLComponents(string: refreshURL)!
components.queryItems = [
URLQueryItem(name: "pid", value: "14"),
URLQueryItem(name: "it", value: "19"),
URLQueryItem(name: "iv", value: "ApN4iB_v5QsHRLDYDvEPmdhbdaknzxNA3-4Al2d_JFnP6IlH9cApY45f3ohEbriGAzRGVmnizG1iE8bV9MK66Dde9wCektrTCHCD8wKmRkimgOJQyfzwWRfMljKUlcN3TE0k"),
URLQueryItem(name: "ct", value: "4"),
URLQueryItem(name: "gpp_sid", value: "7"),
URLQueryItem(name: "cv", value: "CPUCbs9PUDXghADABCENCBCoAP_AAEJAAAAADGwBAAGABPADCAY0BjYAgADAAngBhAMaAAA.YAAAAAAAA4AA"),
URLQueryItem(name: "gpp", value: "DBABLA~BVQqAAAAAAA"),
URLQueryItem(name: "atype", value: "2")
]
var request = URLRequest(url: components.url!)
request.httpMethod = "GET"
request.setValue("com.example.app", forHTTPHeaderField: "origin")
request.setValue("application/json", forHTTPHeaderField: "accept")
URLSession.shared.dataTask(with: request) { data, _, _ in
let body = data.flatMap { String(data: $0, encoding: .utf8) }
}.resume()
NSString *refreshURL = @"https://api.rlcdn.com/api/identity/v2/envelope/refresh?pid=14&it=19&iv=ApN4iB_v5QsHRLDYDvEPmdhbdaknzxNA3-4Al2d_JFnP6IlH9cApY45f3ohEbriGAzRGVmnizG1iE8bV9MK66Dde9wCektrTCHCD8wKmRkimgOJQyfzwWRfMljKUlcN3TE0k&ct=4&gpp_sid=7&cv=CPUCbs9P...&gpp=DBABLA~...&atype=2";
NSURL *url = [NSURL URLWithString:refreshURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"GET";
[request setValue:@"com.example.app" forHTTPHeaderField:@"origin"];
[request setValue:@"application/json" forHTTPHeaderField:@"accept"];
[[NSURLSession.sharedSession dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSString *body = data ? [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] : nil;
}] resume];
When you call the Refresh Envelope API on an expired envelope; you will receive a 204 response. In this case, you'll need to discard the expired envelope and fetch another envelope with the ATS Envelope API.