亚马逊 Marketing Stream(广告)& SP-API 通知 —— SQS 接入要点与踩坑
亚马逊广告 Marketing Stream 与 SP-API 通知:SQS 接入实战(含踩坑)
亚马逊有两套「把数据实时推到你 AWS 队列」的体系:广告侧的 Amazon Marketing Stream 和 Selling Partner API(SP-API)的通知(Notifications)。两者都能落到 Amazon SQS,但机制、鉴权、区域规则完全不同。本文把两套都打通,并把踩过的坑、配置和代码一次性整理出来。
约定:下文 JSON / 代码里的
123456789012代表「你自己的 AWS 账号 ID」,请替换成你的;其余 12 位数字(如437568002678、926844853897、各数据集账号)都是亚马逊官方固定账号,照抄即可。所有 access key / client secret / refresh token 都是占位符,请换成你自己的。
一、两套系统总览
| 维度 | 广告 Marketing Stream | SP-API 通知 |
|---|---|---|
| 投递机制 | SNS 主题 → 你的 SQS | 直连 SQS 或 EventBridge → SQS |
| 发送方 Principal | sns.amazonaws.com |
直连:账号 437568002678 |
| 校验角色 | ReviewerRole(926844853897) |
同一个 437568002678 |
| 源账号数量 | 每数据集 × 每区域一个(很多) | 直连只 1 个 |
| 区域限制 | 强锁(队列区域必须 = 店铺 realm) | 较自由 |
| 按账号建的对象 | 订阅(每 profile × dataSetId) | 订阅(每卖家 × 通知类型) |
| 投递目标(队列) | 多店铺、多类型共享一个 | 多店铺、多类型共享一个 destination |
| 可选投递目标 | SQS / Firehose | SQS / EventBridge |
一句话:两套都是「目标共享、订阅按维度拆」;区别主要在鉴权和区域规则。
二、广告 Amazon Marketing Stream
2.1 队列访问策略(三条语句)
广告走 SNS→SQS,队列策略要做三件事:① 你自己能管理队列;② 允许 SNS 服务发送(并用 SourceArn 限定到对应数据集的官方 SNS 账号);③ 允许 ReviewerRole 校验队列。下面这份覆盖了北美(us-east-1)全部数据集,直接贴到队列的 Access policy 即可:
{
"Version": "2012-10-17",
"Id": "MarketingStreamNAQueuePolicy",
"Statement": [
{
"Sid": "OwnerFullAccess",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::123456789012:root" },
"Action": "SQS:*",
"Resource": "*"
},
{
"Sid": "AllowStreamSendMessage",
"Effect": "Allow",
"Principal": { "Service": "sns.amazonaws.com" },
"Action": "SQS:SendMessage",
"Resource": "*",
"Condition": {
"ArnLike": {
"aws:SourceArn": [
"arn:aws:sns:us-east-1:906013806264:*",
"arn:aws:sns:us-east-1:802324068763:*",
"arn:aws:sns:us-east-1:055588217351:*",
"arn:aws:sns:us-east-1:370941301809:*",
"arn:aws:sns:us-east-1:877712924581:*",
"arn:aws:sns:us-east-1:709476672186:*",
"arn:aws:sns:us-east-1:154357381721:*",
"arn:aws:sns:us-east-1:091028706140:*",
"arn:aws:sns:us-east-1:010312603579:*",
"arn:aws:sns:us-east-1:570159413969:*",
"arn:aws:sns:us-east-1:118846437111:*",
"arn:aws:sns:us-east-1:305370293182:*",
"arn:aws:sns:us-east-1:644124924521:*",
"arn:aws:sns:us-east-1:084590724871:*",
"arn:aws:sns:us-east-1:678715897637:*"
]
}
}
},
{
"Sid": "AllowStreamReviewerGetQueueAttributes",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::926844853897:role/ReviewerRole" },
"Action": "SQS:GetQueueAttributes",
"Resource": "*"
}
]
}
几点说明:
Resource用"*":队列策略只作用于它所附加的队列,所以"*"等价于「本队列」,好处是同区任意队列都能贴这同一份、零改动。想更精确就换成具体队列 ARN。SourceArn带通配符,条件用ArnLike(用ArnEquals也行,AWS 里两者行为相同)。- 不要把账号也通配成
arn:aws:sns:us-east-1:*:*,那等于允许任何账号往你队列灌消息。
2.2 ReviewerRole 是固定常量
arn:aws:iam::926844853897:role/ReviewerRole
- 所有数据集、所有区域(NA/EU/FE)都一样,照抄即可。
- 创建订阅时,亚马逊会用它调
GetQueueAttributes校验队列,缺这条订阅会失败。 - 可以放宽成
arn:aws:iam::926844853897:root(整个亚马逊账号)也能用,但范围更大;不要用"*",也不要删。 - 官方还有个
926844853897:role/SubscriberRole,但 SQS 队列策略不需要它。
2.3 区域必须和店铺 realm 一致(最容易踩的坑)
队列建在哪个 AWS 区不能自由选,由你授权店铺的 realm 决定:
| 店铺站点 | Realm | 队列必须所在区 | Ads API 端点 |
|---|---|---|---|
| US / CA / MX / BR | NA | us-east-1 |
advertising-api.amazon.com |
| 欧洲 + 印度 + 中东(AE/SA/EG/TR) | EU | eu-west-1 |
advertising-api-eu.amazon.com |
| JP / AU / SG | FE | us-west-2 |
advertising-api-fe.amazon.com |
根因:广告底层是 SNS→SQS,而标准 SNS 主题只能投递到同区域的 SQS;亚马逊的 SNS 主题在 realm 对应区,所以你的队列必须也在那个区。官方 CloudFormation 模板甚至用断言把它写死,区域不匹配直接拒绝部署。
2.4 数据集 → SNS 源账号(NA / EU / FE 全表)
策略里 aws:SourceArn 用到的账号按区域分三套,跨区不能混用:
| 数据集 | NA(us-east-1) | EU(eu-west-1) | FE(us-west-2) |
|---|---|---|---|
| sp-traffic | 906013806264 | 668473351658 | 074266271188 |
| sp-conversion | 802324068763 | 562877083794 | 622939981599 |
| budget-usage | 055588217351 | 675750596317 | 100899330244 |
| sd-traffic | 370941301809 | 947153514089 | 310605068565 |
| sd-conversion | 877712924581 | 664093967423 | 818973306977 |
| sb-traffic | 709476672186 | 623198756881 | 485899199471 |
| sb-conversion | 154357381721 | 195770945541 | 112347756703 |
| sb-clickstream | 091028706140 | 219513501272 | 632322331982 |
| sb-rich-media | 010312603579 | 662188760626 | 618223300352 |
| campaigns | 570159413969 | 834862128520 | 527383333093 |
| adgroups | 118846437111 | 130948361130 | 668585072850 |
| ads | 305370293182 | 648558082147 | 802070757281 |
| targets | 644124924521 | 503759481754 | 248074939493 |
| sponsored-ads-campaign-diagnostics-recommendations | 084590724871 | 059061853903 | 489995134625 |
| sp-budget-recommendations | 678715897637 | 158915609581 | 007292432803 |
带前导零的账号(如
055588217351)必须保留完整 12 位。
2.5 多店铺、多数据集如何共用队列
- 多个店铺(profile)共用一个队列:每个 (profile × dataSetId) 建一条订阅,
destinationArn都指向同一队列。 - 不同数据类型也能共用一个队列:把多个数据集账号都列进
SourceArn即可(上面那份就是)。 - 跨 realm 不能共用:北美店 + 欧洲店 = 至少两个队列(us-east-1 + eu-west-1),源账号各用各的。
- 想隔离也行:可以「一数据集一队列」,消息按数据集分流更干净;但不是必须。
队列里靠消息内容区分:广告记录带 advertiser_id / marketplace_id 等标识。
2.6 订阅确认:PENDING_CONFIRMATION → ACTIVE
创建订阅后状态是 PENDING_CONFIRMATION。因为是跨账号 SNS→SQS,亚马逊的 SNS 会先往队列发一条确认消息,你必须确认后订阅才激活:
- 队列里收到一条
"Type": "SubscriptionConfirmation"的消息,里面有SubscribeURL字段。 - 对这个 URL 发一个 GET 请求即表示同意订阅。
- 等几分钟,状态从
PENDING_CONFIRMATION变成ACTIVE,数据开始进队列。
消费端自动确认的关键代码(Go,用 aws-sdk-go-v2 消费 SQS):
// 收到消息后判断是不是 SNS 确认消息,是就 GET 它的 SubscribeURL 完成确认
var v map[string]interface{}
_ = sonic.Unmarshal([]byte(body), &v)
if v["Type"] == "SubscriptionConfirmation" {
if u, _ := v["SubscribeURL"].(string); u != "" {
resp, err := http.Get(u) // 确认订阅 → 几分钟后转 ACTIVE
if err == nil {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}
}
}
2.7 广告踩坑清单
- 队列区域必须 = realm 对应区,否则订阅建不起来。
- 源账号要用对应 realm 那套,别把 NA/EU/FE 混用。
- ReviewerRole 那条不能少;队列必须是 Standard(非 FIFO)。
- 首次订阅卡在
PENDING_CONFIRMATION→ 记得 GETSubscribeURL确认。 - at-least-once,会重复 → 用
时间窗口 + profileId + dataSet做去重键。
三、SP-API 通知
3.1 队列访问策略(直连 SQS)
SP-API 直连 SQS 比广告简单很多:一个固定账号 437568002678 既负责发送、又负责校验队列,没有 SNS、没有 SourceArn、没有 ReviewerRole:
{
"Version": "2012-10-17",
"Id": "SpApiNotificationsQueuePolicy",
"Statement": [
{
"Sid": "OwnerFullAccess",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::123456789012:root" },
"Action": "sqs:*",
"Resource": "*"
},
{
"Sid": "AllowSpApiSendAndValidate",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::437568002678:root" },
"Action": [
"sqs:GetQueueAttributes",
"sqs:SendMessage"
],
"Resource": "*"
}
]
}
这份不含任何区域信息(纯账号),所以任何区域都能用同一份。
3.2 直连 SQS vs EventBridge
SP-API 通知有两条投递链路:
- 直连 SQS:SP-API 账号
437568002678直接把通知SendMessage进你的队列。最简单,本文用这条。 - EventBridge → SQS:SP-API 先发到 partner event source,再由 EventBridge 规则转发到 SQS(发送方变成
events.amazonaws.com,要给 EventBridge 规则授权)。适合要过滤/扇出/触发 Lambda 的场景,组件更多。
如果走 EventBridge,队列策略的发送方要换成 events.amazonaws.com 并用规则 ARN 限定;createDestination 时 resourceSpecification 要用 eventBridge 而不是 sqs。
3.3 grantless token:别用 refresh token
SP-API 的部分接口是 grantless 操作,不需要卖家授权。它不是用 refresh token 换,而是另一种授权类型:
POST https://api.amazon.com/auth/o2/token
Content-Type: application/json
{
"grant_type": "client_credentials",
"scope": "sellingpartnerapi::notifications",
"client_id": "<你的 LWA client_id>",
"client_secret": "<你的 LWA client_secret>"
}
各操作用哪种 token:
| 操作 | 授权 |
|---|---|
| getDestinations / createDestination / getDestination / deleteDestination | Grantless(client_credentials + scope) |
| getSubscriptionById / deleteSubscriptionById | Grantless |
| createSubscription / getSubscription | 卖家授权(refresh_token grant) |
3.4 destination 与 subscription 的关系
- destination 是应用级(用 grantless token 建,不绑卖家)→ 同一个应用下所有店铺、所有通知类型共用一个 destination。
- subscription 是每(卖家 × 通知类型 × payloadVersion)一条,但
destinationId都填同一个。
1 个 SQS 队列
└── 1 个 destination(destinationId = X)
├── 店铺A · ORDER_CHANGE → X
├── 店铺A · ANY_OFFER_CHANGED → X
└── 店铺B · ORDER_CHANGE → X
队列里靠 notificationType(区分类型)和 notificationMetadata.subscriptionId(区分店铺)分流。
3.5 SP-API 踩坑清单
- 队列必须 Standard,不能 FIFO。
- 队列策略要同时给
SendMessage+GetQueueAttributes,少 GetQueueAttributes 则 createDestination 校验失败。 - 加密大坑:开了 SSE 但用 SQS 托管密钥 → 外部账号
437568002678用不了密钥,消息静默收不到。要开就用客户托管 KMS 密钥并在密钥策略放行437568002678(kms:GenerateDataKey*+kms:Decrypt);不确定就先别开加密。 - createDestination 用 grantless token,不是 refresh token。
- 直连 SQS 没有 SNS 那种
SubscribeURL确认步骤(那是广告特有的)。
四、AWS / IAM 通用知识点
arn:aws:iam::123456789012:root不是「只有 root 用户」:它代表「整个账号,具体谁能用由 IAM 决定」。- 同账号下是「或」关系:访问者和队列同账号时,IAM 身份策略或资源(队列)策略任一 Allow 即可,不需要两者都给。owner 那条建议保留(AWS 默认、无害、防锁死)。
:root可简写:"AWS": "123456789012"与"AWS": "arn:aws:iam::123456789012:root"完全等价(控制台常会自动展开)。仅对「账号」成立,角色/用户不能这样简写。Resource: "*"vs 具体 ARN:队列策略里"*"等价于本队列,方便整段复制到多个队列;具体 ARN 更精确。
五、SQS / EventBridge / Firehose 怎么选
| SQS | EventBridge | Firehose | |
|---|---|---|---|
| 比喻 | 信箱,自己拉取 | 智能路由器,规则转发 | 传送带,攒批入库 |
| 实时性 | 近实时,逐条 | 近实时,可触发 Lambda | 有缓冲延迟 |
| 典型用途 | 自己写消费逻辑 | 事件驱动、过滤、扇出 | 灌入 S3/Redshift/Snowflake 做分析 |
| 谁支持 | 广告 & SP-API | SP-API | 广告 |
六、消费端凭证与权限
队列策略只解决「亚马逊能不能往队列发」;你的程序要从队列读取还需要一套 AWS 凭证(access key + secret)。推荐新建 IAM 用户(或给跑在 EC2/Lambda 上的角色授权),并只给消费需要的 SQS 权限:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ConsumeQueues",
"Effect": "Allow",
"Action": [
"sqs:ReceiveMessage",
"sqs:DeleteMessage",
"sqs:GetQueueAttributes",
"sqs:ChangeMessageVisibility",
"sqs:GetQueueUrl"
],
"Resource": "arn:aws:sqs:*:123456789012:*"
}
]
}
别用根用户 access key,也别图省事给
Action:* / Resource:*管理员权限;access key 不要提交进代码仓库,并定期轮换。
七、Go 代码实战(SP-API 直连 SQS)
下面是 SP-API 通知接入的完整跑通代码(基于自研 https://github.com/4379711/amz-sdk 的 notifications 客户端,鉴权用 client_credentials / refresh_token 两条路)。密钥均为占位符。
7.1 准备 app 与 configuration
// app 持有 LWA 应用凭证 + 卖家(refresh_token);configuration 会据此构造一个
// 自动注入 x-amz-access-token 的 http.Client(默认走 refresh_token 流程)。
var app = &auth.SpAuth{
App: &auth.App{
ClientID: "<你的 LWA client_id>",
ClientSecret: "<你的 LWA client_secret>",
},
Seller: &auth.Seller{
CountryCode: "US", // 决定端点:US → sellingpartnerapi-na.amazon.com
SellerType: "SC",
SellerId: "<卖家ID>",
},
Token: &auth.Token{
RefreshToken: "<卖家授权得到的 refresh_token>",
},
}
var configuration = pkg.NewConfiguration(app)
var spNotifyClient = notifications.NewAPIClient(configuration)
7.2 grantless token 助手
createDestination 等 grantless 操作要用 client_credentials 换 token,再通过 context 注入(SDK 的鉴权 transport 会优先用 context 里的 token,跳过 refresh_token 流程):
const spNotificationsScope = "sellingpartnerapi::notifications"
// 用 client_credentials 换一枚 grantless access_token(不经过 refresh_token)
func spGrantlessToken(scope string) (string, error) {
reqBody, _ := sonic.Marshal(map[string]string{
"grant_type": "client_credentials",
"scope": scope,
"client_id": app.ClientID,
"client_secret": app.ClientSecret,
})
resp, err := pkg.DefaultClient.Post(app.GetLwaTokenEndpoint(), "application/json", bytes.NewReader(reqBody))
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
return "", err
}
if resp.StatusCode >= 400 {
b, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("grantless token failed: %s, %s", resp.Status, string(b))
}
var out struct {
AccessToken string `json:"access_token"`
}
if err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&out); err != nil {
return "", err
}
return out.AccessToken, nil
}
// 把 grantless token 注入 context,grantless 操作传它
func spGrantlessCtx() (context.Context, error) {
tk, err := spGrantlessToken(spNotificationsScope)
if err != nil {
return nil, err
}
return context.WithValue(context.Background(), "x-amz-access-token", tk), nil
}
7.3 建 destination(grantless)与订阅(卖家 token)
const (
spQueueArn = "arn:aws:sqs:us-east-1:123456789012:spapi-notifications"
spNotificationType = "ORDER_CHANGE" // 见官方 Notification Type Values
spPayloadVersion = "1.0"
)
// ① 建 destination —— grantless;返回 destinationId,所有店铺/类型复用同一个
func createDestination() {
ctx, err := spGrantlessCtx()
if err != nil {
fmt.Println(err)
return
}
body := notifications.CreateDestinationRequest{
Name: "na-sqs-notifications",
ResourceSpecification: notifications.DestinationResourceSpecification{
Sqs: ¬ifications.SqsResource{Arn: spQueueArn},
},
}
resp, _, err := spNotifyClient.NotificationsAPI.CreateDestination(ctx).Body(body).Execute()
if err != nil {
fmt.Println("createDestination 失败:", err)
return
}
jsonStr, _ := sonic.MarshalIndent(resp, "", " ")
fmt.Println(string(jsonStr)) // 里面有 destinationId
}
// ② 建订阅 —— 卖家 token(默认 refresh_token 流程,直接用 Background)
func createSubscription(destinationId string) {
body := notifications.CreateSubscriptionRequest{
PayloadVersion: spPayloadVersion,
DestinationId: destinationId,
}
resp, _, err := spNotifyClient.NotificationsAPI.
CreateSubscription(context.Background(), spNotificationType).
Body(body).Execute()
if err != nil {
fmt.Println("createSubscription 失败:", err)
return
}
jsonStr, _ := sonic.MarshalIndent(resp, "", " ")
fmt.Println(string(jsonStr)) // 里面有 subscriptionId
}
grantless 注入的关键:鉴权 transport 里有这么一段——
if v := ctx.Value("x-amz-access-token"); v != nil { req.Header.Set("x-amz-access-token", v.(string)); ... },所以只要把 token 放进 context 就能覆盖默认的卖家 token 流程。
7.4 消费 SQS
SP-API 直连 SQS 的消息 body 本身就是通知 JSON(含 notificationType / payload / notificationMetadata),没有 SNS 确认那一步,直接消费即可:
cfg := aws.Config{
Region: "us-east-1",
Credentials: awscreds.NewStaticCredentialsProvider(
"AKIAIOSFODNN7EXAMPLE", // 换成你的 IAM access key
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", // 换成你的 IAM secret
"",
),
}
client := sqs.NewFromConfig(cfg)
queueURL := "https://sqs.us-east-1.amazonaws.com/123456789012/spapi-notifications"
for {
out, err := client.ReceiveMessage(context.Background(), &sqs.ReceiveMessageInput{
QueueUrl: aws.String(queueURL),
MaxNumberOfMessages: 10,
WaitTimeSeconds: 20, // 长轮询
})
if err != nil {
fmt.Println("receive error:", err)
time.Sleep(3 * time.Second)
continue
}
for _, m := range out.Messages {
fmt.Println(aws.ToString(m.Body)) // 通知 JSON,自己按 notificationType 分流
// 处理成功后再删除(at-least-once,需幂等)
client.DeleteMessage(context.Background(), &sqs.DeleteMessageInput{
QueueUrl: aws.String(queueURL),
ReceiptHandle: m.ReceiptHandle,
})
}
}
7.5 跑通顺序
- 建 Standard 队列,贴上 3.1 的策略。
client_credentials换 grantless token →createDestination→ 记下destinationId。- 卖家
refresh_token流程 →createSubscription(填destinationId)→ 记下subscriptionId。 - 触发对应事件(如订单状态变化)→ 消费端就能收到通知。
八、固定值速查
| 项 | 值 |
|---|---|
| 广告 ReviewerRole | arn:aws:iam::926844853897:role/ReviewerRole |
| SP-API 通知账号 | arn:aws:iam::437568002678:root |
| LWA token 端点 | https://api.amazon.com/auth/o2/token |
| grantless scope(通知) | sellingpartnerapi::notifications |
| realm → 区域 | NA=us-east-1,EU=eu-west-1,FE=us-west-2 |
九、参考链接
- 广告 Marketing Stream onboarding:https://advertising.amazon.com/API/docs/en-us/amazon-marketing-stream/onboarding
- SP-API 通知:https://developer-docs.amazon.com/sp-api/docs/notifications-api
- SP-API 授予 SQS 权限:https://developer-docs.amazon.com/sp-api/docs/tutorial-grant-permission-to-sqs-queue
- SP-API grantless 操作:https://developer-docs.amazon.com/sp-api/docs/grantless-operations
- 通知类型取值:https://developer-docs.amazon.com/sp-api/docs/notification-type-values
- go语言sp-api 和广告的sdk:https://github.com/4379711/amz-sdk

浙公网安备 33010602011771号