Enhanced Attribution: Server-to-Server Implementation v1.0¶
Abstract¶
Describes Yahoo Ad Tech’s new Enhanced Attribution implementation and outlines how the feature works, end-to-end, with step-by-step instructions for activating and operating this solution.
Overview¶
Given the current state of the industry, third-party cookies may no longer be available for advertising purposes, owing to increased online privacy regulations and additional tracking protections implemented by browsers and other devices. The blocking of cookies will limit conversion measurement and attribution for both browsers and devices.
Yahoo Ad Tech’s Enhanced Attribution solution mitigates the lost conversion attribution on browsers and devices through the use of a click ID as an identifier instead of a cookie or device ID.
This solution enables tracking of conversions that can work without identity synchronizations and also puts advertisers in control of monitoring the performance of their ad buys. The mechanism can provide post-click attribution for advertisers, and is available for both DSP and Native Ad platform campaigns.
How It Works¶
Enhanced attribution works by using a click ID as an identifier when browsers or devices block third-party cookies. Once you enable this mechanism, a Yahoo Ad Tech click ID macro (${CC}
) is added to the click-through URL, and is expanded with a unique value for that click.
To activate this feature (i.e., passing the click ID), code is appended to ad tags or click trackers that includes the parameter vmcid=
and a click ID macro "${CC}
.
The process flow is illustrated below:
The vmcid
parameter and the click ID pass these values to the landing page URL, which includes a Dot JavaScript tag after a user clicks on an ad. The vmcid click ID is then captured by the Dot JavaScript tag on the page; then stored in the site’s first-party cookie and the browser’s local storage until the user eventually converts on the site within the lookback window controlled by the browser. (Currently, this lookback window is 24 hours for first-party cookies and seven days for local storage.)
Upon the conversion event, the vmcid click ID is then passed back to the Yahoo DSP or Yahoo Native for attribution.
Important
Enhanced Attribution adheres to all privacy regulations because the click event is not tied back to a user but instead only connects the conversion to a click event.
A server-to-server integration with Yahoo Ad Tech is performed by sending the click ID value from a landing page when a conversion happens.
Important
Server-to-server integration requires OAuth 2.0 authentication, which is described in the sectins below. Once the authentication is successful, the click ID-based conversions can be received on the following endpoint:
https://aaca.verizonmedia.com/ --data "id=id123&vmcid=${INSERT_VMCID_COLLECTED_FROM_CLICK}&dp=simple_dp&gv=1
The supported parameters in the URL are described in the table below.
Integration Process¶
The integration process and flow is outlined in the steps below:
1 - Authentication¶
To take advantage of this solution, you must authenticate using OAuth 2.0. See the authentication implementation section outlined below for details on what an OAuth 2.0 solution would look like.
2 - Conversion tracking implementation¶
Once authentication is implemented, attributed conversion data can be posted to the endpoint as the advertisers or partners system generates the attribution data. To pass data to the endpoint, follow the conversion tracking implementation steps outlined in the section below.
3 - Data flow¶
Once conversion data begins flowing, data posted to this endpoint will be reportable in Yahoo DSP and Yahoo Native ad platforms as a conversion metric. This data will flow into CPA optimization and will also power conversion metrics such as dynamic conversion values and ROAS.
Known Limitations¶
Segmentation¶
There is no capability to segment exposed users or clickers for use cases such as retargeting, exclusion, or lookalike modeling.
Data uploads¶
At this time, this solution only supports API upload; there is no batch upload mechanism currently available.
Reporting time¶
All conversion attributions are reported at upload time, not conversion time or impression/click time.
Conversion Tracking Implementation¶
Creative instrumentation¶
Update tracker or click URLs to include the relevant macros to pass along the ID to the advertiser tracking systems. Other macros available are documented at https://developer.verizonmedia.com/dsp/docs/macros/macros.html.
Examples:
Click Tracker Description |
Example URL |
---|---|
Generic example |
https://advertiser.clickserver.com?vmcid=${CC}&measurement_partner_required_param_A=${relevant_macro}&measurement_partner_required_param_B=SOME_FIXED_VALUE&……..INSERT OPTIONAL PARAMS……. |
Branch IO |
https://branchster.app.link/EXAMPLE_BRANCH_APP_ID?$3p=a_vm_network&~click_id=${CC}&~secondary_publisher_id=${pubid} |
Implement a process or script to collect this ID value and associate it with your conversion data.
Rules setup¶
To ensure that this ad spend is measured by Yahoo Ad Tech’s attribution system, create a conversion rule for the Yahoo Native ad campaign or Yahoo DSP line.
Conversion data postback¶
Once the conversion data is available, fire it to Yahoo Ad Tech’s API:
For example:
a) curl --tlsv1.2 -w %{http_code} https://aaca.verizonmedia.com/?id=id123\&vmcid=simple_click_id\&dp=simple_dp\&gv=10.0
b) curl --tlsv1.2 -w %{http_code} https://aaca.verizonmedia.com/ --data "id=id123&vmcid=${INSERCT_VMCID_COLLECTED_FROM_CLICK}&dp=simple_dp&gv=10.0" --header "Content-Type: application/x-www-form-urlencoded"
Required and supported parameters¶
The required and supported parameters are described in the table below:
URL Parameter Name |
Description |
Required? |
Default |
Example |
---|---|---|---|---|
vmcid |
Value of the pixel context macro collected from the click or the third-party ad server. |
yes |
N/A |
See the integration guide |
id |
Unique event id provided by the partner identified in dp. This is a technical mechanism used by Yahoo Ad Tech to prevent duplication in reporting. |
yes |
N/A |
Should be unique guids. Used to log the event for tracking. |
dp |
The entity responsible for passing the data to Yahoo Ad Tech. |
yes |
N/A |
“branch” |
et |
Business Time of the Event (Epoch UTC milliseconds). |
no |
The receive time of the event is used if parameter is not present. |
1585604630466 |
gv |
The numerical value of any given conversion. By default, the value is assumed to be USD. This will be reported as a dynamic conversion value in DSP and calculated as ROAS in Native. |
no |
None |
12.25 |
gc |
Event Value Currency |
no |
None |
|
ec |
Event Category |
no |
None |
|
ea |
Event Action |
no |
None |
|
.yp |
pixel id |
no |
None |
Maps to a pixel ID - if not provided then reporting will only be on the line/campaign level. |
Other parameters |
no |
N/A |
Not to exceed name length of 32 nor value length of 255. |
API Return Codes¶
Code |
Message |
Reason |
---|---|---|
200 |
Submission processed. |
Valid Request |
400 |
Error. Unsupported Content-Type. |
Invalid Content-Type provided. |
400 |
Error. Unsupported Content-Type for request body. |
Request has body but no Content-Type provided. |
400 |
Error. Missing body and no query parameters provided. |
Missing query params and body. |
400 |
Error. Request body/params formatting error. |
Unable to parse request body/params. Failed to decode KVs in request body. Failed to decode KVs in query params. |
400 |
Error. Request does not match specs. |
Key longer than maximum 32 characters. Value longer than maximum 255 characters. Missing required field ‘id’. Missing required field ‘vmcid’. Missing required field ‘dp’. NumberFormatException for keys: et or gv. |
401 |
Error. Invalid ‘Authorization’ HTTP Header. Request a new token. (Not enforced in the initial release). |
Invalid Authorization token. |
500 |
Internal Server Error |
Additional Specs¶
Scenario |
Message/Code |
---|---|
Users either put all of their KVs in the request body or put all of them in the query parameters (not split between them). |
If both are provided, the body is processed only. |
If KVs are provided as ‘query parameters’, Content-Type must be “application/x-www-form-urlencoded” if specified |
Else it will send code 400 (Error. Unsupported Content-Type.) |
If KVs are provided in ‘request body’, Content-Type must be “application/x-www-form-urlencoded” |
Else it will send code 400 (Error. Unsupported Content-Type for request body.) |
Keys and Values must be encoded for both “query parameters” and “request body”. |
Else it will send code 400 (Error. Request does not match specs.) |
Authentication Implementation¶
Summary¶
OAuth 2.0 is a mechanism that relies on continuously generating authentication tokens and then providing those tokens during the posting of data.
Note that Yahoo Ad Tech has no plans to support OAuth 1.0, which depends on static tokens.
Requesting Client Credentials¶
To complete the steps below, you will first need a Client ID and a Secret. Follow the below steps to get them.
Generate a private key.
>> openssl genpkey -aes256 -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private_key.pem
Generate a public key using the above private key.
>> openssl rsa -in private_key.pem -out public_key.pem -outform PEM -pubout
Send us the public key.
We send a file containing credentials encrypted with the above public key to you.
Decrypt the file with the private key.
>> openssl rsautl -decrypt -inkey private_key.pem -in credential.enc -out my_credentials.txt
Security Considerations¶
Be sure to keep your Secret secure. If you want to reset Secret or forget your Secret, follow the instructions above to get new credentials.
Important
It is critical to ensure that the Secret is protected and NEVER exposed. All interactions MUST be protected by TLS. Do not embed the Secret directly in code to avoid being accidentally exposed to the public. Instead of embedding your Secret in the applications, store them in environment variables or in files outside of your application’s source tree.
It’s recommended that you reset your Client ID/Secret periodically.
Important
If the credentials are compromised at any point, it is very important to reset your Client ID/Secret pair.
Generate a JSON Web Token (JWT)¶
To generate an access token, you’ll need to generate a JWT.
A JSON Web Token is composed of three main parts:
Header: normalized structure specifying how token is signed (generally using HMAC SHA-256 algorithm).
Free set of claims embedding whatever you want: client_id, aud, expiration date, etc.
Signature ensuring data integrity.
The signature mechanism is HMAC_SHA256 as defined by the JOSE specifications at <https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31)>
JWT Header¶
{
"alg": "HS256",
"typ": "JWT"
}
JWT Claims¶
{
"aud": "{protocol}://{b2b.host}/identity/oauth2/access_token?realm=aaca",
"iss": "{client_id}",
"sub": "{client_id}",
"exp": {expiry time in seconds},
"iat": {issued time in seconds},
}
Note
The exp
and iat
values should be numeric. Don’t set them as strings. The exp
value should be less than 24 hrs. Preferable time is currentTime + 600 (i.e., 10 minutes). Don’t use currentTime + (24 * 60 * 60). You may get the JWT is has expired or is not valid error. urn:vm:claims:fedidp_tenant
is an optional value. You need to pass this only if you need to do token exchange using a federated token.
JWT Signature¶
jwt_signing_string = base64url_encode(jwt_header) + '.' + base64url_encode(jwt_body);
jwt_signature = base64url_encode(hmac_sha256(jwt_signing_string, client_secret))
JWS = jwt_signing_string + '.' + jwt_signature
Walking through the manual steps to build this JWT value:
jwt_header = '{"typ":"JWT","alg":"HS256"}';
jwt_body = '{
"iss":"client_id",
"sub":"client_id",
"aud":"https://id.b2b.verizonmedia.com/identity/oauth2/access_token?realm=aaca",
"exp":<expiry-time-in-seconds>,
"iat":<issued-time-in-seconds>}';
jwt_signing_string = base64url_encode(jwt_header) + '.' +
base64url_encode(jwt_body);
jwt_signature = base64url_encode(hmac_sha256(jwt_signing_string,
client_secret))
JWS = jwt_signing_string + '.' + jwt_signature
A Final JWT token looks like this:
ew0KICAiYWxnIjogIkhTMjU2IiwNCiAgICJ0eXAiOiAiSldUIg0KfQ.ew0KICAiYXVkIjogIntwcm90b2NvbH06Ly97YjJiLmhvc3R9L2lkZW50aXR5L29hdXRoMi9hY2Nlc3NfdG9rZW4/cmVhbG09PHlvdXItcmVhbG0+IiwNCiAgImlzcyI6ICJ7Y2xpZW50X2lkfSIsDQogICJzdWIiOiAie2NsaWVudF9pZH0iLA0KICAiZXhwIjog4oCce2V4cGlyeSB0aW1lIGluIHNlY29uZHN94oCdLA0KICAiaWF0Ijog4oCce2lzc3VlZCB0aW1lIGluIHNlY29uZHN94oCdDQp9DQo.uKqU9dTB6gKwG6jQCuXYAiMNdfNRw98Hw_IWuA5MaMo
<base64url-encoded header>.<base64url-encoded claims>.<base64url-encoded signature> (They are separated with a “.”)
Sample codes to generate JWT and get an access token are provided below.
Request for an access token¶
Make this POST call:
POST https://id.b2b.verizonmedia.com/identity/oauth2/access_token
Note
The Request POST format requires application/x-www-form-urlencoded.
OAuth2 Client Credentials¶
This API uses the OAuth2 client_credentials flow and identifies the client via a signed JSON object which will need to be created and included in the client_assertion
argument in the request.
Arguments
Field Name |
Required |
Description |
---|---|---|
|
Yes |
MUST be |
|
Yes |
MUST be |
|
Yes |
JWS value (varies for each client request). |
|
Yes |
MUST be |
|
Yes |
MUST be |
Example
Request
POST /identity/oauth2/access_token HTTP/1.1
Host: https://id.b2b.verizonmedia.com
Content-Type: application/x-www-form-urlencoded
Accept: application/json grant_type=client_credentials&scope=upload&realm=aaca&client_assertion_type=urn:ietf:params:o auth:client-assertion-type:jwt-bearer&client_assertion=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc 3MiOiJkNjI0YmI4My03MzViLTRmNTMtYjU1Ni03YTEzMGM5YzAxZjMiLCJzdWIiOiJkNjI0YmI4My03Mz ViLTRmNTMtYjU1Ni03YTEzMGM5YzAxZjMiLCJhdWQiOiJodHRwczovL2lkLXVhdDIuY29ycC5hb2wuY 29tL2lkZW50aXR5L29hdXRoMi9hY2Nlc3NfdG9rZW4_cmVhbG09YjJiIiwiaWF0IjoxNDc1MDk1Mjg1Ljk 1NCwiZXhwIjoxNDc1MDk1NTg1Ljk1NCwicmVhbG0iOiJiMmIifQ.JzeW4YvrN7HC1nAcrj21_9yn2i3Iq9b abpTmbNuPfcM
Response
success
Format: json Status: 200 Headers: Content-Type: application/json
{
"access_token": "3f94eb47-a295-4977-a375-e27bea5c828b",
"scope": "upload",
"token_type": "Bearer",
"expires_in": 599
}
Note
The token remains active for 10 minutes, so be sure to re-use the token instead of requesting a new token for every postback. Also, the token can be refreshed/regenerated at around 8-9 minutes instead of waiting for the 10 minutes.
Putting access token in the request header
You need to put your access token in the request header to invoke AACA APIs. The header name is Authorization and the value is access token.
Example format:
GET /?id=id123&vmcid=simple_click_id&dp=simple_dp&gv=10.0 HTTP/1.1
Host: https://aaca.verizonmedia.com
Authorization: 3f94eb47-a295-4977-a375-e27bea5c828b
Sample Code for Token Generation
Java
package sample.aaca;
import com.google.gson.Gson;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
public class JavaSample {
private static String oAuthURL = "https://id.b2b.verizonmedia.com/identity/oauth2/access_token";
private static String scope = "upload";
private static String realm = "aaca";
private String clientId = "//Insert ClientId here";
private String clientSecret = "//Insert ClientSecret here";
public static final long ACCESS_TOKEN_TTL = 600000;
private String generateJsonWebToken() throws UnsupportedEncodingException {
final HashMap<String, Object> claims = new HashMap<>();
long nowMillis = System.currentTimeMillis();
long expMillis = nowMillis + ACCESS_TOKEN_TTL;
claims.put("iss", clientId);
claims.put("sub", clientId);
claims.put("aud", oAuthURL + "?realm=" + realm);
claims.put("exp", expMillis / 1000);
claims.put("iat", nowMillis / 1000);
JwtBuilder jwtBuilder = Jwts.builder().setClaims(claims);
return jwtBuilder.signWith(SignatureAlgorithm.HS256, clientSecret.getBytes("UTF-8")).compact();
}
private Response getTokenFromAuthServer(String assertion) throws IOException {
Response response = null;
try (CloseableHttpClient httpClient = HttpClientBuilder.create()
.setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build()).build()) {
StringBuilder payload = new StringBuilder().append("scope=").append(scope).append("&grant_type=")
.append("client_credentials").append("&client_assertion_type=")
.append("urn:ietf:params:oauth:client-assertion-type:jwt-bearer").append("&realm=").append(realm)
.append("&client_assertion=").append(assertion);
HttpPost request = new HttpPost(oAuthURL);
StringEntity body = new StringEntity(payload.toString(), ContentType.APPLICATION_FORM_URLENCODED);
request.setEntity(body);
request.addHeader("Accept", ContentType.APPLICATION_JSON.toString());
System.out.println("Starting token request..........");
HttpResponse result = httpClient.execute(request);
System.out.println("Token request completed.......... " +
result.getStatusLine().getStatusCode() + " " +
result.getStatusLine().getReasonPhrase());
String json = EntityUtils.toString(result.getEntity(), "UTF-8");
Gson gson = new Gson();
response = gson.fromJson(json, Response.class);
}
return response;
}
/**
* Get token from server.
* @return token generated.
* @throws Exception Throws exception if connection issues or encryption issues.
*/
public String getToken() throws Exception {
String assertion = generateJsonWebToken();
Response response = getTokenFromAuthServer(assertion);
String token = response.getAccessToken();
return token;
}
/**
* Helper class representing the json response from the IDB2B server
*/
public static class Response {
/**
* String with token value received from
* the IDB2B server
*/
private String access_token;
public Response() {}
public Response(String access_token) {
this.access_token = access_token;
}
public void setAccessToken(String access_token) {
this.access_token = access_token;
}
public String getAccessToken() {
return access_token;
}
}
public static void main(String[] args) {
JavaSample tokenGenerator = new JavaSample();
String assertion;
Response response;
try {
assertion = tokenGenerator.generateJsonWebToken();
response = tokenGenerator.getTokenFromAuthServer(assertion);
System.out.println(response.getAccessToken());
} catch (Exception e) {
System.out.println("Exception occured..." + e.getMessage());
}
}
}
Troubleshooting¶
Invalid client error - JWT is not valid.
You can see the invalid client if the JWT assertion is not correct. The reasons can be that JWT expired or is invalid, audience wrong, etc. Also, the client id is not found or client_id
, secret are invalid.
If you see the JWT expired error below, then ensure the jwt claim values exp
and iat
are correct. Both values should be in seconds (EPOCH time) and exp
should be in the future, but it should be less than sthe erver side configured time (i.e., 24 hrs).
{
"error_description": "JWT is has expired or is not valid",
"error": "invalid_client"
}
Invalid client error - Client authentication failed¶
If you see this error
{
"error_description": "Client authentication failed",
"error": "invalid_client"
}
then perform the following checks:
Ensure the
realm
value is correct.Ensure
client_id
,client_secret
used in JWT are correct.Ensure
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
is correct. Check for typos or any hidden special characters in the values.Log request and see whether you are seeing all endpoints, param names and values properly. Check the url encoded values to ensure they are correct.
Ensure you are hitting the correct endpoint.
If you still can’t find the reason, then delete static values for
grant_type
,client_assertion_type
, scope, realm, etc. and re-add manually just to avoid any copy paste resulting in invisible special characters.
Invalid request¶
If you see this error below, then check that grant_type
is set and the value is client_credentials
.
{
"error_description": "Grant type is not set",
"error": "invalid_request"
}
Invalid scope¶
If you see this error below, then check that the scope is set correctly.
{
"error_description": "Unknown/invalid scope(s): [open]",
"error": "invalid_scope"
}
Server error¶
If the data is not in the expected format or the flow is not supported or some other reason, then you may see this error:
{
"error_description": "Client authentication failed",
"error": "invalid_client"
}
Perform these checks:
Ensure all requested parameters are passed.
No typos in parameters. All parameters are in lowercase.
Check the format of values like JWT(includes header, claims, signature). No truncation, etc.
client_assertion_type
andclient_assertion
are must.Ensure
exp
andiat
in JWT claims are numeric values. Don’t set them as strings.
Document History¶
First release, v1.0 of Enhanced Attribution Implementation document on 06/02/2021.