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.

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. See "Call the ATS Envelope API" for more details on each required parameter.

🚧

Make sure to only call the LiveRamp API with a positive consent string for users in the EU.

📘

In the origin header of your ATS requests, you must include a bundle ID that is tied to an approved Placement ID.

Android

val client = OkHttpClient()

val url = "https://api.rlcdn.com/api/identity/v2/envelope?pid=14&it=4&iv=YOUR_HASHED_IDENTIFIER&atype=4&ct=4&cv=YOUR_TCF_STRING&gpp_sid=7&gpp=YOUR_GPP_STRING"
val request = Request.Builder()
    .url(url)
    .get()
    .addHeader("origin", "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=4&ct=4&cv=YOUR_TCF_STRING";
Request request = new Request.Builder()
    .url(url)
    .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;

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 = 4       // 4 = app
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("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 = 4;

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:@"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 Integrations

When retrieving or refreshing the ATS Envelopes, publishers integrating server-to-server are required to include the X-Forwarded-For header 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 the createdAt timestamp 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 the lastRefreshTime exceeds 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.

ColumnTypeDescription
idLongPrimary key (auto-increment)
envelopeStringThe LiveRamp primary ATS envelope (type 19).
envelope24StringThe Meta envelope (type 24).
envelope25StringThe PAIR envelope (type 25).
envelope26StringThe ATS Direct envelope (type 26).
envelope27StringThe Google SSP envelope (type 27).
lastRefreshTimeLongTimestamp (in milliseconds) of the most recent envelope refresh.
createdAtLongTimestamp (in milliseconds) when the envelope was first generated.
userId (optional)Identifier needed if you store multiple envelopes, one for each user.
📘

Returning Stored Envelopes

For 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=4")
    .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=4")
    .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: "4")
]

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=4";
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.

7. Pass Envelopes into Monetization Workflows

Once ATS is set up, you can now configure the programmatic advertising solution to pass your envelopes into downstream workflows and monetize your inventory. LiveRamp is working with several partners that support setting identity envelopes in their SDKs, and passing them into their programmatic bidstreams.

Prebid (SDK)

You can pass envelopes a Prebid server instance from the Prebid iOS/Android SDK by adding the envelope as an ExternalUserId .

externalUserIdArray.add(new ExternalUserId("liveramp.com", "<liverampEnvelope>", null, null));
externalUserIdArray.append(ExternalUserId(source: "liveramp.com", identifier: "<liverampEnvelope>"))

For more information, see the Prebid SDK documentation for iOS or Android SDK.

InMobi

InMobi allows in-app publishers to pass identity envelopes into InMobi's exchange using their UnifID service. There are two ways you can pass envelopes to InMobi:

  • Use the ATS Envelope API to generate the envelopes and pass them to InMobi's UnifID service using setPublisherProvidedUnifiedId.
val unifiedIds = JSONObject().put("liveramp.com", envelope)
InMobiSdk.setPublisherProvidedUnifiedId(unifiedIds)
var idDictionary = ["liveramp.com": envelope]
IMSdk.self.setPublisherProvidedUnifiedId(idDictionary)
  • Use InMobi’s own SDK to generate and pass envelopes. See InMobi's SDK Specifications to learn how.

Pubmatic

iF you are working with Pubmatic, you can pass envelopes through Pubmatic OpenWrap SDK to configured OpenWrap partners.

// Set the envelope here
// It MUST be liveramp.com to work
var userId = POBExternalUserId(source: "liveramp.com", andId: "<liverampEnvelope>")
 
 
// Add the ID to OW SDK 
OpenWrapSDK.addExternalUserId(userId)
// Set the envelope here
// It MUST be liveramp.com to work
POBExternalUserId adServerUserId = new POBExternalUserId("liveramp.com","<liverampEnvelope>");


// Add the ID to OW SDK 
OpenWrapSDK.addExternalUserId(adServerUserId);

See the following documentation in Pubmatic's portal to learn more (login to Pubmatic required):

Nimbus

Nimbus SDK allows you to pass envelopes to Nimbus' exchange. There are two ways you can do this:

  • Pass envelopes directly to Nimbus SDK. For example:

    NimbusAdManager.addExtendedId(
          source = "liveramp.com",
          id = envelope,
          extensions = mapOf("rtiPartner" to "idl"),
    )
     var extendedId = NimbusExtendedId(source: "liveramp.com", id: envelope)
    extendedId.extensions = ["rtiPartner": NimbusCodable("idl")]
    
    /* Setting extendedIds more than once will clear the previous results and is nil by default */
    NimbusAdManager.extendedIds = [extendedId]
  • Use the Nimbus-LiveRamp extension to generate and pass envelopes. For more information, see Nimbus' documentation for Android and iOS.

MobileFuse

MobileFuse's SDK allows you to pass RampID Envelopes to MobileFuse's exchange - MFX.

There are two ways you can do this:

  • Pass envelopes directly to MobileFuse using MobileFuseTargetingData:
// Pass in a RampID Envelope here
[MobileFuseTargetingData setLiveRampEnvelope: @"<liverampEnvelope>"];
// Pass in a RampID Envelope here
MobileFuseTargetingData.setLiveRampEnvelope(“<liverampEnvelope>”)
  • Use MobileFuse's SDK to generate and pass identity envelopes. See MobileFuse's documentation to learn how or reach out to your MobileFuse account manager.

AppLovin MAX (SDK)

You can pass envelopes to AppLovin MAX fto AppLovin MAX by adding the RampID Envelope as an extra parameter in AppLovinSdkSettings:

AppLovinSdkSettings settings = AppLovinSdk.getInstance( context ).getSettings();
settings.setExtraParameter( "liveramp.com", "<liverampEnvelope>" );
let settings = ALSdk.shared()?.settings
settings?.setExtraParameterForKey("liveramp.com", value: "<liverampEnvelope>")

For more information, see AppLovin's MAX SDK documentation on how to pass identifier tokens for iOS or Android.