Xiaohongshu-PGY-API

蒲公英笔记博主广场(找博主/选号)/PGY note blogger square

Changed on

[中文]

用途:

  • 复刻蒲公英网页「找博主 → 笔记博主广场」的搜索与筛选,约 60 个筛选条件按 7 组传入
  • 每条结果内嵌约 140 个字段(报价、阅读/互动中位数、直播场次等),通常不必再逐个调博主详情类接口

1. 搜索与排序

参数说明
keyword搜索词;空=只筛选不搜索
search_type1=按笔记关键词找博主(默认)0=按昵称搜
column排序字段,共 34 个:comprehensiverank=智能排序(默认) fansCount=粉丝数 price=全部报价 clickNum=阅读中位数-日常 mEngagementNum=互动中位数-日常 accumCommonImpMedinNum30d=曝光中位数-日常 accumPicCommonImpMedinNum30d=图文曝光中位数-日常 accumVideoCommonImpMedinNum30d=视频曝光中位数-日常 pictureClickMidNum=图文阅读中位数-日常 videoClickMidNum=视频阅读中位数-日常 pictureInterMidNum=图文互动中位数-日常 videoInterMidNum=视频互动中位数-日常 thousandLikePercent30=千赞笔记比例 pictureThousandLikePercent30=图文千赞比例 videoThousandLikePercent30=视频千赞比例 hundredLikePercent30=百赞笔记比例 pictureHundredLikePercent30=图文百赞比例 videoHundredLikePercent30=视频百赞比例 videoFinishRate=视频完播率 accumCoopImpMedinNum30d=曝光中位数-合作 readMidCoop30=阅读中位数-合作 interMidCoop30=互动中位数-合作 mCpuvNum30d=外溢进店中位数-合作 picturePrice=图文报价 videoPrice=视频报价 pictureReadCost=图文预估阅读单价 videoReadCost=视频预估阅读单价 inviteReply48hNumRatio=邀约48h回复率 fans30GrowthRate=粉丝量变化幅度 fansActiveIn28dLv=活跃粉丝占比 fansEngageNum30dLv=互动粉丝占比 kliveCnt30d=近30天直播场次 avgLiveViewerNum=场均观播人数 avgAgmv90d=场均销售额
sortdesc 降序(默认)/ asc 升序
brand_user_id品牌方/广告主用户ID,可不填。带上后按该品牌做个性化推荐与已合作标记
similar_user_id找相似博主:填一个博主的 userId,返回与他相似的博主,并且可以叠加下面所有筛选
similar_word配合 similar_user_id 的博主昵称,仅用于回显、不影响结果

similar_user_id 与 /get_blogger_similar 是两条不同链路:前者走本接口、能叠加筛选; 后者只按相似度返回、不支持筛选、每页最多 4 条。想「找相似 + 再按粉丝量/报价筛」用前者。

2. 筛选分组

筛选按维度分成 7 组,每组是一个对象:

分组覆盖的筛选
goal营销目标(曝光 / 种草 / 转化 × 指标 × 成本或规模)
blogger博主类目、人设、擅长内容、内容题材、二十大人群、行业特色画像、预估消费行为、笔记类目、性别、地域、签约情况
fans粉丝量、粉丝年龄、粉丝性别、粉丝地域、婚恋状态、消费水平、母婴阶段、手机价格、手机品牌
note日常笔记的曝光/阅读/互动中位数、千赞笔记比例、笔记类型
coop合作报价、合作订单数、合作信用度、近期合作行业与品牌、传播规模、预估 CPM / 阅读单价 / 互动单价、外溢进店单价
live近 30 天直播场次、场均观播人数、场均销售额
flags明星、优质博主、新锐博主、笔记+直播均可合作、意向行业匹配、行业推荐博主、热门活动、剔除低活/掉粉/已合作/已邀约

数值区间统一写成 {"min": 10000, "max": 50000}。两端都可省略:省略 min 视为 0, 省略 max 视为不限;但两端全空会 422。

3. 各筛选填什么值

筛选值分三类:照表填的固定枚举、要先查字典的、直接填名称的。 填错不会报错,只会安静返回 0 条 —— 所以拿不到结果时先回来核对这一节。

3.1 固定枚举(照表填,填表外的值直接 422)

左列是请求 JSON 里的路径,例如 fans.age 就是 {"fans": {"age": 3}}。

参数路径对应筛选全部取值
search_type搜笔记 / 搜昵称1=搜笔记(默认)0=搜昵称
sort排序方向desc=降序(默认)asc=升序
blogger.gender性别男 女
blogger.signed签约情况-1=不限 0=个人博主 1=机构博主
fans.age粉丝年龄0=不限 1=<18 2=1824 3=2534 4=35~44 5=>44 占比高
fans.gender粉丝性别0=不限 1=男性占比高 2=女性占比高
fans.marital_status婚恋状态-1=不限 0=未婚 1=已婚
fans.consumption_level消费水平-1=不限 0=低消费 1=中消费 2=高消费
fans.child_age(数组)母婴阶段0=备孕 2=0-6月 3=7-12月 4=1-3岁 5=4-6岁 6=7-12岁 7=孕早期 8=孕晚期
fans.device_price(数组)手机价格0-999 1000-1999 2000-2999 3000-3999 4000-4999 5000-5999 6000-6999 7000-7999 8000+
fans.device_brand(数组)手机品牌苹果 华为 OPPO VIVO 荣耀 小米 一加 魅族 中兴 联想
note.note_type笔记类型0=不限 1=图文笔记为主 2=视频笔记为主
coop.industry近期合作行业不限 美妆个护 食品饮料 母婴 3c及电器 日用百货 服装配饰 互联网 生活服务 家居建材 汽车
column排序字段34 个,见上面第 1 节的排序字段表
goal.market_target营销目标见该字段在 Swagger 中的说明(曝光/种草/转化 × 指标 × 成本或规模)

3.2 需要先查字典的筛选

下列筛选的可选项由平台维护、会随运营调整。先调 /get_blogger_filter_options 拿字典,再按对应层级填。字典基本不变,建议缓存后复用,不必每次筛选前都调。

这些参数的写法不统一,下表是唯一可靠的对照。 大多数是「裸名、任意层级、可混填」, 但有三个例外(audience20 要复合串、note_scene / consumer_behavior 要全路径、 industry_crowd 要数字 ID),照着别的参数的写法填会失败。

参数路径对应筛选字典键填什么能填哪一层
blogger.content_tag博主类目contentTags裸名一级 / 二级,可混填
blogger.personal_tags博主人设personalTagsV2裸 tagName一级 / 二级 / 叶子,可混填
blogger.feature_tags擅长内容featureTags裸名一级 / 二级,可混填
blogger.location地域(博主所在地)areas裸地名省 / 市 / 区县,可混填
fans.location粉丝地域fansAreas裸地名省 / 市(没有区县),可混填
blogger.content_theme内容题材contentTheme裸 label只有一级,子项无效
blogger.audience20二十大人群audience20一级裸名,或 "一级 一级-二级"二级必须用复合串
blogger.note_scene笔记类目contentScenes含根节点的空格全路径任意深度
blogger.consumer_behavior预估消费行为consumerBehavior含根节点的空格全路径任意深度
blogger.industry_crowd行业特色画像automotiveIndustryTag最深层叶子的 value(数字 ID)只有叶子
flags.industry_first / industry_second行业(不是博主类目)industryTags裸名,单值不是数组一级 / 二级各一个
flags.activity_codes热门活动activities每项的 code—

数组型参数一律是「或」,多个值取并集去重;上限 30 项、单项 64 字符。

填错了会怎样:两种失败,都不报错
失败形态表现哪些参数
过度过滤total=0,看起来像「真的没有博主」除 consumer_behavior 外的全部
静默不过滤返回未经筛选的结果,你以为筛了其实没筛consumer_behavior

实测(粉丝 100 万~120 万,无其它筛选时 total=455):

  • content_tag=["护肤品"](多一个字)→ total=0
  • fans.location=["浙江省"](多一个「省」)→ total=0
  • audience20=["挑战极限者"](照字典 label 裸填)→ total=0
  • note_scene=["选车攻略"](只填末段,没填全路径)→ total=0
  • industry_crowd=["身心疗愈兴趣人群"](填了 label 不是数字 ID)→ total=0
  • consumer_behavior=["保时捷911"](裸叶子名)→ total=455,等于没筛 ;["zzz不存在zzz"] 同样 455。只有全路径写法才真的生效 (["内容行为预估 汽车"] → 54)。

看到 total=0 时先怀疑取值写法,再怀疑「真的没有符合条件的博主」; 看到结果多得可疑时,检查是不是 consumer_behavior 被静默忽略了。 所有取值都应从 /get_blogger_filter_options 里原样复制,不要手写。

博主类目(blogger.content_tag)怎么多选

网页上勾了好几个类目、有的勾「全部」有的只勾几个子项时,就按下面这样填。

contentTags 是两层树:taxonomy1Tag 是一级(美妆、护肤、个人护理…共 28 个), taxonomy2Tags[] 是它的子项。一级名和二级名混在同一个扁平数组里就行, 不用分开传、也没有嵌套结构。二级名全局唯一、也不与一级名重名,所以不会有歧义。

举个网页上的实际场景:美妆勾了「全部」,护肤只勾了 2 个子项,个人护理也勾了 2 个:

{"blogger": {"content_tag": ["美妆", "面部保养", "面部清洁", "头发产品", "身体护理"]}}

其中 美妆 是一级名(= 该类目全部),后面四个是二级名 (面部保养/面部清洁 属于护肤,头发产品/身体护理 属于个人护理)。

实测规则(固定同一组粉丝量条件、只改类目,对比返回的 total):

传什么total说明
["面部保养"]200—
["面部清洁"]2—
["面部保养","面部清洁"]201多个之间是「或」,并集去重(有 1 个博主两个都占)
["护肤"]379一级名 = 该类目全部
护肤的全部 4 个二级328比一级名少 51 个
["护肤","面部保养"]379父子同填不报错,但子项本就被父项包含,等于只填父
["护肤品"](错别字)0静默返回 0 条、不报错

两条要特别注意:

  • 勾「全部」就填一级名,别自己展开成它的全部二级名。 上表里护肤展开后少了 51 个博主 —— 有些博主只挂了一级类目、没有二级标签,展开就把他们漏掉了。
  • 名字填错不会报错,只会返回 0 条。 务必从 /get_blogger_filter_options 的 contentTags 里原样复制,别手写。看到 total=0 时先怀疑名字,再怀疑「真的没有博主」。

数组最多 30 项,单个标签名最长 64 字符。

地域(blogger.location / fans.location)怎么多选

和类目一样是扁平的:直接填地名,省和市可以混在一起,多个之间是「或」。 网页上浙江勾了「全部」,江苏只勾了 2 个市,广东也勾了 2 个:

{"fans": {"location": ["浙江", "南京", "苏州", "广州", "深圳"]}}

要选整个省就填省名,不用展开成它下面所有的市。博主地域多一层区县,写法一样:

{"blogger": {"location": ["上海", "杭州", "上城区"]}}

上游对这两个字段的要求其实相反(博主地域吃裸地名、粉丝地域吃完整路径 "中国 浙江 杭州"),上游服务会各自转好,你填裸名就行。fans.location 侧 实际会转成:浙江→中国 浙江(不会展开成 11 个市)、杭州→中国 浙江 杭州、 吉林→中国 吉林 和 中国 吉林 吉林(省市同名,两条都发,「或」)。 字典里没有的名字(如 浙江省)原样透传 → total=0。

注意 fans.location 这棵树只到市,没有区县(blogger.location 才有)。

二十大人群(blogger.audience20)怎么多选

这个和上面两个不一样,不是扁平裸名。 二级必须写成 "一级 一级-二级" —— 一级名要出现两次,中间一个空格,后半段用连字符。

网页上自在户外勾了「全部」,自由畅行只勾了 2 个子项,运动焕活也勾了 2 个 (实测 total=169):

{"blogger": {"audience20": [
    "自在户外",
    "自由畅行 自由畅行-都市漫游家", "自由畅行 自由畅行-静奢新贵",
    "运动焕活 运动焕活-轻松健体派", "运动焕活 运动焕活-线条雕塑家"
]}}

注意对比:勾「全部」的那个只写一级裸名,勾了具体子项的每一个都要写成复合串。

实测(基线 455):["自在户外"]=135、["自在户外 自在户外-挑战极限者"]=9、 ["挑战极限者"]=0(照字典 label 裸填,静默返回 0)、两个复合值=18(「或」)、 ["自由畅行","自在户外 自在户外-挑战极限者"]=176(单独 自由畅行=175,可混填)。

博主人设 / 擅长内容 —— 任意层级都能填

personalTagsV2 是三层树,featureTags 是两层,任意一层都能填、可以混填。 想要整组就填那一组的名字,不用把叶子一个个列出来。

{"blogger": {"personal_tags": ["职业身份", "妈妈", "留学背景"],
             "feature_tags": ["风格", "vlog"]}}

实测:personal_tags 一级 职业身份=91、二级 互联网=8、叶子 妈妈=31, ["职业身份","妈妈"]=114(并集去重);feature_tags 一级 形式=183、风格=37, 二级 vlog=136、韩系=19,["风格","vlog"]=150。

内容题材(blogger.content_theme)—— 只能填一级

例外:只有一级 label 有效,填子项一律 0 条。实测 ["通用"]=366, 其子项 ["口播"]=0。想按子项筛只能退而求其次用 feature_tags。

笔记类目 / 预估消费行为 —— 必须填「含根节点的空格全路径」

这两个不能填裸名,要把从根节点到目标节点每一层用空格连起来,可以停在任意深度:

{"blogger": {
    "note_scene": ["汽车 理性决策", "汽车 理性决策 选车攻略 政策"],
    "consumer_behavior": ["内容行为预估 汽车"]
}}

实测 note_scene:["汽车"]=75、["汽车 理性决策"]=23、四级=1, 而只填末段 ["选车攻略"]=0。

⚠️ consumer_behavior 填错的后果和别的参数不一样:不是返回 0 条,而是 整个筛选被静默忽略。实测 ["保时捷911"](裸叶子名)和 ["zzz不存在zzz"] 都返回 455 —— 和完全不加这个筛选一模一样;只有全路径写法才生效 (["内容行为预估 汽车"]=54)。用它时务必用加/不加两次请求的 total 对比确认。 另外这个字典按账号灰度,有些账号取到 null,那就用不了。

行业特色画像(blogger.industry_crowd)—— 只能填数字 ID

四层树,但只有最深层叶子的 value 有效,而且是数字 ID,不是 label。 实测 ["17121263"]=110,填对应 label ["身心疗愈兴趣人群"]=0。

行业(flags.industry_first / industry_second)—— 单值,不能多选

这两个是字符串不是数组,各只能填一个,且要配套使用(二级必须属于一级):

{"flags": {"industry_first": "美妆个护", "industry_second": "护肤"}}

注意这是「行业」,和「博主类目」(blogger.content_tag)是两套完全不同的分类。

3.3 直接填名称的筛选

参数路径对应筛选怎么填
coop.brand_ids近期合作品牌品牌方 userId 列表(平台暂无品牌搜索接口,userId 需自行提供)

想按粉丝人群圈博主,用 blogger.audience20(二十大人群)而不是 coop.brand_ids。

3.4 区间型字段(值写成 {"min": x, "max": y})

参数路径对应网页筛选
fans.count粉丝量
note.imp_median / note.read_median / note.inter_median日常笔记的曝光 / 阅读 / 互动中位数
note.thousand_like_percent千赞笔记比例(单位 %)
coop.pic_price / coop.video_price合作报价(图文 / 视频一口价,元)
coop.order_count合作订单数
coop.reply_48h_ratio合作信用度 → 48h 邀约回复率(%)
coop.imp_median / coop.read_median / coop.inter_median传播规模(合作笔记的中位数)
coop.cpuv / coop.estimate_cpuvCPUV / 外溢进店单价
coop.pic_cpm / coop.video_cpm预估 CPM(图文 / 视频)
coop.pic_read_price / coop.video_read_price预估阅读单价(图文 / 视频)
coop.pic_engage_cost / coop.video_engage_cost预估互动单价(图文 / 视频)
live.live_count / live.avg_viewer / live.avg_gmv近 30 天直播场次 / 场均观播 / 场均销售额

3.5 开关型字段(值写 true)

参数路径对应网页筛选
flags.is_star / flags.high_quality / flags.new_high_quality明星 / 优质博主 / 新锐博主
flags.note_and_live笔记 + 直播均可合作
flags.filter_intention意向行业匹配
flags.exclude_low_active / flags.exclude_fans_down剔除低活 / 掉粉博主
flags.exclude_cooperated / flags.exclude_invited剔除已合作 / 已邀约博主
coop.exclude_brands配合 coop.brand_ids:true = 排除与这些品牌合作过的博主

此外 flags.industry_first / flags.industry_second 是行业推荐博主的一级/二级行业名, flags.activity_codes 是热门活动 code(取自 /get_blogger_filter_options 的 activities)。 枚举型字段(性别、签约情况、粉丝年龄等)的完整取值见各字段在 Swagger 中的说明。

4. 更多示例

本页最下方的 [示例/Example] 区里有 15 条可直接照抄的请求体,覆盖关键词搜索、画像选号、预算/性价比、直播带货、找相似博主、地域投放等场景,外加一节常见错误写法(那几种不会报错、只会安静返回 0 条)。

5. 翻页

参数取值说明
page_num1-250上游只放出前 5000 条 = 250 页,第 251 页起返回空数组
page_size1-20蒲公英网页固定 20。建议保持 20:上游会对当页结果做二次过滤(下架、不可合作等),取太小可能整页被过滤成 0 条

翻页只改 page_num,其余筛选必须原样保持不变,否则两页口径不一致会重复或遗漏。 total 不是命中总数,是可翻上限,恒为 5000。想缩小结果集请加筛选,而不是指望翻到底。

6. 返回

  • kols[]: 命中的博主,每条约 140 个字段
    • 身份: userId / name / redId / headPhoto / location / gender
    • 标签: contentTags / featureTags / personalTags / tradeType
    • 数据: fansNum / clickMidNum 阅读中位数 / interMidNum 互动中位数 / fans30GrowthRate 粉丝增速 / kliveCnt30d 直播场次
    • 商业: picturePrice / videoPrice / lowerPrice / estimatePictureCpm / cooperateState
  • total: 可翻上限(恒 5000)
  • trackId / highlightWords

7. 注意:

  • 字段名写错会直接 422,不会被静默忽略
  • 树形筛选的可选值请用 /get_blogger_filter_options 实时拉取,不要写死

响应结构(取数要剥两层):

  • resp["data"] 是蒲公英原样信封(含 code/msg/success),业务数据还在它里面一层
  • 正确取法:data = resp["data"]["data"];下面「返回」列的字段都在这一层下

计费:

  • HTTP 200 → 计费。包含「查无结果」(data 为 null,如 ID 不存在或筛选无命中)—— 请求已送达蒲公英,同样扣费,不要因为拿不到数据就重试
  • HTTP 400 → 不计费(参数格式非法,或上游资源不足/异常)

[English]

Purpose:

  • Full replica of PGY "Find Bloggers → Note Blogger Square"; ~60 filters grouped into 7 objects
  • Each result embeds ~140 fields (quotes, read/engagement medians, live sessions, ...)

1. Search & Sort

ParamDescription
keywordSearch term; empty = filter only
search_type1=find bloggers by note keyword (default), 0=search by nickname
columnSort field, 34 options — see the Chinese table above for the full list with labels
similar_user_idFind similar bloggers: pass a blogger's userId; results can be combined with every filter below
similar_wordNickname echo for similar_user_id; does not affect results
sortdesc (default) / asc
brand_user_idBrand/advertiser user ID, optional; enables brand-specific recommendation and "already cooperated" marks

2. Filter Groups

GroupCovers
goalMarketing goal (exposure / seeding / conversion × metric × cost or scale)
bloggerCategory, persona, expertise, content theme, top-20 crowds, industry-specific portrait, estimated consumption, note category, gender, location, agency status
fansFans count, age, gender, location, marital status, consumption level, parenting stage, phone price, phone brand
noteOrganic note impression / read / engagement medians, 1000-like ratio, note type
coopQuotes, order count, credit score, recent industries & brands, reach scale, estimated CPM / read cost / engagement cost, store-visit cost
liveLive sessions in last 30d, average viewers, average GMV
flagsCelebrity, high-quality, rising bloggers, note+live capable, intention match, industry recommendation, campaigns, exclude inactive / fans-declining / already-cooperated / already-invited

Numeric ranges are written as {"min": 10000, "max": 50000}. Either bound may be omitted (omitting min means 0, omitting max means unlimited), but omitting both returns 422.

3. What Values Go In Each Filter

Filter values come in three kinds: fixed enums (copy from the table), dictionary-backed (fetch first), and plain names. A wrong value does not raise — it just silently returns 0 results, so check this section first when a query comes back empty.

3.1 Fixed enums (values outside the table return 422)

PathWeb filterAllowed values
search_typeSearch notes / nickname1=notes (default) 0=nickname
sortSort directiondesc (default) / asc
blogger.genderGender男 女
blogger.signedAgency status-1=any 0=individual 1=agency
fans.ageFans age0=any 1=<18 2=1824 3=2534 4=35~44 5=>44
fans.genderFans gender0=any 1=male-skewed 2=female-skewed
fans.marital_statusMarital status-1=any 0=single 1=married
fans.consumption_levelConsumption level-1=any 0=low 1=mid 2=high
fans.child_age (array)Parenting stage0 2 3 4 5 6 7 8
fans.device_price (array)Phone price0-999 … 8000+
fans.device_brand (array)Phone brand苹果 华为 OPPO VIVO 荣耀 小米 一加 魅族 中兴 联想
note.note_typeNote type0=any 1=image-text 2=video
coop.industryRecent industry不限 美妆个护 食品饮料 母婴 3c及电器 日用百货 服装配饰 互联网 生活服务 家居建材 汽车
columnSort field34 options — see the sort table in section 1

3.2 Dictionary-backed filters

Call /get_blogger_filter_options first, then fill values from the matching level. The dictionary rarely changes — cache it rather than calling it before every search.

These parameters do NOT share one convention — this table is the only reliable reference. Most take plain names at any level and can be mixed, but three are exceptions (audience20 needs a compound string, note_scene / consumer_behavior need a full path, industry_crowd needs a numeric ID). Copying the convention from another parameter will fail.

PathWeb filterDict keyWhat to passWhich level
blogger.content_tagBlogger categorycontentTagsplain namelevel 1 / 2, mixable
blogger.personal_tagsPersonapersonalTagsV2plain tagNamelevel 1 / 2 / leaf, mixable
blogger.feature_tagsExpertisefeatureTagsplain namelevel 1 / 2, mixable
blogger.locationBlogger locationareasplain place nameprovince / city / district, mixable
fans.locationFans locationfansAreasplain place nameprovince / city (no district), mixable
blogger.content_themeContent themecontentThemeplain labellevel 1 only, children invalid
blogger.audience20Top-20 crowdsaudience20level-1 name, or "L1 L1-L2"level 2 needs the compound form
blogger.note_sceneNote categorycontentScenesspace-joined full path incl. rootany depth
blogger.consumer_behaviorEstimated consumptionconsumerBehaviorspace-joined full path incl. rootany depth
blogger.industry_crowdIndustry portraitautomotiveIndustryTagdeepest leaf value (numeric ID)leaves only
flags.industry_first / industry_secondIndustry (not blogger category)industryTagsplain name, single value, not an arrayone each
flags.activity_codesCampaignsactivitieseach item's code—

Array parameters are always OR-ed (union, de-duplicated); max 30 items, 64 characters per item.

Two ways a wrong value fails — neither raises an error
FailureWhat you seeWhich parameters
Over-filteringtotal=0, looks like "no such bloggers"everything except consumer_behavior
Silently not filteringunfiltered results — you think it applied, it didn'tconsumer_behavior

Measured (fans 1.0M–1.2M, total=455 with no other filter):

  • content_tag=["护肤品"] (one extra character) → total=0
  • fans.location=["浙江省"] (extra 省) → total=0
  • audience20=["挑战极限者"] (bare level-2 label) → total=0
  • note_scene=["选车攻略"] (last segment only, not the full path) → total=0
  • industry_crowd=["身心疗愈兴趣人群"] (label instead of numeric ID) → total=0
  • consumer_behavior=["保时捷911"] (bare leaf) → total=455 — no filtering at all; ["zzz不存在zzz"] also returns 455. Only the full-path form works (["内容行为预估 汽车"] → 54).

When you get total=0, suspect the value format before concluding "no such bloggers". When you get suspiciously many results, check whether consumer_behavior was silently ignored. Always copy values verbatim from /get_blogger_filter_options.

How to fill location (the most common mistake): just pass the plain place name — ["上海"], ["杭州"], ["上城区"] all work, OR-ed together. A full path ("中国 浙江 杭州") also works; upstream normalizes it. The only difference: fans.location has no district level — city is the finest.

Selecting multiple blogger categories (blogger.content_tag)

contentTags is a two-level tree: taxonomy1Tag is level 1 (28 of them — 美妆, 护肤, 个人护理 …) and taxonomy2Tags[] are its children. Mix level-1 and level-2 names in the same flat array — there is no nesting and nothing to send separately. Level-2 names are globally unique and never collide with level-1 names, so a flat array is unambiguous.

A real case from the web UI: 美妆 = "all", plus 2 children of 护肤 and 2 of 个人护理:

{"blogger": {"content_tag": ["美妆", "面部保养", "面部清洁", "头发产品", "身体护理"]}}

美妆 is a level-1 name (= the whole category); the other four are level-2 names.

Measured behaviour (same fan-count filter throughout, only the category changed, comparing the returned total):

ValuetotalNote
["面部保养"]200—
["面部清洁"]2—
["面部保养","面部清洁"]201Multiple tags are OR-ed, de-duplicated (1 blogger has both)
["护肤"]379A level-1 name means the whole category
all 4 children of 护肤32851 fewer than the level-1 name
["护肤","面部保养"]379Parent + child is accepted but pointless — the child is already covered
["护肤品"] (typo)0Silently returns 0 — no error

Two things to watch out for:

  • For "all", pass the level-1 name — do NOT expand it into all its children. Expanding 护肤 above loses 51 bloggers: some carry only the level-1 category and have no level-2 tag, so expanding drops them.
  • A misspelled name is not rejected — it just returns 0 results. Copy names verbatim from contentTags in /get_blogger_filter_options. When you see total=0, suspect the spelling before concluding "no such bloggers".

Max 30 items per array; each tag name at most 64 characters.

Locations (blogger.location / fans.location)

Flat, like categories: pass plain place names; provinces and cities can be mixed; multiple values are OR-ed. The web-UI case — 浙江 set to "all", 2 cities picked under 江苏, 2 more under 广东:

{"fans": {"location": ["浙江", "南京", "苏州", "广州", "深圳"]}}

For a whole province just pass the province name — do not expand it into all its cities. Blogger location has one extra level (district), same syntax:

{"blogger": {"location": ["上海", "杭州", "上城区"]}}

Upstream actually wants opposite formats for these two fields (blogger location takes bare names, fans location takes full paths like "中国 浙江 杭州"); the upstream service normalizes each, so plain names are all you need. On the fans.location side: 浙江→中国 浙江 (not expanded into its 11 cities), 杭州→中国 浙江 杭州, 吉林→ both 中国 吉林 and 中国 吉林 吉林 (a province and a city share that name; both are sent, OR-ed). A name absent from the dictionary (e.g. 浙江省) is passed through as-is → total=0.

Note fans.location only goes down to city — no districts (blogger.location has them).

Top-20 crowds (blogger.audience20)

This one is NOT a flat plain name. Level 2 must be written as "L1 L1-L2" — the level-1 name appears twice, separated by a space, joined by a hyphen in the second half.

The web-UI case — 自在户外 set to "all", 2 children under 自由畅行, 2 under 运动焕活 (measured total=169):

{"blogger": {"audience20": [
    "自在户外",
    "自由畅行 自由畅行-都市漫游家", "自由畅行 自由畅行-静奢新贵",
    "运动焕活 运动焕活-轻松健体派", "运动焕活 运动焕活-线条雕塑家"
]}}

Note the contrast: the "all" one is a bare level-1 name; every individually picked child must use the compound form.

Measured (baseline 455): ["自在户外"]=135, ["自在户外 自在户外-挑战极限者"]=9, ["挑战极限者"]=0 (bare level-2 label — silently returns 0), two compound values=18 (OR), ["自由畅行","自在户外 自在户外-挑战极限者"]=176 (vs 175 for 自由畅行 alone — mixing works).

Persona / Expertise — any level works

personalTagsV2 is a three-level tree, featureTags two. Any level works and they can be mixed. For a whole group just pass that group's name — no need to enumerate its leaves.

{"blogger": {"personal_tags": ["职业身份", "妈妈", "留学背景"],
             "feature_tags": ["风格", "vlog"]}}

Measured: personal_tags level-1 职业身份=91, level-2 互联网=8, leaf 妈妈=31, ["职业身份","妈妈"]=114 (union). feature_tags level-1 形式=183, 风格=37; level-2 vlog=136, 韩系=19; ["风格","vlog"]=150.

Content theme (blogger.content_theme) — level 1 only

An exception: only the level-1 label works; children always return 0. Measured ["通用"]=366 but its child ["口播"]=0. To filter by a child, use feature_tags instead.

Note category / Estimated consumption — full path required

These two do not take plain names. Join every level from the root with spaces; you may stop at any depth:

{"blogger": {
    "note_scene": ["汽车 理性决策", "汽车 理性决策 选车攻略 政策"],
    "consumer_behavior": ["内容行为预估 汽车"]
}}

Measured note_scene: ["汽车"]=75, ["汽车 理性决策"]=23, 4-level=1, while the last segment alone ["选车攻略"]=0.

⚠️ consumer_behavior fails differently from every other parameter: instead of returning 0, the whole filter is silently ignored. Measured ["保时捷911"] (bare leaf) and ["zzz不存在zzz"] both return 455 — identical to not passing the filter at all. Only the full-path form works (["内容行为预估 汽车"]=54). When using it, compare total with and without the filter to confirm it applied. This dictionary is also rolled out per account — some accounts get null, in which case the filter is unavailable.

Industry portrait (blogger.industry_crowd) — numeric IDs only

A four-level tree, but only the deepest leaf's value works, and it is a numeric ID, not a label. Measured ["17121263"]=110; the matching label ["身心疗愈兴趣人群"]=0.

Industry (flags.industry_first / industry_second) — single value

These are strings, not arrays — one value each, used together (the level-2 value must belong to the level-1 one):

{"flags": {"industry_first": "美妆个护", "industry_second": "护肤"}}

This is industry, a completely different taxonomy from blogger category (blogger.content_tag).

3.3 Plain-name filters

PathWeb filterHow to fill
coop.brand_idsRecent brandsBrand userId list (no brand search endpoint — supply userIds yourself)

3.4 Range Fields (value = {"min": x, "max": y})

PathWeb filter
fans.countFans count
note.imp_median / note.read_median / note.inter_medianOrganic note impression / read / engagement median
note.thousand_like_percent1000-like note ratio (%)
coop.pic_price / coop.video_priceSponsored quote (image-text / video, CNY)
coop.order_countSponsored order count
coop.reply_48h_ratio48h invitation reply rate (%)
coop.imp_median / coop.read_median / coop.inter_medianSponsored note medians
coop.cpuv / coop.estimate_cpuvCPUV / store-visit cost
coop.pic_cpm / coop.video_cpmEstimated CPM (image-text / video)
coop.pic_read_price / coop.video_read_priceEstimated cost per read
coop.pic_engage_cost / coop.video_engage_costEstimated cost per engagement
live.live_count / live.avg_viewer / live.avg_gmvLive sessions / avg viewers / avg GMV

3.5 Boolean Flags (value = true)

PathWeb filter
flags.is_star / flags.high_quality / flags.new_high_qualityCelebrity / high-quality / rising blogger
flags.note_and_liveAccepts both note and live cooperation
flags.filter_intentionIndustry intention match
flags.exclude_low_active / flags.exclude_fans_downExclude inactive / fans-declining bloggers
flags.exclude_cooperated / flags.exclude_invitedExclude already-cooperated / already-invited
coop.exclude_brandsWith coop.brand_ids: true = exclude bloggers who worked with those brands

flags.industry_first / flags.industry_second are industry names for industry-recommended bloggers; flags.activity_codes are campaign codes from /get_blogger_filter_options.

4. More Examples

The [示例/Example] block at the bottom of this page has 15 ready-to-copy request bodies covering keyword search, audience targeting, budget/ROI, live commerce, similar-blogger expansion and geo targeting — plus a common mistakes section (those silently return 0 results instead of erroring).

5. Paging

ParamRangeNote
page_num1-250Upstream exposes only the first 5000 items = 250 pages; page 251+ returns an empty array
page_size1-20Fixed at 20 on the web page. Keep it at 20: upstream post-filters each page, so a small size may leave 0 rows

Change page_num only and keep every other filter identical, otherwise pages overlap or skip. total is not the match count, it is the paging cap and is always 5000.

6. Return

  • kols[]: matched bloggers (~140 fields each)
    • Identity: userId / name / redId / headPhoto / location / gender
    • Tags: contentTags / featureTags / personalTags / tradeType
    • Metrics: fansNum / clickMidNum (read median) / interMidNum (engagement median) / fans30GrowthRate (fan growth) / kliveCnt30d (live sessions)
    • Commercial: picturePrice / videoPrice / lowerPrice / estimatePictureCpm / cooperateState
  • total: paging cap (always 5000 — not the real match count, see Paging above)
  • trackId / highlightWords

7. Notes:

  • Misspelled field names return 422 instead of being silently ignored
  • Fetch tree-style option values from /get_blogger_filter_options instead of hardcoding them

Response shape (two layers):

  • resp["data"] is PGY's raw envelope (with code/msg/success); the payload is one level deeper
  • Correct access: data = resp["data"]["data"] — every field listed under "Return" lives here

Billing:

  • HTTP 200 → charged, including "no result" (data is null, e.g. ID not found or filters matched nothing): the request did reach PGY, so retrying on empty data just burns quota
  • HTTP 400 → not charged (malformed params, or upstream unavailable/error)

[示例/Example]

// ① 关键词找博主:搜「咖啡」相关笔记的博主,按粉丝数降序;带品牌方 ID 做个性化推荐并剔除已合作过的
{"page_num": 1, "page_size": 20, "keyword": "咖啡", "search_type": 1,
 "column": "fansCount", "sort": "desc",
 "brand_user_id": "5dcfa5370000000001006030",
 "flags": {"exclude_cooperated": true}}

// ② 按昵称找号:search_type=0 时 keyword 匹配的是博主昵称而不是笔记内容
{"page_num": 1, "page_size": 20, "keyword": "小鹿", "search_type": 0}

// ③ 最基础的类目 + 粉丝量筛选:粉丝 1 万-5 万的美妆博主,按互动中位数降序
{"page_num": 1, "page_size": 20, "column": "mEngagementNum",
 "fans": {"count": {"min": 10000, "max": 50000}},
 "blogger": {"content_tag": ["美妆"]}}

// ④ 按画像选号:1 万-5 万粉、粉丝以 25-34 岁女性为主的美妆「妈妈」博主
{"page_num": 1, "page_size": 20,
 "blogger": {"content_tag": ["美妆"], "personal_tags": ["妈妈"]},
 "fans": {"count": {"min": 10000, "max": 50000}, "gender": 2, "age": 3}}

// ⑤ 按预算与响应速度选号:图文报价 1 万-5 万、48h 邀约回复率 ≥80%
{"page_num": 1, "page_size": 20,
 "coop": {"pic_price": {"min": 10000, "max": 50000},
          "reply_48h_ratio": {"min": 80}}}

// ⑥ 按性价比选号:图文报价 ≤5000、阅读中位数 ≥3000 的视频博主,只看优质号并剔除低活
{"page_num": 1, "page_size": 20,
 "note": {"read_median": {"min": 3000}, "note_type": 2},
 "coop": {"pic_price": {"max": 5000}},
 "flags": {"high_quality": true, "exclude_low_active": true}}

// ⑦ 找会直播带货的博主:近 30 天开播 1-5 场、场均销售额 ≥1 万,且笔记+直播均可合作
{"page_num": 1, "page_size": 20,
 "live": {"live_count": {"min": 1, "max": 5}, "avg_gmv": {"min": 10000}},
 "flags": {"note_and_live": true}}

// ⑧ 多维组合:7 个筛选组同时生效(字段名写错会直接 422,不会被静默忽略)
{"page_num": 1, "page_size": 20, "column": "comprehensiverank", "sort": "desc",
 "goal":    {"market_target": "mAccumImpNum"},
 "blogger": {"content_tag": ["美妆"], "gender": "女", "signed": 0},
 "fans":    {"count": {"min": 10000}, "age": 3, "device_brand": ["苹果"]},
 "note":    {"inter_median": {"min": 500}, "thousand_like_percent": {"min": 20}},
 "coop":    {"industry": "美妆个护", "video_price": {"max": 8000}},
 "live":    {"avg_viewer": {"min": 5000}},
 "flags":   {"exclude_cooperated": true}}


// ⑨ 找相似博主:给一个满意的标杆博主,扩一批相似的,**并叠加自己的筛选**
//    (/get_blogger_similar 只按相似度返回、不支持筛选,需要叠加筛选就用这里)
{"page_num": 1, "page_size": 20,
 "similar_user_id": "5a8cf39111be10466d285d6b",
 "fans": {"count": {"min": 50000}},
 "coop": {"pic_price": {"max": 20000}}}

// ⑩ 预算优先:按「图文预估阅读单价」从低到高排,先看最便宜的
{"page_num": 1, "page_size": 20,
 "column": "pictureReadCost", "sort": "asc",
 "coop": {"pic_price": {"min": 1000, "max": 10000}}}

// ⑪ 涨粉快的新秀:按近 30 天粉丝增长率降序,并剔除掉粉的
{"page_num": 1, "page_size": 20,
 "column": "fans30GrowthRate", "sort": "desc",
 "fans": {"count": {"min": 5000, "max": 100000}},
 "flags": {"exclude_fans_down": true}}

// ⑫ 爆款率优先:按百赞笔记占比降序(看重内容稳定性而非粉丝量时用)
{"page_num": 1, "page_size": 20,
 "column": "hundredLikePercent30", "sort": "desc",
 "note": {"thousand_like_percent": {"min": 10}}}

// ⑬ 地域投放:找上海本地的探店博主
//    blogger.location 直接填地名即可,不要填「上海市-上海市-黄浦区」这种全路径
{"page_num": 1, "page_size": 20,
 "blogger": {"location": ["上海"], "content_tag": ["美食"]}}

// ⑭ 按粉丝所在地投放(和 ⑬ 是两个不同维度:⑬ 是博主在哪,⑭ 是粉丝在哪)
{"page_num": 1, "page_size": 20,
 "fans": {"location": ["广东"], "count": {"min": 20000}}}

// ⑮ 直播带货 + 90 天口径:场均观播 ≥5000、场均销售额 ≥1 万
//    注意 avg_viewer / avg_gmv 是近 90 天,而 live_count 是近 30 天
{"page_num": 1, "page_size": 20,
 "live": {"live_count": {"min": 1}, "avg_viewer": {"min": 5000}, "avg_gmv": {"min": 10000}}}

常见错误写法 / Common mistakes

分两类。第二类才是真正的坑 —— 它们不会报错,只会安静返回 0 条。

A. 会直接 422 的写法(好在会立刻报错)

// ✗ 区间写成了数字 —— 区间型字段必须是对象
{"fans": {"count": 10000}}
// ✓
{"fans": {"count": {"min": 10000}}}

// ✗ 字段名拼错(驼峰)—— 本接口 extra="forbid",写错直接 422,不会被静默忽略
{"blogger": {"contentTag": ["美妆"]}}
// ✓
{"blogger": {"content_tag": ["美妆"]}}

// ✗ 排序列填了未放行的值
{"column": "forecastReadUv"}
// ✓ 见第 1 节的 34 个排序字段
{"column": "avgAgmv90d"}

// ✗ page_size 超上限(1-20)
{"page_size": 100}
// ✓
{"page_size": 20}

// ✗ 百分比量纲错了 —— 千赞比例填 200 或 9500(按万分比/原始条数填)
{"note": {"thousand_like_percent": {"min": 200}}}
// ✓ 想表达「20%」就填 20
{"note": {"thousand_like_percent": {"min": 20}}}

B. 不会报错、但查不到数据的写法(最难排查)

下面这些全都能通过校验,返回 200,结果却是 0 条 —— 因为值本身合法、只是没有匹配项。

// ✗ 地域填了带分隔符的全路径
{"blogger": {"location": ["上海市-上海市-黄浦区"]}}
// ✓ 直接填地名
{"blogger": {"location": ["上海"]}}

// ✗ 百分比填了小数(0.2 是合法的 0.2%,不是 20%)
{"note": {"thousand_like_percent": {"min": 0.2}}}
// ✓ 想表达「20%」
{"note": {"thousand_like_percent": {"min": 20}}}

// ✗ 类目值是自己编的
{"blogger": {"content_tag": ["美妆护肤"]}}
// ✓ 用 /get_blogger_filter_options 的 contentTags 里真实存在的值
{"blogger": {"content_tag": ["美妆"]}}

// ✗ 开关型填 false —— 等于没筛,不是「反向筛」
{"flags": {"exclude_cooperated": false}}
// ✓ 想生效就填 true
{"flags": {"exclude_cooperated": true}}

// ✗ 把「行业」当成「博主类目」用(两个是不同的筛选)
{"flags": {"industry_first": "美妆"}}
// ✓ 博主类目用 blogger.content_tag
{"blogger": {"content_tag": ["美妆"]}}

搜不到结果时的排查顺序:先把筛选一个个去掉,定位是哪个条件卡住的; 需要查字典的筛选(类目 / 场景 / 消费行为 / 地域)优先怀疑值写错了。

post/api/v1/xiaohongshu/pgy/get_blogger_list

Request

  • Base URL
    https://api.tikhub.io (the document declares no server; this is the origin it was published from)
  • Auth
    HTTP bearer

Request body

page_numinteger

页码,1-250(上游只放出前 5000 条)/Page, 1-250

page_sizeinteger

每页数量,1-20(网页固定 20,建议保持 20)/Page size, 1-20

keywordstring

搜索词;空=只筛选不搜索/Keyword

search_type0 | 1

1=按笔记关键词找博主(默认) 0=按昵称搜/Search mode

column'comprehensiverank' | 'fansCount' | 'price' | 'clickNum' | 'mEngagementNum' | 'accumCommonImpMedinNum30d' | 'accumPicCommonImpMedinNum30d' | 'accumVideoCommonImpMedinNum30d' | 'pictureClickMidNum' | 'videoClickMidNum' | 'pictureInterMidNum' | 'videoInterMidNum' | 'thousandLikePercent30' | 'pictureThousandLikePercent30' | 'videoThousandLikePercent30' | 'hundredLikePercent30' | 'pictureHundredLikePercent30' | 'videoHundredLikePercent30' | 'videoFinishRate' | 'accumCoopImpMedinNum30d' | 'readMidCoop30' | 'interMidCoop30' | 'mCpuvNum30d' | 'picturePrice' | 'videoPrice' | 'pictureReadCost' | 'videoReadCost' | 'inviteReply48hNumRatio' | 'fans30GrowthRate' | 'fansActiveIn28dLv' | 'fansEngageNum30dLv' | 'kliveCnt30d' | 'avgLiveViewerNum' | 'avgAgmv90d'

排序字段,共 34 个,见接口说明的排序字段表/Sort column, 34 options, see endpoint docs

sort'desc' | 'asc'

排序方向 desc/asc

brand_user_idstring nullable

品牌方/广告主用户ID,可不填/Brand user ID, optional

similar_user_idstring nullable

找相似博主:填一个博主的 userId,返回与他相似的博主,并且可以叠加下面所有筛选(这是它与 /get_blogger_similar 的关键区别)/Find similar bloggers; unlike /get_blogger_similar it can be combined with all filters below

similar_wordstring nullable

配合 similar_user_id 的博主昵称,仅用于回显、不影响结果/Nickname echo for similar_user_id; does not affect results

Example request

{
  "page_num": 1,
  "page_size": 20,
  "keyword": "咖啡",
  "search_type": 1,
  "column": "comprehensiverank",
  "sort": "desc",
  "brand_user_id": "5dcfa5370000000001006030",
  "similar_user_id": "5c668b3e0000000012021605",
  "goal": {
    "market_target": "mAccumImpNum"
  },
  "blogger": {
    "content_tag": [
      "美妆",
      "面部保养",
      "面部清洁"
    ],
    "personal_tags": [
      "妈妈"
    ],
    "feature_tags": [
      "vlog"
    ],
    "content_theme": [
      "汽车特色"
    ],
    "audience20": [
      "自由畅行",
      "自在户外 自在户外-挑战极限者"
    ],
    "industry_crowd": [
      "15795914"
    ],
    "consumer_behavior": [
      "内容行为预估 汽车"
    ],
    "note_scene": [
      "汽车 理性决策 选车攻略 政策"
    ],
    "gender": "女",
    "location": [
      "上海"
    ]
  },
  "fans": {
    "count": {
      "min": 10000,
      "max": 50000
    },
    "age": 3,
    "gender": 2,
    "device_price": [
      "3000-3999"
    ],
    "device_brand": [
      "苹果"
    ],
    "location": [
      "上海"
    ]
  },
  "note": {
    "imp_median": {
      "min": 10000,
      "max": 50000
    },
    "read_median": {
      "min": 10000,
      "max": 50000
    },
    "inter_median": {
      "min": 10000,
      "max": 50000
    },
    "thousand_like_percent": {
      "min": 20,
      "max": 80
    },
    "note_type": 2
  },
  "coop": {
    "pic_price": {
      "min": 10000,
      "max": 50000
    },
    "video_price": {
      "min": 10000,
      "max": 50000
    },
    "order_count": {
      "min": 10000,
      "max": 50000
    },
    "reply_48h_ratio": {
      "min": 20,
      "max": 80
    },
    "industry": "美妆个护",
    "imp_median": {
      "min": 10000,
      "max": 50000
    },
    "read_median": {
      "min": 10000,
      "max": 50000
    },
    "inter_median": {
      "min": 10000,
      "max": 50000
    },
    "cpuv": {
      "min": 10000,
      "max": 50000
    },
    "pic_cpm": {
      "min": 10000,
      "max": 50000
    },
    "video_cpm": {
      "min": 10000,
      "max": 50000
    },
    "pic_read_price": {
      "min": 10000,
      "max": 50000
    },
    "video_read_price": {
      "min": 10000,
      "max": 50000
    },
    "pic_engage_cost": {
      "min": 10000,
      "max": 50000
    },
    "video_engage_cost": {
      "min": 10000,
      "max": 50000
    },
    "estimate_cpuv": {
      "min": 10000,
      "max": 50000
    }
  },
  "live": {
    "live_count": {
      "min": 10000,
      "max": 50000
    },
    "avg_viewer": {
      "min": 10000,
      "max": 50000
    },
    "avg_gmv": {
      "min": 10000,
      "max": 50000
    }
  },
  "flags": {
    "industry_first": "美妆个护"
  }
}

Response

Successful Response

codeinteger

HTTP status code | HTTP状态码

request_idstring nullable

Unique request identifier | 唯一请求标识符

messagestring

Response message (EN-US) | 响应消息 (English)

message_zhstring

Response message (ZH-CN) | 响应消息 (中文)

supportstring

Support message | 支持消息

timestring

The time the response was generated | 生成响应的时间

time_stampinteger

The timestamp the response was generated | 生成响应的时间戳

time_zonestring

The timezone of the response time | 响应时间的时区

docsstring nullable

Link to the API Swagger documentation for this endpoint | 此端点的 API Swagger 文档链接

cache_messagestring nullable

Cache message (EN-US) | 缓存消息 (English)

cache_message_zhstring nullable

Cache message (ZH-CN) | 缓存消息 (中文)

cache_urlstring nullable

The URL to access the cached result | 访问缓存结果的 URL

routerstring

The endpoint that generated this response | 生成此响应的端点

paramsunknown
dataunknown

Changes

    • breaking

      removed from the request property anyOf list

    • info

      added to the request property anyOf list

    • info

      added the optional property // to the response with the status

    • info

      added the optional property // to the response with the status

    • breaking

      removed from the request property anyOf list

    • breaking

      removed from the request property anyOf list

    • info

      added to the request property anyOf list

    • info

      added to the request property anyOf list