苹果ASA归因对接以及API接入

一、归因概要

广告归因,目的是用于衡量广告带来的激活用户的成本以及后续进一步的用户质量表现。 Apple Ads 广告平台是基于 App Store(站内广告),同时属于自归因平台(通常称为 SAN)。这两个因素,决定了 ASA 与其他大部分广告平台(站外广告)的区别。 ASA 广告投放前,无需创建投放链接、监测链接,ASA 归因由 Apple 自身完成,可以保证用户隐私同时还能做到用户级归因 ,目前获取归因数据可采用自归因或第三方归因的方式。

以下是该方案的要点和广告主应对措施:

  • 此归因方案仅适用 Apple Search Ads 广告;仅支持 iOS 14.3 及更高版本;14.3 之前的版本,需使用 iAd Framework

    • 从 2023 年 2 月 7 日开始,iAd 框架停止用于归因 Apple Ads 的广告安装。

    • 所有通过iAd Framework发送的请求都会收到 "iad-attribution"=false的错误,具体请查看iAd Changelog | Apple Developer Documentation

    • 开发者需实施 AdSercices 框架以归因来自 Apple Ads 的广告安装。此框架于 2021 年 1 月发布,如果已完成实施、或使用主流 MMP 的开发者无需做更改。

    • AdServices[3] 仅支持 iOS 14.3 及更高版本的设备。

    • 届时 14.2 及更低旧版本的设备将无法归因苹果广告安装。

  • 此方案将极大提高 ASA 广告安装的激活率

  • 安装到激活的差距逐渐降至极低

  • 此方案涉及前后端的系统开发,需自己归因的开发者应尽早制定计划、安排实施

  • 使用 MMP 服务的开发者,与您的服务提供商沟通,了解其SDK对此方案的支持进度

  • 无论开发者自己实施还是采用 MMP,均需要发布新的app版本

  • 针对 iOS 14 及更高版本的设备,LAT 的概念已失效,受众的年龄和性别定向不再排除限制跟踪的用户

  • 什么是ASA?

      ASA(Apple Search Ads),也叫ASM(App Store Marketing)---应用商店广告,即用户在App Store 搜索应用时出现在搜索结果上方的广告。

      搜索广告有三种展现形式:标题+icon+截图/描述/视频, App 在搜索广告上的展示内容与其在自然搜索排名下的内容相同,开发者不必也不能单独为搜索广告设置素材,且无法选择或设置广告具体以哪种方式展现;

  • ASA广告竞价逻辑

ASA广告类似谷歌搜索广告,属于竞价广告模式,以点击计费;

广告展现量受5个因素影响,分别是相关性、出价、关键词热度、竞争者、投放位置;广告单价受3个因素影响,分别是相关性、出价、竞争者;

1)相关性(关键词和APP之间的相关性):一个关键词和这个 App 之间的相关性是由这个 App 的元数据和用户 行为所决定的;元数据即 App 的标题、icon、截图、描述等。

2)出价:在其他参数不变的情况下,价高者得;

其他影响因素:产品权重(安装总量/总榜/分类榜单越高,是受到用户喜爱得产品)、关键词热度(aso吸引力-100关键词的覆盖)

二、归因实施说明

归因业务流程图

4cc231f0a6354af0ad35fbf815004a6c.png

这个归因方案,包含两部分:客户端的 AdServices 框架,和从苹果Search Ads归因服务器获取归因数据的 RESTful API。

下图说明了结合使用 AdServices 框架和 RESTful API 来完成归因。

2e6a719abe614bd89afc79934d49f596.png

Request Token 获取 token

  • (NSString *)attributionTokenWithError:(NSError * _Nullable *)error;

AdServices 框架返回的 token 为字符串类型,并且只有 24 小时有效期。

AAAttributionErrorCode 归因错误枚举值 

typedef enum AAAttributionErrorCode : NSInteger {// 没有返回 token,网络不可用。AAAttributionErrorCodeNetworkError = 1,// 没有返回 token,发生了内部错误。AAAttributionErrorCodeInternalError = 2,// 没有返回 token,操作系统平台不支持。AAAttributionErrorCodePlatformNotSupported = 3
} AAAttributionErrorCode;

Request attribution 获取归因数据

您可以将 token 提供给 MMP,或者使用该 token 在 24 小时内进行 POST API 调用,以获取归因数据。在请求正文中带上 token:

POST https://api-adservices.apple.com/api/v1/
yourtokenyourtoken

Response Codes 返回状态码

Response状态码Description描述
200成功。API 找到了匹配的归因记录,返回值包含 attribution=true。如果API没有找到对应的归因记录,返回值为 attribution=false。在这种情况,状态码 200 仅表示服务器有数据返回。
400token 无效。
404没有找到。API 无法获取到请求的归因记录。Tokens 只有 24 小时的有效期。如果请求超过了 24 小时,苹果会返回 404 状态码。如果 token 是有效的,一个最佳实践:最多重试 3 次,每次间隔 5 秒钟。
500服务器暂时关闭或无法访问。请求可能是有效的,但是你需要选择合适的时间点进行重试。

代码示例:

- #import <AdServices/AdServices.h>
(void) methodToGetToken {if (@available(iOS 14.3, *)) {NSError *error;NSString *token = [AAAttribution attributionTokenWithError:&error];if (token != nil) {// 发送POST请求归因数据}} else {// Fallback on earlier versions}
}
- (void) attributionWithToken:(NSString *)token {NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];NSURL *url = [NSURL URLWithString:@"https://api-adservices.apple.com/api/v1/"];NSMutableURLRequest 
request = [NSMutableURLRequest requestWithURL:urlcachePolicy:NSURLRequestUseProtocolCachePolicytimeoutInterval:60.0];[request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"];[request setHTTPMethod:@"POST"];
*  NSData*postData = [token dataUsingEncoding:NSUTF8StringEncoding];[request setHTTPBody:postData];NSURLSessionDataTask *postDataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {NSError *resError;NSMutableDictionary *resDic = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&resError];}];[postDataTask resume];
}

 

字段类型说明
attributionBoolean如果用户在应用下载前 30 天点击了 App Store、Apple News 以及 Stocks,则其值为 true。如果 API 找不到匹配的归因记录,则为false。
orgIdInteger广告系列所属的账户 ID。
campaignIdInteger广告系列 ID。
conversionTypeString表明是否首次下载。"Redownload" 说明用户在本设备下载/卸载过,或者用同一账户在其他设备下载过。
clickDateDate/time string用户点击相应广告的日期和时间。此字段仅出现在详细归因数据包中。
adGroupIdInteger广告组 ID。
countryOrRegionString国家或地区。
keywordIdInteger关键字的 ID。
creativeSetIdInteger广告素材集的 ID。

 

这里只要是attribution=true,就意味着用户是通过点击广告下载的app。反之会返回false。原则是用户在下载app之前,30天内是否有广告点击的记录。所以客户如果只想判断用户是不是通过广告带来的,只看这个字段就可以,=true的情况下click date一定是在30天内的。

conversiontype是用来判断用户是首次安装还是重新安装,对于判断用户是否来自广告投放没有太大的关系。

客户端调用AdService框架

  • AdServices framework发起调用请求生成 Token 。

  • AdServices framework生成Token 返回(Token有效期24小时)。更多详请,请参阅 attributionToken

  • 使⽤Token向Attribution API请求结果

  • Attribution API返回true or false,判断该客户是否点击过苹果搜索广告

    • True: ⽤户在过去30天内曾经点击过苹果广告,记录最近一次用户点击广告

    • False: ⽤户在过去30天没有点击过苹果广告(用户为IOS14.3以下版本请求Token就会报错)

  • MMP 或开发⼈员使⽤ Token 发起 RESTful API 获取归因记录请求,苹果的归因服务器响应请求。更多详请,请参阅Attribution Payload。

  • API 返回的归因数据中的键值与Apple Ads ⼴告系列管理 API 中的⼴告系列的字段相对应。更多详情,请参阅 Attribution Payload Descriptions。

*苹果官网参考文档:https://developer.apple.com/doc

三、ManagementAPI接⼊具体流程

授权广告主账户对应广告系列组“API只读”

  • 需要广告主提供一个开发者账号AppleID(即邮箱)

  • 授权后需要甲方到邮箱查收邀请邮件,并点击邮件中链接确认授权

开发者生成公钥+私钥,准备上传到ads.apple.com后台

  • 生成公钥私钥:

  • 如果您使用的是MacOS或类似UNIX的操作系统,OpenSSL可以原生运行。如果您使用的是Windows平台,则需要下载OpenSSL。

  • openssl ecparam -genkey -name prime256v1 -noout -out private-key.pemopenssl ec -in private-key.pem -pubout -out public-key.pem

    上传公钥私钥,生成clientid、keyid、teamid

  • 登录被授权的账号,查看右上⻆设置d41535b27c864ee29f84e7141c4fbbb1.png

  • 粘贴前⾯⽣成的公钥进去,点击保存按钮;并记录⽣成的clientid、keyid、teamid

  • 使⽤新⽣成的clientid、keyid、teamid,⽣成client_secret

41ff03a0d842462eb946cf38dad24df3.png

  • 生成出来的ID示例:

clientId SEARCHADS.aeb3ef5f-0c5a-4f2a-99c8-fca83f25a9
teamId SEARCHADS.hgw3ef3p-0w7a-8a2n-77c8-scv83f25a7
keyId a273d0d3-4d9e-458c-a173-0db8619ca7d7
  • 使⽤3个id⽣成client_secret的示例

import jwt
import datetime as dt
client_id = 'SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577'
team_id = 'SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577' 
key_id = 'bacaebda-e219-41ee-a907-e2c25b24d1b2' 
audience = 'https://appleid.apple.com'
alg = 'ES256'
Define issue timestamp.
issued_at_timestamp = int(dt.datetime.utcnow().timestamp())
Define expiration timestamp. May not exceed 180 days from issue timestamp.
expiration_timestamp = issued_at_timestamp + 86400*180 
Define JWT headers.
headers = dict()
headers['alg'] = alg
headers['kid'] = key_id
Define JWT payload.
payload = dict()
payload['sub'] = client_id
payload['aud'] = audience
payload['iat'] = issued_at_timestamp
payload['exp'] = expiration_timestamp
payload['iss'] = team_id 
Path to signed private key.
KEY_FILE = 'private-key.pem' 
with open(KEY_FILE,'r') as key_file:key = ''.join(key_file.readlines())
client_secret = jwt.encode(
payload=payload,  
headers=headers,
algorithm=alg,  
key=key
)
with open('client_secret.txt', 'w') as output:output.write(client_secret.decode("utf-8"))
  • JWT请求头和请求data示例

Header
{
"alg": "ES256",
"kid": "bacaebda-e219-41ee-a907-e2c25b24d1b2"
}
Payload
{
"iss": "SEARCHADS.hgw3ef3p-0w7a-8a2n-77c8-scv83f25a7", # 您上一步获得的teamId
"iat": 2234567891, # 创建客户端密钥时的 UNIX UTC 时间戳
"exp": 2234567900, # 客户端密码过期的 UNIX UTC 时间戳。该值必须大于当前时间
"aud": "https://appleid.apple.com",
"sub": "SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577" # 您上一步获得的client_secret
}
  • 请求⽣成的client_secret示例

eyJraWQiOiJiYWNhZWJkYS1lMjE5LTQxZWUtYTkwNy1lMmMyNWIyNGQxYjIiLCJhbGciOiJFUzI1NiJ9.
eyJpc3MiOiJEcmVhbWNvbXBhbnkiLCJhdWQiOiJBdXRoZW50aWNhdG9yIiwiZXhwIjoxNTcxNjcwNjIx
LCJuYmYiOjE1NzE2NjcwMjEsInN1YiI6Im11c3RlciIsImNsaWVudF9pZCI6ImFiY2QxMjM0IiwiYWRt
aW4iOiJ0cnVlIn0.s4C3p9kVNFeRAB5tChatC3ldQX07v9mG7thL7FeEO6cClfNuiaLSgq8f8ymbfO3O
QYW_KuwaA1KYRuoy1JmKk 4DBbYLcz6aoABe0pzI5Z_6wgMzAyqz8pQtwDAcd4Idoi8JdRbtzZce9o-0
nZiFA4hVAXqYwpEYC4UU8ZmJO_z8tY4juHPTV3nDugdtqyNnmAiBoLryOfGNngQZccdY1_QvkXS1y0bg1
a0k8cVVtnq- _93fYJIt9Z64CTvlH3uOeh7uaEv3nIxpXhvhkTySpUmY8e04TO09oTyZijiloByv3KFQ9
2OOJ8L 5N5_CeEc5p9LWjT1pcX8ATamOycZz2Q

使⽤client_secret,请求苹果接⼝⽣成access_token

access_token存在过期的情况,通常为1小时,需要定时维护

  • 请求获取 access_token 示例

curl -X POST \
-H 'Host: appleid.apple.com' \
-H 'Content-Type: application/x-www-form-urlencoded' \
'https://appleid.apple.com/auth/oauth2/token?grant_type=client_credentials&
client_id=SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577&client_secret=eyJ0
eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.zI1NiIsImprdSI6Imh0dHBzOi8vYXV0aC5kZXYuYXB
pLnJpY29oL3YxL2Rpc2NvdmVyeS9rZXlzIiwia2lkIjoiMmIyZTgyMTA2NzkxZGM4ZmFkNzgxNW
Q3ZmI1NDRhNjJmNzJjMTZmYSJ9.eyJpc3MiOiJodHRwczovL2F1dGguZGV2LmFwaS5yaWNvaC8iL
CJhdWQiOiJodHRwczovL2lwcy5kZXYuYXBpLnJpY29oLyIsImlhdCI6MTQ5MDg1Mjc0MSwiZXhwI
joxNDkwODU2MzQxLCJjbGllbnRfaWQiOiI4ODQwMWU1MS05MzliLTQ3NzktYjdmNy03YzlmNGIzZj
kyYzAiLCJzY29wZSI6Imh0dHBzOi8vaXBzLmRldi5hcGkucmljb2gvdjEiLCJyaWNvaF9tc3Mi
OnsibWVkaWEiOnsicXVvdGEiOjEwLCJ0aHJvdHRsZSI6eyJ2YWx1ZSI6MCwid2luZG93IjowfX1
9fQ.jVq_c_cTzgsLipkJKBjAHzm8KDehW4rFA1Yg0EQRmqWmBDlEKtpRpDHZeF6ZSQfNH2OlrBW
FBiVDV9Th091QFEYrZETZ1IE1koAO14oj4kf8TCmhiG_CtJagvctvloW1wAdgMB1_Eubz9a8oim
cODqL7_uTmA5jKFx3ez9uoqQrEKZ51g665jSI6NlyeLtj4LrxpI9jZ4zTx1yqqjQx0doYQjBPhOB
06Z5bdiVyhJDRpE8ksRCC3kDPS2nsvDAal28sMgyeP8sPvfKvp5sa2UsH78WJmTzeZWcJfX2C2ba3
xwRMB5LaaVrQZlhj9xjum0MfDpIS1hJI6p5CHZ8w&scope=searchadsorg'
  • 请求接⼝的字段说明

Header
headers = {'Content-Type': 'application/x-www-form-urlencoded','Host': 'appleid.apple.com'
}
Payload
grant_type:固定值client_credentials;
client_id:前面获取的client_id;
client_secret:前面获取的client_secret;
scope:固定值searchadsorg
  • 响应示例

{"access_token":"eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjpudWxsfQ..lXm332TFi0u2E9YZ.bVVBvsjcavoQbBnQVeDiqEzmUIlaH9zLKY6rl36A_TD8wvgvWxpyBXMQuhs-qWG_dxQ5nfuJEIxOp8bIndfLE_4a3AiYtW0BsppO3vkWxMe0HWnzglkFbKUHU3PaJbLHpimmnLvQr44wUAeNcv1LmUPaSWT4pfaBzv3dMe3PNHJJCLVLfzNlWTmPxViIivQt3xyiQ9laBO6qIQiKs9zX7KE3holGpJ-Wvo39U6ZmGs7uK9BoNBPaFtd_q914mb9ChHAKcQaxF3Gadtu_Z5rYFg.vD0iQuRwHGYVnDy27qexCw","token_type": "Bearer","expires_in": 3600,"scope": "searchadsorg"
}
  • 响应字段说明

access_token:返回的 access_token,用于请求苹果广告相关数据接口 https://developer.apple.com/documentation/apple_search_ads/calling_the_apple_search_ads_api
token_type:固定值Bearer
expires_in:token有效期,3600秒即1小时
scope:访问权限

使⽤access_token获取⼴告账户下的⼴告orgid

官方文档:Get User ACL | Apple Developer Documentation

  • 请求示例

curl --location --request GET 'https://api.searchads.apple.com/api/v4/acls'
--header 'Authorization: Bearer eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwia2
lkIjpudWxsfQ..FGvVpX2vBYfyC91X.FKDdsLoAZwsfE8lgd5Ts5vSTY-lScKSp2myVIP9Eh7H
wc-7vsXr63aHcvNaDec9p7MwFHYnCC8Zphuf-BoqhAgyEu3hg_oHuqNEhbyS7iZexDfdgScTop
zGD5IP7Kiag71n0bbKb8o68MlJV8P-faGcsZR1-FBuISuyIrP6ZdUwXjoNygNCr4RkOLLNLjng
ShnnFUICJ3Q7dvrMQk0kUI_OFP3OcfZrsbHOIPWgDxA0W6GYI000Ua9x3TPT46kW2RQ67Pk5RS
2Ft_sqPgVh8V-A.7Rs0pIsQR09nRccM8'响应示例:{"orgName": "org name example","orgId": 40669820,"currency": "USD","timeZone": "America/Los_Angeles","paymentModel": "PAYG","roleNames": ["Admin"],"parentOrgId": "27154130","displayName": "display name example"
}

使⽤ access_token 获取⼴告账户投放数据

7.1 请求头参数说明

  • Authorization 设定值时组合⽅式为:Bearer {access_token},即Bearer在前,后⾯加 access_token

  • X-AP-Context 的值为orgId={orgId},orgId=不能漏掉

  • orgId 即广告系列组ID,上面第6部分接口中获取到的orgId

7.2 请求示例 - 获取广告系列组下的广告组数据

  • 以下仅为1个示例,实际对接时可查看苹果官⽅Management API⽂档,根据业务需要确认请求接⼝

  • 下面示例的官方文档参考: Get an Ad Group | Apple Developer Documentation

7.2.1 请求说明

URL
https://api.searchads.apple.com/api/v4/campaigns/{campaignId}/adgroups/{adgroupId}请求方式
GET请求头参数
Authorization
X-AP-ContextURL中参数
campaignId  #广告系列ID
adgroupId  #广告组ID

7.2.2 响应状态码

200 请求成功
401 无权限,请检查请求参数
403 拒绝请求,请检查请求参数
404 请求资源为找到,请检查请求参数
500 苹果服务器异常,稍后重试

7.2.3 响应示例

{"id": 542370539,"campaignId": 56543219,"name": " ad group name example","cpaGoal": {"amount": "100","currency": "USD"},"startTime": "2021-04-08T12:00:22.788","endTime": "2021-04-09T12:00:22.788","automatedKeywordsOptIn": false,"defaultBidAmount": {"amount": "100","currency": "USD"},"pricingModel": "CPC","targetingDimensions": {"age": {"included": [{"minAge": 20,"maxAge": 25},{"minAge": 25,"maxAge": 55}]},"gender": {"included": ["M","F"]},"country": null,"adminArea": null,"locality": null,"deviceClass": {"included": ["IPAD","IPHONE"]},"daypart": {"userTime": {"included": [1,3,22]}},"appDownloaders": null},"orgId": 40669820,"modificationTime": "2020-04-08T19:00:24.105","status": "ENABLED","servingStatus": "RUNNING","servingStateReasons": null,"displayStatus": "RUNNING","deleted": false
}

示例代码:

import jwt
import datetime as dt
import requests
import json
import time
client_id = 'SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577'
team_id = 'SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577'
key_id = 'bacaebda-e219-41ee-a907-e2c25b24d1b2'
audience = 'https://appleid.apple.com'
alg = 'ES256'
Define issue timestamp.
issued_at_timestamp = int(dt.datetime.utcnow().timestamp())
issued_at_timestamp = int(time.time())
Define expiration timestamp. May not exceed 180 days from issue timestamp.
expiration_timestamp = issued_at_timestamp + 600
Define JWT headers.
headers = dict()
headers['alg'] = alg
headers['kid'] = key_id
Define JWT payload.
payload = dict()
payload['sub'] = client_id
payload['aud'] = audience
payload['iat'] = issued_at_timestamp
payload['exp'] = expiration_timestamp
payload['iss'] = team_id
Path to signed private key.
KEY_FILE = 'private-key.pem'
with open(KEY_FILE,'r') as key_file:
key = ''.join(key_file.readlines())
key = '''-----BEGIN EC PRIVATE KEY-----
MHcCAQEEINEGa2CfhaseOXzsHoya/UW4kgQsij9ZW6j3+GQS2zEwoAoGCCqGSM49
AwEHoUQDQgAENEUUVYnQ+hWQRT3+YEwT3m2VGS5jlO6lanvZgLDHkWfOXhBiUfF8
Cyz/X3bzbkP8pOdJZ901qGdyeW73RV1RmA==
-----END EC PRIVATE KEY-----'''
获取 client_secret
client_secret = jwt.encode(
payload=payload,
headers=headers,
algorithm=alg,
key=key
)
client_secret = client_secret.decode('utf-8')
print('-------------------- client_secret --------------------')
print(client_secret)
获取 access_token
url = 'https://appleid.apple.com/auth/oauth2/token?grant_type=client_credentials&client_id=' + client_id + '&client_secret=' + client_secret + '&scope=searchadsorg'
token = requests.post(url=url)
token_json = json.loads(token.content.decode())
access_token = token_json['access_token']
print('-------------------- access_token --------------------')
print(access_token)
acls
url = "https://api.searchads.apple.com/api/v4/acls"
headers = {'Authorization': 'Bearer %s' % (access_token),'Content-Type': 'application/json'
}
acl = requests.get(url=url, headers=headers)
print('-------------------- acl --------------------')
print(acl.text)
org_id = 123456
campaign_id = 1234567890
headers = {'Authorization': 'Bearer %s' % (access_token),'Content-Type': 'application/json','X-AP-Context': 'orgId=%d' % (org_id),
}
获取 campaigns
url = 'https://api.searchads.apple.com/api/v4/campaigns'
campaigns = requests.get(url=url, headers=headers)
print('-------------------- campaign --------------------')
print(campaigns.text)
获取 campaign report
url = 'https://api.searchads.apple.com/api/v4/reports/campaigns'
data = {# "startTime": str(dt.date.today()),# "endTime": str(dt.date.today()),"startTime": "2022-09-01","endTime": "2022-09-01","selector": {"orderBy": [{"field": "localSpend","sortOrder": "ASCENDING"}],"conditions": [{"field": "campaignId","operator": "EQUALS","values": [campaign_id],"ignoreCase": False},{"field": "deleted","operator": "IN","values": ["false","true"]}],"pagination": {"offset": 0,"limit": 1000}},"returnRowTotals": True,"granularity": "DAILY","timeZone": "ORTZ","returnGrandTotals": True,"returnRecordsWithNoMetrics": True
}
data = json.dumps(data)
reports = requests.post(url=url, data=data, headers=headers)
print('-------------------- campaign reports --------------------')
print(reports.text)
package main
import ("fmt""io/ioutil""net/http""strings""time""github.com/dgrijalva/jwt-go/v4"
)
func main() {client_id := "SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577"team_id := "SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577"key_id := "bacaebda-e219-41ee-a907-e2c25b24d1b2"audience := "https://appleid.apple.com"alg := "ES256"issued_at_timestamp := time.Now().Unix()expiration_timestamp := issued_at_timestamp + 600claim := jwt.MapClaims{"sub": client_id,"aud": audience,"iat": issued_at_timestamp,"exp": expiration_timestamp,"iss": team_id,}private_pem := `-----BEGIN EC PRIVATE KEY-----
MHcCAQEEINEGa2CfhaseOXzsHoya/UW4kgQsij9ZW6j3+GQS2zEwoAoGCCqGSM49
AwEHoUQDQgAENEUUVYnQ+hWQRT3+YEwT3m2VGS5jlO6lanvZgLDHkWfOXhBiUfF8
Cyz/X3bzbkP8pOdJZ901qGdyeW73RV1RmA==
-----END EC PRIVATE KEY-----`private_key, _ := jwt.ParseECPrivateKeyFromPEM([]byte(private_pem))token := jwt.NewWithClaims(jwt.SigningMethodES256, claim)token.Header = map[string]interface{}{"alg": alg,"kid": key_id,}client_secret, _ := token.SignedString(private_key)fmt.Println("-------------------- client_secret --------------------")fmt.Println(client_secret)// 获取 access_tokenurl := "https://appleid.apple.com/auth/oauth2/token"res_token, err := http.Post(url, "application/x-www-form-urlencoded", strings.NewReader("grant_type=client_credentials&client_id="+client_id+"&client_secret="+client_secret+"&scope=searchadsorg"))if err != nil {fmt.Println(err.Error())}defer res_token.Body.Close()access_token, _ := ioutil.ReadAll(res_token.Body)fmt.Println("-------------------- access_token --------------------")fmt.Println(string(access_token))
}

ASAToken.java

- package com.appsa.asa;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.apache.commons.io.IOUtils;
import java.io.DataOutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.interfaces.ECKey;
import java.util.Date;
import java.util.HashMap;
/**
apple ads java demo
*/
public class ASAToken {private static String client_id = "xxx";private static String team_id = "xxx";private static String key_id = "xxx";private static String aud = "https://appleid.apple.com";private static String alg = "ES256";// 生成私钥// openssl ecparam -genkey -name prime256v1 -noout -out private-key.pem// 使用私钥生成公钥// openssl ec -in private-key.pem -pubout -out public-key.pem// 将PKCS1私钥转换为PKCS8(该格式一般Java调用)// openssl pkcs8 -topk8 -inform pem -in private-key.pem -outform pem -nocrypt -out private-key-new.pemprivate static final String PRIVATE_KEY_FILE_256 = "/Users/xxx/Downloads/private-key-new.pem";public static void main(String[] args) {System.out.println("client_id:" + client_id);System.out.println("  team_id:" + team_id);System.out.println("   key_id:" + key_id);try {String clientSecret = createClientSecret(PRIVATE_KEY_FILE_256);System.out.println("clientSecret 建议保存,有效期可设置最长 180 天");System.out.println(clientSecret);String url = "https://appleid.apple.com/auth/oauth2/token";String urlParameters = "grant_type=client_credentials&scope=searchadsorg&client_id=" + client_id + "&client_secret=" + clientSecret;byte[] postData = urlParameters.getBytes(StandardCharsets.UTF_8);int postDataLength = postData.length;URL obj = new URL(url);HttpURLConnection con = (HttpURLConnection) obj.openConnection();con.setDoOutput(true);con.setRequestMethod("POST");con.setRequestProperty("charset", "utf-8");con.setRequestProperty("Content-Length", Integer.toString(postDataLength));con.setRequestProperty("Host", "appleid.apple.com");con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");con.setUseCaches(false);try (DataOutputStream wr = new DataOutputStream(con.getOutputStream())) {wr.write(postData);}if (con.getResponseCode() == 200) {String result = IOUtils.toString(con.getInputStream(), StandardCharsets.UTF_8);JSONObject jsonObject = JSONObject.parseObject(result);System.out.println("access_token 有效期1个小时");System.out.println(jsonObject.getString("access_token"));}} catch (Exception e) {System.err.println("error");}}/*** Create a Client Secret** @return client secret*/public static String createClientSecret(String privateKeyPath) throws Exception {Algorithm algorithm = Algorithm.ECDSA256((ECKey) PemUtils.readPrivateKeyFromFile(privateKeyPath, "EC"));return JWT.create().withIssuer(team_id).withAudience(aud).withHeader(new HashMap() {{put("alg", alg);put("kid", key_id);}}).withSubject(client_id).withIssuedAt(new Date()).withExpiresAt(new Date(System.currentTimeMillis() + 86400 * 180 * 1000L)).sign(algorithm);}
}

PemUtils.java

package com.appsa.asa;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
public class PemUtils {private static byte[] parsePEMFile(File pemFile) throws IOException {if (!pemFile.isFile() || !pemFile.exists()) {throw new FileNotFoundException(String.format("The file '%s' doesn't exist.", pemFile.getAbsolutePath()));}PemReader reader = new PemReader(new FileReader(pemFile));PemObject pemObject = reader.readPemObject();byte[] content = pemObject.getContent();reader.close();return content;}private static PublicKey getPublicKey(byte[] keyBytes, String algorithm) {PublicKey publicKey = null;try {KeyFactory kf = KeyFactory.getInstance(algorithm);EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);publicKey = kf.generatePublic(keySpec);} catch (NoSuchAlgorithmException e) {System.out.println("Could not reconstruct the public key, the given algorithm could not be found.");} catch (InvalidKeySpecException e) {System.out.println("Could not reconstruct the public key");}return publicKey;}private static PrivateKey getPrivateKey(byte[] keyBytes, String algorithm) {PrivateKey privateKey = null;try {KeyFactory kf = KeyFactory.getInstance(algorithm);EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);privateKey = kf.generatePrivate(keySpec);} catch (NoSuchAlgorithmException e) {System.out.println("Could not reconstruct the private key, the given algorithm could not be found.");} catch (InvalidKeySpecException e) {System.out.println("Could not reconstruct the private key.");}return privateKey;}public static PublicKey readPublicKeyFromFile(String filepath, String algorithm) throws IOException {byte[] bytes = PemUtils.parsePEMFile(new File(filepath));return PemUtils.getPublicKey(bytes, algorithm);}public static PrivateKey readPrivateKeyFromFile(String filepath, String algorithm) throws IOException {byte[] bytes = PemUtils.parsePEMFile(new File(filepath));return PemUtils.getPrivateKey(bytes, algorithm);}
}

四、注意事项总结

1. 框架须知

  • AdServices.framework 这个框架是用于 ASA 归因的,不受 ATT 约束,就是无论用户是否允许跟踪,都可以归因,区别为是否能获取点击时间,仅支持 14.3 及更高版本系统,需 XCode 12.3 及更高版本支持,设置为 optional。

  • AppTrackingTransparency.framework 在 iOS 14 及更高版本用于征求用户跟踪许可的框架,就是弹窗询问用户是否同意跟踪;在 iOS 14.5 上苹果将强制要求开发者实施,也是获取 IDFA 的前提。

2.token相关注意事项

  • AdServices 获取的 token 不可作为唯一设备标识

  • 获取 token 需要设备联网,并且要做超时及容错处理

  • 获取token后需等待五秒再发送请求给归因API

  • 获取 token 的步骤,可以借助日志系统收集相关信息,用于排查问题和代码优化

  • APP首次激活时获取token,建议获取网络权限后,等待500ms-1000ms再做token请求

  • 如果 App 退出前台,在下一次打开进行 token 请求重试

  • 对token请求成功率进行监测,建议成功率下降>=5%应触发预警机制,并根据报错调查原因

  • token 有效期为 24 小时

  • 当 token 无效时,接口响应码为 400

  • 当 token 过期(有效期为 24 小时)时,接口响应码为 404

3.数据归因相关注意事项

  • AdServices 的 restful api 请求,失败后 每隔 5 秒重试,建议最多 3 次 ,也可根据需求增加重试次数,如重试之后仍然没有正常,统计返回的报错日志

  • 有数据差合理,如进行反馈需提供 APP Id、发生问题的时间、API返回结果

  • Apple Search Ads归因窗口期为30天,用户30天以前点击的广告我们无法归因

  • Apple Search Ads采用末次归因,用户如果30天或一天内惦记了搜索标签,又搜了关键词,归因返回的是用户最后一次点击广告的信息

  • 目前苹果搜索广告归因仅支持Adservice,并且Adservice仅支持14.3及以上的用户,14.3以下的用户不会被归因到,这些用户会被计入自然量

  • 14.3以下的用户还可请求API,返回结果为attribution = false

  • 目前14.3以下的用户占比非常低,过去4年推出的ipnone 90%使用ios16,81%所有设备使用ios16

4.归因字段缺失原因

  • AdServices 归因在 ATT弹框之前,不返回字段clickDate

  • ATT弹框后,用户不允许跟踪,不返回字段clickDate

  • AdServices 归因建议在 ATT 弹窗之后,用户如允许跟踪,则可以获得 clickDate

  • 开启 Search Match(搜索匹配)的广告组带来的激活,不返回字段 keywordId

  • 默认素材带来的激活,不返回字段 adId

5.客户端处理逻辑参考

  • 由于各种原因导致的获取归因包失败时,需要做容错处理,及时进行重试(必需)

  • 重试多次仍然失败的,应用在下次启动时再进行获取(必需)

  • 当归因包返回的 attribution 为 false,7 天后再请求归因(建议)

  • iOS 14.3 以下,设置项为限制跟踪时,7 天后再次请求归因(建议)

  • 当归因包返回的 attribution 为 true 时,30 天后再请求归因(建议)

 

五、参考文档

Apple Ads 归因 API

Apple Ads 归因 API 文档 (PDF)

ad_services

Apple Search Ads

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/61076.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

hhdb数据库介绍(9-18)

Oracle兼容性说明 数据类型兼容 本节主要介绍 HHDB Server与Oracle 数据库中数据类型的详细兼容对比信息。 比较项Oracle数据库数据类型HHDB Server数据类型对应项/替代项字符串/字符VARCHAR2( n )VARCHAR( n )字符串/字符NVARCHAR2( n )VARCHAR( n ) character set utf8字符…

服务器数据恢复—raid5阵列故障导致上层系统分区无法识别的数据恢复案例

服务器数据恢复环境&#xff1a; 某品牌DL380服务器&#xff0c;服务器中三块SAS硬盘组建了一组raid5阵列。服务器安装Windows Server操作系统&#xff0c;划分了3个分区&#xff0c;D分区存放数据库&#xff0c;E分区存放数据库备份。 服务器故障&#xff1a; RAID5阵列中有一…

STM32设计电流与温度监控python上位机监控平台设计

目录 前言 一、本设计主要实现哪些很“开门”功能&#xff1f; 二、电路设计原理图 电路图采用Altium Designer进行设计&#xff1a; 三、实物设计图 四、程序源代码设计 五、获取资料内容 前言 在现代工业自动化和智能设备管理中&#xff0c;对电流和温度的实时监控是…

STM32F103ZET6快速创建工程

1.创建工程目录,点击进目录, 然后复制目录的路径 2.双击打开kile5, 然后点击project, 创建工程 3.找到刚才的目录, 然后加入工程 4.输入芯片 STM32F103ZE, 然后保存 5.勾选对应的初始文件 6.加入main.c函数 7.双击进入Source_code文件夹 , 然后加入User,进入User 8.加入main.c文…

GIS与Web开发结合的产物:WebGIS

WebGIS&#xff0c;其实是利用Web开发技术结合地理信息系统&#xff08;GIS&#xff09;的产物&#xff0c;它是一种通过Internet实现GIS交互操作和服务的最佳途径。 WebGIS通过图形化界面直观地呈现地理信息和特定数据&#xff0c;具有可扩展性和跨平台性。 它提供交互性&am…

Python模块、迭代器与正则表达式day10

1、Python模块 1.1模块的简介 在编写代码的时候&#xff0c;创建的.py文件就被称为一个模块 1.2模块的使用 想要在a文件里使用b文件的时候&#xff0c;只要在a文件中使用关键字import导入即可 1.2.2 from ...import...语句 导入模块可以使用import&#xff0c;如果只导入模…

调用 Xinference OpenAI接口时报错 Model not found in the model list, uid

错误如下, 请不要被错误吓住或蒙蔽双眼, 自己看最下面的报错内容 Traceback (most recent call last): File "C:\Users\HW\.conda\envs\aibot\Lib\site-packages\starlette\responses.py", line 259, in __call__ await wrap(partial(self.listen_for_disconn…

小红书内容推荐算法开发:利用API打造个性化用户体验

在这个内容为王的时代&#xff0c;个性化推荐算法成为了各大平台争夺用户注意力的利器。小红书&#xff0c;作为国内领先的内容分享社区&#xff0c;其丰富的用户生成内容&#xff08;UGC&#xff09;和独特的社区氛围&#xff0c;为推荐算法的开发提供了肥沃的土壤。本文将深入…

没钱买KEGG怎么办?REACTOME开源通路更强大

之前搜集免费生物AI插图时简单提到了通路数据库Reactome&#xff08;https://reactome.org/&#xff09;&#xff0c; 那些精美的生物插图只能算是该数据库附赠的小礼品&#xff0c;他的主要功能还是作为一个开源的通路数据库&#xff0c;为相关领域的研究者提供直观的可视化生…

ChatGPT学术专用版,一键润色纠错+中英互译+批量翻译PDF

ChatGPT academic项目是由中科院团队基于ChatGPT专属定制。论文润色、语法检查、中英互译、代码解释等可一键搞定&#xff0c;堪称科研神器。 功能介绍 我们以3.5版本为例&#xff0c;ChatGPT学术版总共分为五个区域&#xff1a;输入控制区、输出对话区、基础功能区、函数插件…

版本控制【Git Bash】【Gitee】

目录 一、什么是版本控制&#xff1f; 二、版本控制的种类&#xff1a; 1、本地版本控制 2、集中版本控制 3、分布式版本控制 三、下载Git Bash 四、Git Bash 配置 五、Git Bash使用 1、切换目录&#xff1a;cd 2.查看当前文件路径&#xff1a;pwd 3.列出当前目录下文件…

关于Java合并多个Excel中的数据【该数据不是常规列表】,并入库保存的方案

1. 背景 最近在使用RPA&#xff08;机器人流程自动化&#xff09;做数据采集的时候。发现那个RPA采集&#xff0c;一次只能采集相同格式的数据&#xff0c;然后入到Excel或者库中。由于院内系统的业务限制&#xff0c;导致采集的数据是多个Excel&#xff0c;并且我们这边的需求…

【进阶系列】python的模块

模块 创建一个 .py 文件&#xff0c;这个文件就称之为 一个模块 Module 如何使用 import 想要B.py文件中&#xff0c;使用A.py文件&#xff0c;只需要在B.py文件中使用关键字import导入即可。 import A# 若A是一个包的话&#xff0c;可以这样写 import A.函数名from impor…

使用Web Workers提升JavaScript的并行处理能力

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 使用Web Workers提升JavaScript的并行处理能力 使用Web Workers提升JavaScript的并行处理能力 使用Web Workers提升JavaScript的…

从零开始使用GOT-OCR2.0——多模态通用型OCR(非常具有潜力的开源OCR项目):项目环境安装配置 + 测试使用

在之前精读了这个多模态的OCR项目论文&#xff0c;理解了其基本的模型架构&#xff0c;论文精读地址&#xff1a; 【论文精读】GOT-OCR2.0源码论文——打破传统OCR流程的多模态视觉-语言大模型架构&#xff1a;预训练VitDet 视觉模型 阿里通义千问Qwen语言模型-CSDN博客 本文在…

【Golang】——Gin 框架中的表单处理与数据绑定

在 Web 应用开发中&#xff0c;表单是用户与服务器交互的重要手段。Gin 框架对表单处理提供了高效便捷的支持&#xff0c;包括数据绑定、验证等功能。在本篇博客中&#xff0c;我们将详细介绍如何使用 Gin 框架处理表单数据&#xff0c;涵盖基础操作与进阶技巧&#xff0c;帮助…

删除k8s 或者docker运行失败的脚本

vi delete_exited_containers.sh#!/bin/bash# 列出所有停止的容器并存储到数组 list_exited_containers() {echo -e "\nStopped containers:"containers()# 获取停止的容器信息并存入数组while IFS read -r line; docontainers("$line")done < <(do…

Linux(命令格式详细+字符集 图片+大白话)

后面也会持续更新&#xff0c;学到新东西会在其中补充。 建议按顺序食用&#xff0c;欢迎批评或者交流&#xff01; 缺什么东西欢迎评论&#xff01;我都会及时修改的&#xff01; 在这里真的很感谢这位老师的教学视频让迷茫的我找到了很好的学习视频 王晓春老师的个人空间…

Clip结合Faiss+Flask简易版文搜图服务

一、实现 使用目录结构&#xff1a; templates ---upload.html faiss_app.py 前端代码&#xff1a;upload.html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content&quo…

SpringBoot 之整合gRPC

父工程中引入基本的依赖&#xff1a; <modules><module>api</module><module>client</module><module>service</module></modules><parent><artifactId>spring-boot-starter-parent</artifactId><group…