做 CPS 电商联盟这几年,我见过最多的撕逼不是价格,而是数据:渠道说我带来了 100 单,平台说只有 82 单有效,月底结算差出 18%,双方各拿各的后台截图吵到脸红。而这类纠纷十有八九,根子都在同一个地方 —— Postback(S2S 回传)体系没搭对。
这篇文章把 CPS 回传的完整链路一次讲透:click_id 的生成、传递、落库和过期;回传 URL 的宏参数怎么设计;订单从下单到退款的每个状态怎么逐事件回传、金额口径怎么定;重试和幂等怎么做才不重不漏;平台侧怎么做到"接新渠道不发版";对账表怎么建;最后是渠道方最关心的扣量争议怎么举证。无论你是渠道方要接联盟平台,还是平台方要给渠道回数,这套东西都是通用的。
一、为什么 CPS 必须走 S2S,而不是像素或 SDK
先对齐一个认知:CPS 的转化不发生在点击当下。用户今天点了你的推广链接,可能三天后才下单,七天后才付款,半个月后又退款 —— 一笔佣金从产生到最终能结算,状态会变好几次。前端像素打点只能捕捉"发生了一次",捕捉不了"后来又变了";而 CPS 恰恰是"后来又变了"决定你到底赚不赚钱。
再叠加前端环境的天然不可靠:广告拦截杀像素、跟踪限制掐 Cookie、用户下完单就卸载 App、跨设备下单直接断链 —— 这些在 CPA 场景里顶多丢几个安装,在 CPS 里丢的是真金白银的订单归属。
所以行业通行做法是 S2S(Server to Server)Postback:电商平台的订单状态变化通过 API 同步到联盟平台服务器,联盟平台完成归因后,再通过 HTTP 请求把每一笔转化事件推给渠道方服务器。没有浏览器、没有 App 参与,天然规避前端顽疾;更关键的是,退款冲正这种"负向事件"只有 S2S 能干净地表达 —— 你没法让一个像素"把上周那单的钱扣回去"。
整条链路一句话概括:
渠道投放时带上 click_id → 用户点击、跳转、下单 → 电商订单同步回联盟平台,平台从订单里解析出这个 click_id 完成归因 → 订单每次状态变化,平台按渠道提供的 Postback URL 回传一条事件 → 渠道按约定应答确认接收,失败进重试。
钱能不能算清,取决于 click_id 能不能完整地走完这一圈。 下面这张图是全链路时序,后文所有章节都是在拆解图里的某一步:
图1:CPS S2S Postback 全链路时序:从用户点击到事件回传渠道
二、click_id 的一生:生成、传递、落库、过期
click_id 是渠道生成的点击唯一标识,是整个归因体系的锚点。它的生命周期分四段,每一段都有坑。
第一段:生成与注入。 渠道在投放链接里放宏,比如 {click_id},由渠道的投放系统在用户点击瞬间替换成真实值 —— 通常是一个会话级唯一字符串。生成规则渠道自定,但有三条底线:全局唯一(别用毫秒时间戳裸奔,高并发必撞)、只含 URL 安全字符(出现 &、#、空格的 click_id 在透传链路上必被截断或转义变形)、长度克制(下游电商平台的追踪参数有总长上限,click_id 一家就占掉 100 字符,sub 槽位就没得住了)。第一个坑就在这:宏没被替换、原样传了个字面量 {click_id} 过来,是联调期最高频的事故,平台侧收到的是一堆一模一样的花括号,归因全废。第二个坑是编码:有的渠道系统会把花括号转义成 %7B / %7D 再替换,有的不转义;平台的追踪模板必须显式声明"宏字符保留原文还是 URL 编码",两边不对齐,宏替换引擎可能压根识别不出这是个宏。
第二段:随订单链路透传。 电商平台的联盟链接一般允许挂一个自定义追踪参数(常见叫 tracking_id / event_id 之类),订单产生后会原样吐回给你。问题是这一个参数要装下你所有的归因信息,所以成熟做法是设计一个下划线拼接的复合结构:
{advertiser_id}_{offer_id}_{publisher_id}_{click_id}[_{slot1}_{slot2}...]
前三段是平台内部归因键(广告主、Offer、渠道),第四段固定放 click_id,后面是可变长度的自定义槽位(装渠道的 sub 参数:campaign、creative、placement……)。这里有两个经验之谈:
- click_id 的位置必须固定。解析器按位取值,今天放第四段明天放末段,历史数据的解析就得写两套兼容逻辑 —— 我们真的写过,不想再写第二次。
- 槽位设计成可变长度,而不是固定 N 段补
na。渠道要传的宏参数天差地别,有的只要 click_id,有的要传五六层投放维度,写死段数只会逼运营填一堆占位垃圾。另外注意电商平台对追踪参数通常有长度上限,槽位不是无限的,超长会被静默截断 —— 截断的位置如果恰好在 click_id 中间,这条点击就废了。
第三段:入库落地。 订单同步进来时,平台从追踪参数里解析出 click_id 和归因三元组,连同 SKU 明细一起写入转化明细表,并在这一步就判定"该订单归属哪个渠道、可不可回传、下一个待回传事件是什么"。注意:归因判定发生在入库时,不是回传时 —— 如果入库时 click_id 解析失败(宏没替换、参数被截断、追踪模板对不上),这条转化会带着"解析失败"标记躺在库里,永远进不了回传队列,后面配置改得再对也救不回来。所以联调时第一件事永远是查"点击有没有被正确解析入库",而不是盯着回传日志。
第四段:过期。 click_id 不是永久有效的。两边各有一个时间窗:电商平台侧有归因窗口(点击后 N 天内的订单才算联盟带来的,常见 7~30 天),联盟平台和渠道侧有点击日志保留期。渠道侧要注意:你的点击日志保留期必须长于归因窗口加上最长的退款周期 —— 一笔订单可能在点击后第 25 天成交、第 40 天退款,如果你第 30 天就把点击日志清了,退款冲正回来时你自己都对不上这个 click_id 是谁。跨归因窗口的多次点击按什么规则归属(一般是 last click),也要在对接文档里写死,别留给月底吵。
三、回传 URL 与宏参数设计
回传本体通常就是一个 HTTP GET(也有渠道要求 POST JSON,本节末尾说),单次请求携带一条订单转化记录。一个设计合理的通用回传 URL 长这样:
GET https://pb.example.com/event?click_id={click_id}&order_id={order_id}
&sku_id={sku_id}&event_name={event_name}&event_value={event_value}
&gmv={gmv}&event_time={event_time}¤cy={currency}
&sub1={sub1}&sub2={sub2}&sub3={sub3}&sub4={sub4}&sub5={sub5}
逐个说字段,这张表就是双方对接文档的核心:
| 参数 | 必传 | 作用 | 要点与示例 |
|---|---|---|---|
click_id |
是 | 归因 | 渠道点击 ID,全链路原样透传、不可变形,如 abc123 |
order_id |
是 | 幂等去重 | 平台生成的转化唯一 ID,渠道侧去重、对账全靠它,如 cv_9af2c1 |
sku_id |
是 | 商品维度 | 一笔电商订单可能拆成多条 SKU 转化记录,统一以 order_id 去重 |
event_name |
是 | 事件类型 | 如 purchase / rejected,决定加钱还是冲账 |
event_value |
是 | 佣金金额 | 正向转化为正数(10.5),冲正事件为负数(-10.5) |
gmv |
是 | 订单成交额 | 原始商品成交金额(单价×数量),不按分成比例扣减,如 120.35 |
event_time |
是 | 事件时间 | 建议格式 yyyy-MM-dd HH:mm:ss.SSS,时区口径必须写进文档 |
currency |
是 | 币种 | ISO 4217,同时约束 event_value 和 gmv,缺省 USD |
sub1~`sub5` |
否 | 自定义透传 | 投放侧传什么,回传时原样还什么,平台不解析含义 |
一条真实形态的 purchase 回传(占位域名):
GET https://pb.example.com/event?click_id=abc123&order_id=cv_9af2c1
&sku_id=sku_12345&event_name=purchase&event_value=10.5&gmv=120.35
&event_time=2026-05-10%2012%3A00%3A00.000¤cy=USD
&sub1=campaign_001&sub2=creative_A
