简介:OpenRTB 2.5中文翻译文档是面向国内程序化广告开发者、算法工程师和广告运营人员的协议参考手册,解决英文原版阅读门槛高、术语理解成本高的问题。内容完整覆盖Bid Request与Bid Response、多Impression竞价机会、User与Device定向信息、Site/App环境描述、Ext扩展字段、Bidder Seat出价席位及Price Floor价格保护机制等核心对象,同时讲解GDPR隐私合规、视频音频富媒体扩展和精准地理位置定位等新特性。文档以JSON格式示例穿插说明,便于读者对照实际接口联调。资源为单个PDF文件,大小约1.36MB,已有709人学习,适合广告交易平台、DSP/SSP方向的技术人员作为日常开发与方案评审时的速查材料。
1. 为什么你在对接实时竞价时,需要一份 OpenRTB 2.5 中文文档
做程序化广告的后端同学,十有八九都经历过这种阶段:对方交易平台扔过来一份 OpenRTB 2.5 英文规范,几百页 PDF,字段密密麻麻,光一个 BidRequest 就嵌套了十来个对象。有人硬啃一周,把 impression 当成“印象”,把 seatbid 当成“座位竞价”,等到联调的时候才发现请求里少了 bidfloor 的币种、响应里没解析 nbr,整个排期全砸在翻字段表上。这时候你最需要的,其实是一份靠谱的 OpenRTB 2.5 中文翻译文档——它不是让你扔掉英文原版,而是让你在查字段、拼 JSON、对错误码的时候,能直接看中文语义快速定位,省掉来回翻译的时间。这篇内容我按“能落到代码和排查上”的标准,把中文文档怎么看、怎么用、有哪些坑讲清楚,适合刚接触 RTB 的广告后端、客户端 SDK 开发,以及要评估流量变现平台的技术负责人。
2. OpenRTB 2.5 协议骨架:把 BidRequest、Imp、SeatBid 理成四张表
拿到中文翻译文档,别从头到尾当小说读,那是浪费时间。OpenRTB 2.5 整个协议本质上就是两张请求表加两张响应表:请求侧是 BidRequest 和 Imp,响应侧是 BidResponse 和 SeatBid/Bid。先把这四个对象的关系拆清楚,后面所有字段都只是往里填内容。
2.1 请求侧:BidRequest 与 Imp 是两个正交的维度
BidRequest 是 RTB 请求的最外层容器,它描述的是“一次拍卖请求”的全局信息:请求 id、拍卖类型、超时时间、屏蔽类目、对竞价方的白名单。而真正描述“这个广告位值多少钱、要什么样式”的,是它里面的 imp 数组。imp 全称是 impression,中文文档通常把它翻译成“展示”或“广告位请求”,一个 BidRequest 里可以挂多个 imp,每个 imp 是独立的售卖单元。
我一般会跟团队讲一个类比:BidRequest 是拍卖会的会场信息,imp 是会场里一个个待拍卖的展位。会场可以说今天来了哪些买家、屏蔽哪些品类;每个展位自己说清楚要多大尺寸、底价多少、是不是私有交易。这两个维度是正交的——同一个 app、同一个设备、同一个用户,完全可以在一次请求里带两个尺寸不同的 imp,也可以拆成两次请求各带一个 imp。理解了这个,后面查字段归属就不会乱。
BidRequest 顶层字段里,项目上最常打交道的就这么几个,我按中文文档里常见译法列一下:
| 字段 | 类型 | 必填 | 说明与中文文档译法 |
|---|---|---|---|
| id | string | 是 | 请求唯一 ID,交易平台用它做日志关联,常见译法“请求 ID” |
| imp | object 数组 | 是 | 展示对象数组,至少一个,常见译法“广告位请求/展示” |
| site / app | object | 二者至少一个 | 媒体环境,app 指 App 内流量,site 指网页流量 |
| device | object | 否 | 设备信息,包括 UA、IP、机型、屏幕尺寸 |
| user | object | 否 | 用户信息,包括用户 id、年龄、性别、兴趣 |
| at | integer | 否 | 拍卖类型,1 为第一价格,2 为第二价格,默认 2 |
| tmax | integer | 否 | 最大响应超时,单位毫秒,建议显式设置 |
| bcat / badv | string 数组 | 否 | 屏蔽的广告类目 / 屏蔽的广告主域名 |
| regs | object | 否 | 合规字段,包含 coppa 以及 GDPR 的 ext 扩展 |
| ext | object | 否 | 扩展字段,各交易平台自定义 |
Imp 对象下面又有自己的子对象,banner、video、native、audio 分别对应横幅、视频、原生、音频四类广告形式。一个 imp 里至少出现其中一种。bidfloor 和 bidfloorcur 是一对核心组合:前者是底价,后者是底价币种,默认 USD。很多新手只填 bidfloor 不填币种,交易平台按美元算,流水直接差七倍。
2.2 响应侧:BidResponse、SeatBid、Bid 的嵌套关系
响应侧的层级比请求侧更绕,因为它是三层嵌套:BidResponse 下面挂 seatbid 数组,每个 SeatBid 下面再挂 bid 数组。中文文档里 seat 常被翻译成“席位”,指的是广告交易平台上某个竞价方所在的席位,通俗理解就是“哪个买家出的价”。一个交易平台的一次拍卖,可能有多个席位同时出价,所以 seatbid 是数组。
Bid 对象才是真正装着“谁要投、出多少钱、广告素材是什么”的地方。它的核心字段是 id、impid、price,其中 impid 必须对上请求里 imp 的 id,price 是竞价价格,单位跟随 BidResponse 里的 cur,默认 USD。如果请求里有三个 imp,响应里通常会有多组 bid,每个 bid 通过 impid 指明自己竞的是哪一个。没有 impid 的 bid,最好的情况是被丢进日志,最坏的情况是匹配错广告位直接展示错素材。
BidResponse 顶层字段里有两个容易看漏:nbr 和 cur。nbr 是“没出价原因码”,交易平台在 no bid 的时候会通过它告诉请求方为什么没出价;cur 是货币代码。联调出现大量 no bid,第一件事就是去看 nbr,而不是怀疑自己网络被墙或者超时设置错了。
2.3 中文文档里最容易产生歧义的翻译对照
我翻过不止一版社区翻译文档,发现同一份 OpenRTB 2.5,不同人翻出来的术语能差出十万八千里。最典型的是 impression,有人翻“展示”,有人翻“展现”,还有人直译成“印象”。语义上都不算大错,但你在中文文档里找不到一个词,再去回查英文原版,来回成本很高。
这里把我在几个项目里遇到过的翻译对照整理成表,你手头的中文文档如果和这个不一致,建议以英文键名为准,中文只辅助理解。
| 英文键名 | 常见中文翻译 | 实际含义 |
|---|---|---|
| impression / imp | 展示、展现、印象 | 一次广告位售卖请求的对象 |
| seat | 席位、座位 | 竞价方在交易平台里的投标席位 |
| bidfloor | 底价、最低出价 | 广告位的最低成交价 |
| nurl | 通知 URL、回传地址 | 竞价成功后的通知回调地址 |
| deal | 交易、私有交易 | PMP 里约定好的私有购买条款 |
| bundle | 包名、应用包名 | App 的应用包名或安卓包名 |
| adm | 广告物料、广告标记 | 广告创意内容,可能是 JSON 或 HTML |
| cid / crid | 广告主 ID / 创意 ID | 投放主体标识 |
实践经验是:中文文档只用来建立第一遍认知,真正写代码、配字段、对日志,一定要看英文 JSON 键名。我见过一个项目把文档里的“底价”理解成“当前最高价”,竞价逻辑反了三天。不是翻译错了,而是看中文的人把术语带到了自己熟悉的语境里。协议字段之间是有严格语义边界的,中文只能是拐杖,不能当标准。
3. 从中文文档到能跑的代码:用 Java 解析 BidRequest 与 BidResponse
学完对象骨架,下一步就是把文档翻译成能跑的代码。这一章我用一个相对通用的场景:Java 后端 + Gson 序列化/Josn 解析。你手头如果用 Fastjson、Jackson,原理完全一样,只是注解不同。
3.1 先按中文文档拼一个最小可用的 BidRequest JSON
我建议所有对接都从“最小可用请求”开始。所谓最小,就是只保留必填字段和当前业务要用的字段,先别把文档里的字段全堆上去。多了反而不好定位问题。下面这个 JSON 是一个比较典型的 App 横幅广告位竞价请求:
{ "id": "test-req-001", "at": 2, "tmax": 120, "imp": [ { "id": "imp-001", "tagid": "adslot-home-banner", "bidfloor": 0.5, "bidfloorcur": "USD", "banner": { "w": 320, "h": 50, "pos": 1 }, "secure": 1 } ], "app": { "id": "app-10001", "name": "DemoApp", "bundle": "com.example.demo", "publisher": { "id": "pub-88" } }, "device": { "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)", "ip": "203.0.113.10", "devicetype": 4, "os": "iOS", "osv": "15.0", "w": 375, "h": 812 }, "user": { "id": "user-abc-123" } }这个请求的意图翻译成人话就是:请交易平台帮我拍一个 320x50 的横幅广告位,底价 0.5 美元,媒体是某款 DemoApp,设备是一台 iPhone,用户是 user-abc-123。字段里 at=2 表示第二价格拍卖,tmax=120 告诉对方 120 毫秒内必须响应。至于 app.name 和 app.bundle,虽然是可选字段,但很多交易平台拿去接广告主白名单校验,建议只要知道就填上。
3.2 用 Gson 做反序列化,数据类字段名必须对齐 JSON
接下来是把这段 JSON 映射成 Java 对象。关键点只有一个:字段名必须和协议 JSON 键名完全一致,否则 Gson 反序列化出来全是 null。Gson 默认按字段名匹配,而协议的键名全是 snake_case,比如 bidfloor、impid、seatbid,Java 字段名如果用 camelCase,就得加 @SerializedName 注解:
public class BidRequest { @SerializedName("id") public String id; @SerializedName("imp") public List<Impression> imp; @SerializedName("app") public App app; @SerializedName("site") public Site site; @SerializedName("device") public Device device; @SerializedName("user") public User user; @SerializedName("at") public Integer at; @SerializedName("tmax") public Integer tmax; } public class Impression { @SerializedName("id") public String id; @SerializedName("banner") public Banner banner; @SerializedName("video") public Video video; @SerializedName("native") public NativeAd nativeAd; @SerializedName("bidfloor") public Double bidfloor; @SerializedName("bidfloorcur") public String bidfloorcur; }序列化的时候用同一个类,Gson 也能正确输出 snake_case 的 JSON。有一点要特别提醒:浮点字段请用 Double 或者 BigDecimal,不要用 Float。bidfloor 和 price 这类字段在真实流量里经常出现 0.37、0.625 这种精度,Float 会丢精度,对账的时候差一分钱都查得头疼。Integer 字段全部用包装类型而不是 int,这样空值可以被序列化成 null 而不是 0,避免把“没传”和“传了 0”混淆。
3.3 字段默认值与必填项:文档表格没有明写的三处代码风险
中文文档会给你字段表,但默认值、条件必填这种细节,有时表格里没写全。这三个坑是代码层面最常翻车的,先列出来:
第一,at 字段的默认值是 2,也就是第二价格拍卖。但国内不少交易平台实际用的是第一价格。如果你不显式传 at,对方服务端可能按第二价格处理,而你的收入模型按第一价格测算,最后的结算差价就是纯亏。代码里显式设置 at,不要依赖默认值。
第二,bidfloorcur 默认是 USD。你可以不传,但你的广告位如果是境内人民币流量,请显式传 "CNY"。否则请求发出去,交易平台理解成美元底价,而实际上你的目标客户是按人民币出价的,两边不在一个坐标系里,要么流拍要么低价成交。
第三,imp 里至少要有一个广告形式子对象。banner、video、native、audio 至少出现一个,否则这个请求非法。很多交易平台面对这种请求直接忽略整个 imp,甚至直接 no bid。代码里在拼请求时,要校验 imp 内部是否为空,不能只校验 imp 数组不为空。
4. OpenRTB 2.5 对接避坑清单:五个让人翻车的字段与流程问题
这一章是纯踩坑记录。每一个都是我在不同项目里真实遇到过的现象,前因后果和解决方案都走了一遍。你对照着排查,至少能省掉三分之二的联调时间。
4.1 现象:请求发出去交易平台一直不回 bid
联调第一天请求发过去,等了半分钟一个响应都没有,超时设置从 120ms 改到 500ms,照样空手而归。抓包看请求也发出去了,状态码 200。问题出在请求体本身,而不是网络。
原因:请求里的 at 没传,tmax 也没传,交易平台默认按 2 处理,但我们的竞价策略机只在第一价格拍卖时出价,服务端判断完直接拒了。另外,没有 tmax 时,某些交易平台会默认一个很小的超时值,你的服务端还没来得及处理,对方已经放弃了。
解决:显式设置 at 和 tmax。at 按你和交易平台商务确认的拍卖模式来,tmax 设置成 200 或 300 毫秒。如果你们的竞价策略是抖动的,tmax 要覆盖策略机的最坏耗时。这个坑在中文文档里不算细节,但极其容易漏。
4.2 现象:adomain 传错层级导致 DSP 拒投
响应里明明返回了 bid,也收到了 nurl 回调,但广告主侧迟迟不起量。查交易平台后台,看到不少 bid 被标记为“广告主域名校验失败”。这里问题出在响应方(DSP 侧)填了 adomain,但填的是 App 名称而不是广告主域名。
原因:adomain 字段在 Bid 对象里,类型是 string 数组,存放的是广告主的域名列表,比如 example.com。它被交易平台和媒体侧拿去做过滤和品牌安全校验。很多新手把应用商店的开发者名称或者 App 名填进去,校验自然不过。
解决:回到中文文档里看 adomain 的字段描述,确认它强调的是“广告主域名”。把正确的投放域名放进数组。如果你们是中间件,上游传来的 adomain 也要做一层过滤,把明显是应用名的值剔除掉,避免污染下游。
4.3 现象:price 用 float 解析导致金额对不上
一个响应里返回的价格是 3.725,用 float 接回来变成 3.7249999,存到数据库再取出来,下游对账系统直接报不平。这种问题最玄学,白天看不出来,一到月底对账就炸。
原因:OpenRTB 2.5 里 price、bidfloor 都是 float 类型,但 JSON 反序列化的时候如果用 Float、float,精度丢失是必然的。尤其竞价价格这种带小数的金额,float 只有 7 位有效数字,0.625 这种还能存下,3.725 这种就完了。
解决:所有金额类型统一用 Double 接收,入库用 Decimal 或把金额扩大成整数存储。线上代码里禁止用 float 表示任何金额字段。我在团队里定的规矩是:出现 float 关键字做金额的代码,直接打回。
4.4 现象:nurl 宏替换把价格丢了
竞价成功以后,请求方拿到 nurl,然后把 nurl 里的宏替换成实际成交价再回传给广告主。结果日志里看到的成交价是 0,或者干脆原样输出了 ${AUCTION_PRICE},广告主侧统计不到真实价格。
原因:nurl 是通知 URL,协议规定竞价成功后请求方要主动访问它,并把里面的宏替换掉。文档里写法是${AUCTION_PRICE},但实际项目中有的交易平台用%%AUCTION_PRICE%%,有的用[AUCTION_PRICE]。你按文档的宏去替换,替换了个寂寞。
解决:对接前先问清交易平台实际支持的宏语法,或者干脆抓一单真实成功的 nurl 回调来看。替换完以后,在日志里把替换前后的 URL 都打出来,肉眼确认价格已经落在参数里。这个坑是联调后期最容易踩的,因为它影响的是数据回传,不是投放流程,很多项目上线了大半年才发现。
4.5 现象:响应里出现文档没写过的 ext 字段,解析直接崩了
正常响应解析一切正常,某天交易平台在响应里加了一个 ext.custom_data 字段,我们的解析代码直接抛异常,整个线程废弃。排查下来,是用了严格模式的反序列化,不允许未知字段出现。
原因:OpenRTB 2.5 文档里的 ext 字段是各交易平台自己扩展的,不写进标准。有的平台把 ext 用在响应里传竞价失败原因,有的传广告主 ID,格式各不相同。严格模式解析器看到未知字段就抛异常,等于把协议扩展变成了事故入口。
解决:反序列化配置要允许未知字段。Gson 默认就是忽略未知字段的;用 Jackson 时记得关掉 FAIL_ON_UNKNOWN_PROPERTIES。ext 字段统一用 JsonObject 或 Map 接收,不要一开始就强绑定具体类型。你不知道别人明天会往里塞什么。
5. 用中文文档反向校验线上请求:NBR 错误码与必填字段自查
联调通过不代表万事大吉。上线以后交易平台返回 no bid 的比例突然升高,这时候最怕的是靠肉眼翻日志。正确做法是把中文文档的必填字段表转成一个静态校验工具,再把 NBR 错误码接进告警。
5.1 把必填字段列成自检脚本
OpenRTB 2.5 的字段表里没有那种完整的 JSON Schema,但每个对象都标了必填项。把这些必填项转成脚本,在请求发出前做一次静态检查,能拦截掉大部分低级错误。我用 Python 写过一个简单的检查器,核心就是这个思路:
import json import sys REQUIRED_TOP_LEVEL = {"id", "imp"} REQUIRED_IMP = {"id"} AD_TYPE_KEYS = {"banner", "video", "native", "audio"} def check_bid_request(path): with open(path, encoding="utf-8") as fp: req = json.load(fp) missing = REQUIRED_TOP_LEVEL - set(req.keys()) if missing: print("[FAIL] BidRequest missing:", missing) sys.exit(1) for idx, imp in enumerate(req.get("imp", [])): imp_keys = set(imp.keys()) if not (REQUIRED_IMP <= imp_keys): print(f"[FAIL] imp[{idx}] missing id") if not (imp_keys & AD_TYPE_KEYS): print(f"[FAIL] imp[{idx}] has no banner/video/native/audio") if "bidfloor" in imp and "bidfloorcur" in imp: print(f"[PASS] imp[{idx}] bidfloor={imp['bidfloor']} {imp['bidfloorcur']}") print("[PASS] basic check done") if __name__ == "__main__": check_bid_request(sys.argv[1])这个脚本不长,但把最容易出问题的三件事都查了:顶层 id 和 imp 数组、每个 imp 必须要有 id、广告形式必须至少出现一个。脚本跑完再用文档字段表人工过一遍业务字段,基本能把低级的拼错字段问题挡在测试环境。参数说明很简单:第一个参数是 JSON 文件路径,脚本只做检查不发出网络请求,可以打进 CI。
5.2 NBR 错误码对照表:交易平台说 no bid 时的排查入口
如果交易平台在响应里带了 nbr 字段,那是最有价值的排查线索。中文文档里通常都会附 NBR 错误码表,我把常见码整理成下面这份,你排查时直接对照:
| nbr | 含义 | 排查方向 |
|---|---|---|
| 0 | 未知错误 | 看交易平台日志,联系对方接口人 |
| 1 | 技术错误 | 请求格式是否有异常 |
| 2 | 无效请求 | 必填字段缺失或类型错误 |
| 3 | 已知设备但被屏蔽 | 检查设备过滤白名单 |
| 4 | 未授权设备 | 设备未通过认证或未授权 |
| 5 | 屏蔽的设备 | 设备在黑名单,一般是作弊名单 |
| 6 | 广告类目被屏蔽 | 检查 bcat 和广告物料的类目 |
| 7 | 广告主被屏蔽 | 检查 badv 或广告主域名黑名单 |
| 8 | 出价低于底价 | 检查 bidfloor 和竞价的 price |
| 9 | 未找到可用创意 | 广告物料没有通过审核或尺寸不匹配 |
实际排查时,对照 nbr=8 的场景最多。不是你的价格算错了,而是请求里的 bidfloor 币种和竞价价格币种不一致,两边压根不在一个数量级。优先检查 bidfloorcur 与响应 cur 是否一致。
5.3 从日志里还原一次请求-响应流程
线上定位问题的时候,只靠交易平台后台的报错截图是不够的。我的习惯是每次请求都打一条结构化日志,包含请求 id、imp id、请求发出时间、响应返回时间、bid 数量、nbr 值。日志统一进 ELK,出了问题直接按一个 request id 把请求和响应拼起来看。
# 用 curl 模拟一次请求,把响应原样保留 curl -s -X POST \ -H "Content-Type: application/json" \ -d @bid_request.json \ https://adx.example.com/openrtb/bid \ -o bid_response.json # 格式化响应并提取关键字段 python3 -c " import json resp = json.load(open('bid_response.json')) print('response id :', resp.get('id')) print('nbr :', resp.get('nbr')) print('seatbid count :', len(resp.get('seatbid', []))) for seatbid in resp.get('seatbid', []): for bid in seatbid.get('bid', []): print('bid impid/price :', bid.get('impid'), bid.get('price')) "这里的关键是不要只看响应整体。日志里必须拆到 bid 粒度,否则你不知道哪个 imp 被投了、哪个被放弃了。联调阶段把这段脚本放进一个 shell 脚本里跑,效率比在 IDE 里打断点高得多。
6. 把中文文档当协议词典维护:团队的长期打法与扩展用法
聊完落地和排查,最后一件事是让这份文档的价值持续增值。OpenRTB 2.5 是一个标准,但各家交易平台都有私有扩展,文档本身会过期,翻译也可能不准。我的做法是把中文文档当作团队的“协议词典”来维护,而不是一次性的参考资料。
具体来说,团队里可以约定三条规矩。第一,所有对接项目的代码注释里,涉及协议字段的地方统一引用文档里的中文术语,不要各写各的“座位”“位置”“席位”。第二,每个项目联调结束后,把发现的字段歧义、坑、特殊 ext 回写到共享文档的附录里,后面的人不用重新踩一遍。第三,每隔一段时间对照英文原版做一次 diff——重点不是看翻译有没有错,而是看有没有漏掉后来发布的补充说明和修订。
除了当词典,它还有一个扩展用法:生成离线字段速查表。把 BidRequest、BidResponse 的所有常用字段整理成 Markdown 表,挂在项目 Wiki 里。字段不多的时候不觉得,等广告形式增加到视频、原生、激励视频之后,你会发现手写 JSON 完全靠翻文档效率太低,直接查团队维护的速查表会快很多。另一个用途是给新来的同事做 30 分钟的协议讲解,拿一个真实请求例子从发起竞价到竞价成功走一遍,比背诵字段定义有效得多。
最后说个我自己的教训。很早之前接过一个移动广告聚合 SDK 的需求,我当时仗着看过几页文档没细读,把 Banner 的 w/h 在代码里偷懒默认成 0,结果线上大量交易平台返回错误码,排查了一整天才发现是尺寸匹配问题。从那以后我不管对接哪个平台,都会先把最小请求跑通、再逐步加字段,每加一个都在日志里确认一次,而不是直接梭哈全量字段。这个习惯后来帮我节省了不知道多少个夜班。OpenRTB 2.5 这份协议不复杂,但细节多,翻译文档给了你一个快速入口,真正能走多远,还是靠你在每个字段上多较真一次。希望帮到你。
本文还有配套的精品资源,点击获取