亚马逊 Marketing Stream(广告)& SP-API 通知 —— SQS 接入要点与踩坑

亚马逊广告 Marketing Stream 与 SP-API 通知:SQS 接入实战(含踩坑)

亚马逊有两套「把数据实时推到你 AWS 队列」的体系:广告侧的 Amazon Marketing StreamSelling Partner API(SP-API)的通知(Notifications)。两者都能落到 Amazon SQS,但机制、鉴权、区域规则完全不同。本文把两套都打通,并把踩过的坑、配置和代码一次性整理出来。

约定:下文 JSON / 代码里的 123456789012 代表「你自己的 AWS 账号 ID」,请替换成你的;其余 12 位数字(如 437568002678926844853897、各数据集账号)都是亚马逊官方固定账号,照抄即可。所有 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 会先往队列发一条确认消息,你必须确认后订阅才激活:

  1. 队列里收到一条 "Type": "SubscriptionConfirmation" 的消息,里面有 SubscribeURL 字段。
  2. 对这个 URL 发一个 GET 请求即表示同意订阅。
  3. 等几分钟,状态从 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 → 记得 GET SubscribeURL 确认。
  • 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: &notifications.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 跑通顺序

  1. 建 Standard 队列,贴上 3.1 的策略。
  2. client_credentials 换 grantless token → createDestination → 记下 destinationId
  3. 卖家 refresh_token 流程 → createSubscription(填 destinationId)→ 记下 subscriptionId
  4. 触发对应事件(如订单状态变化)→ 消费端就能收到通知。

八、固定值速查

广告 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

九、参考链接

posted @ 2026-06-17 17:57  rm-rf*  阅读(29)  评论(0)    收藏  举报