本文档面向合作方("第三方应用")的接入工程师,描述将贵方业务以完整第三方登录模式接入我方平台的流程、需要调用的接口及其请求/响应字段。
接入前,贵方应用需在我方平台完成注册,由我方分配以下凭据:
app_id、api_key、redirect_url(可配置为逗号分隔的多个,便于流量分散)。secret_key正在逐步启用,当前通过/thirdparty/authurl的X-SECRET-KEYHeader 传入,暂不强制要求。
apiBaseUrl)| 环境 | apiBaseUrl |
|---|---|
| 测试环境 | https://lite-stg.zeniaiai.com |
| 生产环境 | https://h5web.seseai.com |
本文档中的 H5 地址示例默认使用测试环境。生产上线时,请将
apiBaseUrl切换为生产环境地址。
当前推荐使用完整第三方登录模式,贵方需要具备用户系统和支付系统:
| 模式 | 贵方用户系统 | 贵方支付系统 | 调用方式 | 积分管理方 |
|---|---|---|---|---|
完整第三方登录(is_need_pay=false) |
有 | 有 | 后端 → 我方 /thirdparty/authurl + /login + /logout 全流程 |
贵方(资产 ↔ 积分双向同步) |
核心流程:贵方负责收款和资产管理,通过 asset 字段把"等价积分"带入我方系统供消费,会话结束时我方将剩余积分以 asset 形式归还给贵方。
第三方后端接口(/api/thirdparty/...)使用以下 Header 鉴权:
| Header | 说明 |
|---|---|
X-APP-ID |
我方分配的应用 ID(同 body 中的 app_id 字段;Header 优先生效) |
X-APP-KEY |
我方分配的 API Key |
X-SECRET-KEY |
我方分配的Secret Key,90天有效期,到期7天前会通知更换 |
缺失 X-APP-KEY 直接返回 ErrInvalidInput;app_id 在 /thirdparty/authurl 上为必填。X-SECRET-KEY 缺失时保持兼容,不会因为缺失而拒绝请求。
完整第三方登录模式的 asset 字段与我方内部积分余额按 1 : 10 换算:
points = asset × 10
asset = points ÷ 10 (响应中的 balance 字段,保留 2 位小数)
user_id贵方在请求体中传入的 user_id 是贵方系统内的用户标识(最大 256 字节,去除首尾空白后非空),用于在我方系统内做一一映射。我方为同一 (app_id, user_id) 创建唯一本地账号;后续相同请求会复用该账号。
channel_code 与 is_need_paychannel_code:贵方渠道编码,用于运营归因。在 /thirdparty/authurl 上每次登录都会更新到用户行;省略时回退到 app_id。is_need_pay:当前仅接受 false,表示由贵方负责收款和资产管理。请求必须传入 is_need_pay=false;true 不再支持。该值会以查询参数形式回显在 magic-link URL 上。magic-link 跳转到我方 H5 后,是否展示「充值」入口、点击后走哪条收银路径,由以下三个参数共同决定。它们作用在不同层:
| 参数 | 所在层 | 来源 | 作用 |
|---|---|---|---|
is_need_pay |
后端字段 | /authurl |
固定传入 false,回显到 magic-link 的 is_need_pay=false,不展示我方内置充值入口 |
top_up_text |
后端字段 | /authurl 请求体 |
「余额不足 / 充值」提示的 CTA 文案。典型用于 parent_support_pay=true:余额不足时 H5 弹出充值提示,按钮文案取此值,点击后经 postMessage 交由父窗口收银。按会话持久化(24h,logout 清除),原样回显在 /auth/:token 的 topUpText 字段(无默认值、不随 callback 订阅门控),不写入 magic-link URL |
parent_support_pay |
前端 URL 参数 | 贵方自行拼接到 H5 URL | 非后端 API 字段,我方后端不感知。 iframe 嵌入场景下,余额不足点击充值时通过 postMessage({ type: 'topup' }) 通知父窗口,由外层系统收银;按钮文案即上面的 top_up_text |
is_need_pay固定为false;top_up_text与parent_support_pay配套使用:前者由贵方后端经/authurl下发文案,后者由贵方前端拼到 H5 URL 决定收银路径。H5 侧的取值规范、组合行为矩阵、postMessage事件协议、受影响页面等完整说明见 §8。
所有接口(含错误)共用以下信封:
{
"success": true,
"data": { ... },
"meta": { ... }
}
错误时 success=false 并附带 error.code / error.message。
适用场景:贵方既有账号体系,也有自己的支付与积分体系。希望由贵方收款,再把"已购买的等价积分"以 asset 字段同步到我方,由我方代为提供 AI 能力消费;会话结束时把未消费的积分以 asset 形式归还给贵方记账。
┌──────────────────────────────────────────────────────────────────┐
│ 贵方支付(贵方账本) │
│ ↓ 用户购买 N 资产 │
│ 贵方调用 /thirdparty/authurl 或 /login,asset=N │
│ ↓ bring-in:我方记账 N × 10 = 10N 积分 │
│ 用户在我方 H5 / API 消费积分 │
│ ↓ 消费 M 积分(M ≤ 10N) │
│ 贵方调用 /thirdparty/logout │
│ ↓ settle:我方扣减剩余 (10N − M) 积分 │
│ ↓ 响应 balance = (10N − M) / 10 │
│ 贵方据 balance 把剩余资产回写到贵方账本 │
└──────────────────────────────────────────────────────────────────┘
登录入口(推荐 /thirdparty/authurl,得到 magic-link):用户在贵方系统购买/触发后,贵方后端调用 /thirdparty/authurl:
{
"user_id": "{your_user_id}",
"asset": 100.0,
"is_need_pay": false,
"channel_code": "{your_channel_code}",
"nickname": "Bob",
"lang": "zh-hans"
}
is_need_pay=false,当前不接受其他值。asset=100 表示"贵方账本中扣除等值的 100 资产,请我方记账 1000 积分"。app_id、同一 user_id 的 authurl:bring-in 只发生一次——session 还在活跃期内的二次调用不会刷新余额(防止贵方误用导致刷余额漏洞)。响应:
{
"success": true,
"data": {
"authUrl": "https://lite-stg.zeniaiai.com/cb?token=...&is_need_pay=false",
"balance": 100.0,
"userID": "1234567890",
"thirdPartyUserID": "{your_user_id}"
}
}
贵方前端将用户重定向到 authUrl,由我方 H5 拿 token 换 JWT 后登录。
替代入口(纯服务端,/thirdparty/login,不返回 magic-link):如果贵方完全不需要 H5、只是想在贵方页面里通过 JWT 直接调用我方 API,可改用 /thirdparty/login:
{
"user_id": "{your_user_id}",
"asset": 100.0
}
响应是 ThirdPartyBalanceOutput(含 balance),不含 authUrl。该接口同样有 bring-in 的 session 互斥保护。
会话结束:用户结束使用、贵方主动登出时,调用 /thirdparty/logout:
{
"user_id": "{your_user_id}"
}
响应 balance 即结算时剩余的资产单位。贵方据此把剩余金额回写到贵方账本。
核对积分流水:如需对账,可拉取该用户在我方系统的消费流水:
POST /api/thirdparty/transactions
{
"user_id": "{your_user_id}",
"page": 1,
"size": 50
}
logout_deducted + remaining_balance == initial_balance + refund_amount 始终成立。asset < 0 视为"不触发 bring-in"——可用于仅建立会话而不带入积分的场景。| 接口 | 完整第三方登录模式 |
|---|---|
POST /api/thirdparty/authurl |
✅ |
POST /api/thirdparty/login |
可选(替代 authurl) |
POST /api/thirdparty/logout |
✅(结算) |
GET /api/thirdparty/auth/:token |
✅(我方 H5 内部) |
POST /api/thirdparty/transactions |
✅ |
GET /api/thirdparty/generations |
可选 |
| 接收我方回调 POST(第 6 节) | 可选(transaction + generate) |
POST /api/thirdparty/authurl申请一次性 magic-link,返回前端跳转 URL。
请求 Header:
| Header | 必需 | 说明 |
|---|---|---|
X-APP-ID |
✅ | 渠道 App ID;也可通过 body 的 app_id 传入,Header 优先 |
X-APP-KEY |
✅ | 渠道 API Key |
X-SECRET-KEY |
否 | 渠道 Secret Key,最大 100 个字符;当前处于兼容期 |
Secret Key 鉴权与保存规则:
如果 X-APP-KEY 在我方 third_party_apps 中不存在,会调用 Titan v2 鉴权接口:
GET {TITAN_API}/aiapi/api/v2/mch/auth
X-API-KEY: {X-APP-KEY}
X-SECRET-KEY: {X-SECRET-KEY} # 仅在前端传入非空值时发送
Titan 鉴权成功后创建 third_party_apps 记录,并保存 api_key 与 secret_key。未传 Secret Key 时,secret_key 保存为空值。
如果 X-APP-KEY 已存在:
X-SECRET-KEY:直接复用现有应用,不重新鉴权;secret_key 相同:直接复用现有应用,不重新鉴权;third_party_apps.secret_key,失败则返回认证错误。历史应用的 secret_key 为空时,第一次传入非空 Secret Key 会触发鉴权,成功后写入。
X-SECRET-KEY 不会出现在响应中。后续版本可能会将该 Header 改为必填。
请求 Body:
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
user_id |
string | ✅ | 贵方用户标识,≤ 256 字节,去空白后非空 |
asset |
float64 | 否 | 等价资产数量;>= 0 时触发 bring-in |
is_need_pay |
bool | ✅ | 必须传入 false,表示由贵方负责收款和资产管理;不接受 true |
channel_code |
string | 否 | 渠道编码,省略时回退到 app_id |
nickname |
string | 否 | 用户昵称(仅写一次:首次创建;后续不会覆盖资料) |
avatar |
string | 否 | 用户头像 URL |
lang |
string | 否 | 语言代码,默认 zh-hans,会回显到 magic-link URL |
theme |
string | 否 | 主题(如 dark、light),回显到 magic-link URL |
top_up_text |
string | 否 | 「余额不足 / 充值」CTA 文案,典型配合 parent_support_pay=true(见 §2.5);按会话持久化(24h,logout 清除),原样回显在 /auth/:token 的 topUpText 字段(无默认值、独立于 callback),不写入 magic-link URL |
app_id |
string | 否 | 与 X-APP-ID 二选一;Header 优先 |
callback |
object | 否 | 订阅本会话的事件回调,详见 第 6 节;字段:url (https, 必填), events (["transaction", "generate"] 子集,省略=全部), share_text (用于前端 share UI 文案,省略时回落到默认值 分享你的AI创作) |
响应 data:
| 字段 | 类型 | 说明 |
|---|---|---|
authUrl |
string | 一次性 magic-link,含 token、theme、lang、is_need_pay 查询参数 |
balance |
float64 | 用户当前在我方系统的余额(资产单位,积分 ÷ 10) |
userID |
string | 我方分配的用户 ID(Snowflake,字符串) |
thirdPartyUserID |
string | 回显贵方传入的 user_id |
示例:
curl -X POST 'https://api.example.com/api/thirdparty/authurl' \
-H 'X-APP-ID: partner-app-1' \
-H 'X-APP-KEY: ak_xxxxxx' \
-H 'X-SECRET-KEY: sk_xxxxxx' \
-H 'Content-Type: application/json' \
-d '{
"user_id":"u-42",
"asset":100.0,
"is_need_pay":false,
"channel_code":"chan-a",
"lang":"zh-hans"
}'
POST /api/thirdparty/login纯服务端登录,不返回 magic-link——只创建/复用用户、做 bring-in、返回当前余额。可作为 authurl 的替代。
请求 Header:X-APP-ID, X-APP-KEY
请求 Body:
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
user_id |
string | ✅ | 贵方用户标识 |
asset |
float64 | 否 | 同 authurl;>= 0 触发 bring-in,< 0 跳过 |
app_id |
string | 否 | 与 X-APP-ID 二选一;Header 优先 |
响应 data (ThirdPartyBalanceOutput):
| 字段 | 类型 | 说明 |
|---|---|---|
userID |
string | 我方用户 ID |
thirdPartyUserID |
string | 回显 user_id |
balance |
float64? | 当前余额(资产单位) |
POST /api/thirdparty/logout结束会话并触发余额结算(drain)。
请求 Body:
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
user_id |
string | ✅ | 贵方用户标识 |
app_id |
string | 否 | 与 X-APP-ID 二选一 |
响应 data:
| 字段 | 类型 | 说明 |
|---|---|---|
userID |
string | 我方用户 ID |
thirdPartyUserID |
string | 回显 user_id |
balance |
float64? | 首次 logout(本次 bring-in 周期内还没有结算过)时返回,单位为资产单位(积分 ÷ 10),反映 logout 那一刻被划走的剩余资产;重复 logout 时此字段省略 |
old_balance |
float64? | 重复 logout 时返回(本次 bring-in 周期内已经有过 logout 结算行),值等于上一次真正划走的资产单位;首次 logout 时此字段省略 |
首次 vs 重复 logout 的判定:
Bring In Assets 审计行开始,到下一次 Bring In Assets 之前。balance 等于划走的资产单位,old_balance 省略。amount=0 的 DEDUCT 审计行作为会话结束边界,响应中 balance 省略,old_balance 等于上次真正划走的资产单位(来自 ABS(amount)/10 的最近非零 logout 行)。/authurl 重新带入):当前余额 = 退款额 > 0,我方会划走这部分残值并写 amount=-residual 的 DEDUCT 行;响应中 balance 仍然有值(等于本次划走的资产单位),old_balance 省略。换言之:只要本次调用确实划走了非零积分,就走 balance 字段——贵方应当按 balance 入账。balance 表示"本次为贵方划走了多少",old_balance 表示"上次真正划走了多少(本次没动)"。贵方据 balance 入账、据 old_balance 识别幂等重试。幂等性:若用户从未登录过(identity 不存在),接口返回 ErrAuthInvalid;若 session 归属其他 app_id,返回 ErrResourceConflict;其他情况下重复 logout 不会报错——重复语义见上文 balance / old_balance 字段说明。
GET /api/thirdparty/auth/:token通常由我方 H5 内部调用,贵方一般不直接调用——magic-link 中的 token 被前端解析后向此端点换取 JWT。仅作参考说明。
用一次性 token 换取 LoginOutput(含 JWT accessToken / refreshToken)。
查询参数 lang 可选,若提供且与用户已存语言不同则更新。
token 在 24 小时内多次调用都有效,并在下次 authurl 触发轮换。
响应在 LoginOutput 字段基础上额外返回三个 thirdparty 专属字段。其中 share / shareText 由本会话 callback 配置派生(详见 第 6 节);topUpText 独立于 callback,由 /authurl 的 top_up_text 直接下发(见 §2.5):
| 字段 | 类型 | 说明 |
|---|---|---|
share |
bool | 当且仅当 callback.events 订阅了 generate 时为 true;前端据此决定是否渲染 share/分享开关 |
shareText |
string | share=true 时回显贵方传入的 callback.share_text(或默认值),用于前端 share UI 文案;share=false 时省略 |
topUpText |
string | 原样回显贵方在 /authurl 传入的 top_up_text(无默认值、不依赖 callback、不依赖 share);未传则省略。供前端自定义「余额不足 / 充值」按钮文案 |
POST /api/thirdparty/transactions拉取我方系统内某用户的"消费"流水。返回结果按调用方 app_id 限定,且只包含 type=CONSUME 的消费记录——bring-in、退款(REFUND)、奖励等其它类型不在此列。
请求 Header:X-APP-ID, X-APP-KEY
请求 Body:
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
user_id |
string | 否 | 不传时返回该 app_id 下全部用户 |
app_id |
string | 否 | 与 X-APP-ID 二选一;Header 优先 |
page |
int | 否 | 偏移分页页码,默认 1(仅在不传 cursor 时生效) |
size |
int | 否 | 每页大小,默认 10,最大 10000 |
cursor |
int64 | 否 | 游标分页(向后/更旧):上一响应 meta.pagination.next_cursor 的值 |
prev_cursor |
int64 | 否 | 游标分页(向前/更新):上一响应 meta.pagination.prev_cursor 的值 |
sortBy / sortOrder |
string | 否 | 偏移模式下的排序字段与方向;游标模式下被忽略。省略时默认按交易 ID(≈ createdAt)倒序 |
两种分页模式(始终返回 §2.6 信封;data 为流水数组,分页信息在 meta.pagination):
cursor/prev_cursor):用 page/size 翻页,行为与旧版一致。cursor 或 prev_cursor):按交易 ID 倒序、稳定翻页,优先于 page(同时传入时忽略 page,且忽略 sortBy/sortOrder)。游标模式不受深翻页性能衰减影响,大数据集请优先使用。next_cursor 原样回传为 cursor,直到 has_next=false(next_cursor 字段随之省略)。prev_cursor 原样回传为 prev_cursor,直到 has_prev=false(prev_cursor 字段随之省略)。cursor 或 prev_cursor 之一;同时传入时 cursor(向后)优先。首页用偏移模式(不传游标),其响应也会带 next_cursor,据此即可切入游标遍历。meta.pagination 字段:
| 字段 | 说明 |
|---|---|
mode |
"offset" 或 "cursor",标识本次响应的分页模式 |
total |
当前过滤条件下的总条数(已按 app_id/provider 限定) |
pages |
ceil(total/size),供展示 |
size / page |
生效的每页大小 / 页码 |
has_next |
是否还有下一页(更旧方向);游标模式由 next_cursor 是否存在决定 |
has_prev |
是否有上一页(更新方向);游标模式由 prev_cursor 是否存在决定 |
cursor |
本次请求使用的向后游标(向后翻页时回显;首页/偏移/向前翻页省略) |
next_cursor |
下一页(更旧)游标;无更多数据时省略 |
prev_cursor |
上一页(更新)游标;无更新数据时省略 |
响应 data(交易记录数组,按交易 ID 倒序;每条记录字段):
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string | 交易记录 ID(Snowflake,字符串)。游标分页的 cursor / next_cursor / prev_cursor 即取此值 |
userID |
string | 我方内部用户 ID(Snowflake,字符串) |
thirdPartyUserID |
string? | 回显贵方用户标识;不传 user_id 拉取全部用户时用于区分记录归属 |
type |
string | 交易类型,本接口恒为 CONSUME |
amount |
float64 | 带符号的积分变动量,消费为负值 |
balanceAfter |
float64 | 交易后用户在我方系统的积分余额 |
source |
string | 积分变动来源标识;AI 生成消费为 ai_task_consume |
generationID |
string? | 触发本次消费的 AI 生成记录 ID(Snowflake);同一值即 generate 回调的 event_id,可据此与 /thirdparty/generations 关联 |
bid |
string? | 外部任务 ID(来自 AI 供应商,便于贵方溯源) |
referenceID |
string? | 关联引用 ID(若有) |
description |
string? | 交易描述(若有) |
metadata |
object? | 附加元数据(若有) |
status |
string | 交易状态(pending / completed / failed,消费记录通常为 completed;内部字段,贵方一般无需关心) |
retryCount |
int | 重试计数(内部字段) |
createdAt |
string | 交易创建时间(RFC3339) |
updatedAt |
string? | 交易更新时间(RFC3339,若有) |
单位提醒:本接口
data[].amount/balanceAfter是我方内部积分原始值,与 §6.4 回调 payload 中已换算为资产单位的costs/balance不同。贵方按1 asset = 10 points换算(积分 ÷ 10)即可与自身账本对账。
响应示例(偏移模式首页):
{
"success": true,
"data": [
{
"id": "1739450000000000001",
"userID": "1234567890",
"thirdPartyUserID": "u-42",
"type": "CONSUME",
"amount": -100,
"balanceAfter": 900,
"source": "ai_task_consume",
"generationID": "1739450000000000000",
"status": "completed",
"createdAt": "2026-05-26T08:30:00Z"
}
],
"meta": {
"request_id": "req-abc123",
"timestamp": "2026-05-26T08:31:00Z",
"pagination": {
"mode": "offset",
"total": 128,
"pages": 13,
"size": 10,
"page": 1,
"has_next": true,
"has_prev": false,
"next_cursor": "1739450000000000001"
}
}
}
/api/thirdparty/transactions/all 是上述接口忽略 user_id 的便捷形式,已标记 Deprecated,新接入请直接使用 /transactions 并省略 user_id。 其分页行为与 /transactions 完全一致。
GET /api/thirdparty/generations列出该 app_id 下所有用户(或指定单用户)的 AI 生成记录,用于核对生成产物、消耗与审核状态。它是 generate 回调(第 6 节)的拉取式补充:回调由我方主动推送单条产物,本接口供贵方按需分页回查。
注意:本接口是 GET,参数走 query string(其余 thirdparty 接口多为 POST + JSON body)。
请求 Header:X-APP-ID, X-APP-KEY
请求 Query 参数:
| 参数 | 类型 | 必需 | 说明 |
|---|---|---|---|
app_id |
string | 否 | 与 X-APP-ID 二选一;Header 优先 |
user_id |
string | 否 | 贵方用户标识;省略时返回该 app_id 下全部用户的生成记录 |
status |
string | 否 | 生成状态过滤,可重复传参或逗号分隔多值(如 status=completed,failed) |
tool_id |
string | 否 | 按 AI 工具 ID 过滤 |
audit_status |
string | 否 | 审核状态过滤:pending / passed / rejected |
created_at_from |
string | 否 | 创建时间下界(含),RFC3339 |
created_at_to |
string | 否 | 创建时间上界(含),RFC3339 |
page |
int | 否 | 偏移分页页码,默认 1(仅在不传 cursor/prev_cursor 时生效) |
size |
int | 否 | 每页大小,默认 10,最大 10000 |
cursor |
int64 | 否 | 游标分页(向后/更旧):上一响应 meta.pagination.next_cursor 的值;传入即切换到游标模式 |
prev_cursor |
int64 | 否 | 游标分页(向前/更新):上一响应 meta.pagination.prev_cursor 的值 |
sortBy/sortOrder/keyword/deleted不支持;两种分页模式都已排除软删除记录。分页有两种模式(与 §5.5/transactions一致): - 偏移模式(默认,不传游标):用page/size翻页,结果按createdAt倒序。整页时响应也会带next_cursor,据此即可切入游标遍历。 - 游标模式(传cursor或prev_cursor,优先于page):按生成记录id倒序稳定翻页,不受深翻页性能衰减影响,大数据集请优先使用。向后/向前翻页与游标回传规则同 §5.5。
响应 data(生成记录数组,按创建时间倒序;每条记录字段):
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string | 生成记录 ID(Snowflake,字符串);即 generate 回调的 event_id,也对应 §5.5 消费流水的 generationID |
thirdPartyUserID |
string | 回显贵方用户标识 |
status |
string? | 生成状态(如 pending / completed / failed) |
auditStatus |
string? | 审核状态:pending / passed / rejected |
toolID |
string? | 生成所用 AI 工具 ID(Snowflake) |
aiTool |
object? | 该工具的简要信息;无工具或工具未知时省略。子字段:id(Snowflake 字符串)、name(工具标识名)、displayName(多语言显示名对象,如 {"en":"Text to Image"}) |
provider |
string? | 该工具背后的 AI 生成供应商(如 titan_aiapi),非 oauth 租户 provider |
costInPoints |
float64 | 本次生成消耗的积分(÷ 10 得资产单位) |
resultUrls |
string[]? | 生成产物的结果 URL 数组;生成完成前或无产物时省略 |
createdAt |
string | 创建时间(RFC3339) |
安全与差异说明:出于安全考虑,本接口刻意不返回加密密钥、提示词 / 输入参数(prompt、generation_parameters)及内部 metadata,避免向接入方泄露用户内容或媒体密钥。
resultUrls是数据库存储的原始值,不保证像 §6.4 回调assets_url那样用应用 BaseURL 补全为绝对 URL;若为根相对路径需贵方自行拼接域名。
meta.pagination 字段:与 §5.5 完全一致。偏移模式 mode="offset",含 total / pages / size / page / has_next / has_prev,整页时附 next_cursor;游标模式 mode="cursor",has_next / has_prev 由 next_cursor / prev_cursor 是否存在决定。
示例:
curl -G 'https://api.example.com/api/thirdparty/generations' \
-H 'X-APP-ID: partner-app-1' \
-H 'X-APP-KEY: ak_xxxxxx' \
--data-urlencode 'user_id=u-42' \
--data-urlencode 'status=completed' \
--data-urlencode 'audit_status=passed' \
--data-urlencode 'page=1' \
--data-urlencode 'size=20'
响应:
{
"success": true,
"data": [
{
"id": "1739450000000000000",
"thirdPartyUserID": "u-42",
"status": "completed",
"auditStatus": "passed",
"toolID": "880123456789",
"aiTool": {
"id": "880123456789",
"name": "text-to-image",
"displayName": { "en": "Text to Image", "zh-hans": "文生图" }
},
"provider": "titan_aiapi",
"costInPoints": 100,
"resultUrls": [
"https://cdn.example.com/.../foo.png"
],
"createdAt": "2026-05-26T08:30:00Z"
}
],
"meta": {
"request_id": "req-def456",
"timestamp": "2026-05-26T08:31:00Z",
"pagination": {
"total": 42,
"pages": 3,
"size": 20,
"page": 1,
"has_next": true,
"has_prev": false
}
}
}
适用于完整第三方登录模式。贵方在 /thirdparty/authurl 请求体中携带 callback 对象,订阅本会话内由我方主动推送的事件。订阅是按会话生效的——配置随 session 一同写入 Redis,logout 时一并清除。
在 POST /api/thirdparty/authurl 的请求体中追加 callback 对象:
{
"user_id": "{your_user_id}",
"asset": 100.0,
"callback": {
"url": "https://partner.example.com/seselite/cb",
"events": ["transaction", "generate"],
"share_text": "分享你的AI创作"
}
}
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
url |
string | ✅ | 我方将事件 POST 到此 URL,必须 https,不跟随 3xx 跳转 |
events |
string[] | 否 | 订阅的事件名,子集 ⊆ ["transaction", "generate"];省略或空数组等价于全部订阅;大小写不敏感,去重 |
share_text |
string | 否 | 前端 share/分享 UI 的文案;省略时默认 分享你的AI创作(仅在 events 含 generate 时通过 /auth/:token 回显给前端) |
重复调用 /authurl 的语义:
callback 时,覆盖 现有配置(URL、events、share_text 都替换)。callback 时,保留 现有配置(不会清空)。/thirdparty/logout。action |
触发时机 | 适用模式 | 说明 |
|---|---|---|---|
consume |
用户消费积分(AI 生成完成、AI 工具调用等) | 当前模式 | 由 events: ["transaction", ...] 订阅 |
refund |
AI 生成失败、退款回写积分 | 当前模式 | 同上 |
generate |
AI 生成完成,资产 URL 已就绪 | 当前模式 | 由 events: [..., "generate"] 订阅,且 提交时 share=true(见 6.3) |
generate 事件的 per-submit opt-ingenerate 事件除了需要会话级 events 订阅,还需要单次 AI 提交时显式 opt-in:
/thirdparty/auth/:token 的响应字段 share 知道本会话是否启用了 share 开关,根据 shareText 渲染文案。用户在 share 开关打开(或贵方自行通过 JWT 调用我方 AI 接口)的情况下,AI 提交请求体携带 "share": true:
POST /api/ai-generations
{ ..., "share": true }
我方在提交时把 callback.url 快照到 ai_generations.callback_url——后续即使 callback 配置过期或被覆盖,本次生成的回调目标也不会丢失。
share: false 或未传 ⇒ 该次提交不会触发 generate 回调,即使会话订阅了 generate。per-submit 的开关具有最终决定权。我方向 callback.url 发起的请求:
POST /seselite/cb HTTP/1.1
Host: partner.example.com
Authorization: Bearer {your_api_key}
Content-Type: application/json
X-Seselite-Event-Id: {action}:{event_id}
{
"action": "consume",
"user_id": "{your_user_id}",
"event_id": "1234567890",
"data": { ... }
}
| 字段 / Header | 说明 |
|---|---|
Authorization: Bearer ... |
值即我方分配给贵方的 api_key(与贵方调用我方接口时用的 X-APP-KEY 同值);贵方应据此校验请求来自我方 |
X-Seselite-Event-Id |
{action}:{event_id} 的冗余拷贝,便于在网关/代理层做 header 级去重 |
action |
consume / refund / generate |
user_id |
贵方系统内的 user_id(同贵方 /authurl 请求中传入的值) |
event_id |
字符串化的事件 ID:consume / refund 为我方 point_transactions.id,generate 为 ai_generations.id |
data |
事件载荷,按 action 不同;见下文 |
data 字段形状:
// action=consume:amount 为负数(积分),costs 为正数(资产单位),balance 为交易后余额(资产单位)
{
"action": "consume", "user_id": "u-42", "event_id": "456",
"data": { "amount": -100, "costs": 10, "balance": 90 }
}
// action=refund:amount 为正数(积分),costs 为正数(资产单位,等于 |amount|/10),balance 为交易后余额(资产单位)
{
"action": "refund", "user_id": "u-42", "event_id": "789",
"data": { "amount": 100, "costs": 10, "balance": 100 }
}
// action=generate:assets_url 为该次生成产出的资产**完整 URL**数组
{
"action": "generate", "user_id": "u-42", "event_id": "abc",
"data": {
"assets_url": [
"https://api.example.com/files/frcreative/users/.../foo.png",
"https://cdn.example.com/.../bar.mp4"
]
}
}
consume / refund 的 data 字段(详):
| 字段 | 类型 | 单位 | 说明 |
|---|---|---|---|
amount |
float64 | 积分 | 带符号:consume 为负、refund 为正。保留为向后兼容字段,单位与 costs / balance 不同。 |
costs |
float64 | 资产 | ABS(amount) / 10(保留 2 位小数),即本次交易消耗/返还的资产单位绝对值,可直接和贵方账本比对。 |
balance |
float64 | 资产 | 本次交易之后用户在我方系统的剩余余额,单位为资产;等于 point_transactions.balance_after / 10。 |
三个字段都按
1 asset = 10 points换算;提供costs与balance是为了让贵方无需在本地再做点对资产的换算,减少四舍五入歧义。
data.assets_url中的每个元素都是绝对 URL:我方在入箱时若资产路径为/…这样的根相对路径,会用应用基础 URL(App.BaseURL)补齐;已经是http(s)://开头的则原样透传。贵方可直接对 URL 发起 GET 下载,无需拼接前缀。
(action, event_id) 去重。[30s, 2m, 10m, 1h, 4h, 16h] 退避重试;第 7 次仍失败标记 dead(死信),整体重试窗口约 21 小时。app_id 已被禁用,事件直接进入 dead 不再重试。贵方实现的 callback 端点需满足:
/authurl 校验阶段即被拒。Authorization Bearer 头——值必须与我方分配的 api_key 一致;不一致返回 401(仍会被重试,但便于排查)。(action, event_id) 幂等——同一 event_id 重复到达视为同一事件,不重复结算。POST /thirdparty/logout 在事务提交后会异步删除 Redis 中的 thirdparty:session_cfg:<userID> 键:
consume / refund:由于 cfg 已被清,不会再产生新的 outbox 行(silent skip)。generate:URL 在提交时就已快照到 ai_generations.callback_url,仍会照常投递——这是有意为之,保护贵方拿到用户当时主动 share 的产物。/logout——直接下次 /authurl 重新带 callback 覆盖即可。/api/debug/echo我方在开发环境(DevMode=true)下提供一个回显端点用于贵方排查 callback:
POST /api/debug/echo
GET /api/debug/echo
200 OK(dispatcher 因此会标记 delivered)。callback.url 指向 https://{我方dev域名}/api/debug/echo,触发一次流程,要么读我方服务器日志、要么用我方返回给 dispatcher 的 echo body 来验证 payload 形状与 Authorization 头。DevMode 时注册),故不可作为贵方 callback 联调用以外的依赖。通用错误码(详见 docs/ERROR_CODE_SYSTEM.md):
| HTTP | 业务码语义 | 触发场景 |
|---|---|---|
| 400 | ErrInvalidInput |
缺 app_key、user_id 为空或超长、is_need_pay 类型不对等参数错误 |
| 401 | ErrAuthInvalid |
api_key 错误;/logout 时 user_id 在该 app_id 下不存在 |
| 409 | ErrResourceConflict |
/logout 时发现 session 归属于另一个 app_id(用户已被别的应用接管) |
| 503 | ErrCacheUnavailable |
Redis 不可用,会话读写失败 |
logout 在写入余额时若发生数据库异常会保留 Redis session,方便贵方按相同 user_id 重试。
本节面向以 Webview / iframe 形式嵌入或深度集成我方 H5 的贵方前端工程师,详述 §2.5 概述的三个充值控制参数在 H5 内的完整行为。is_need_pay 固定为 false,由后端经 magic-link 回显;top_up_text 由后端经 /authurl 下发;parent_support_pay 则由贵方前端自行拼接到 H5 URL。
is_need_pay 仅接受 false。parent_support_pay 仍支持 yes / no / true / false,系统会自动归一化为 yes 或 no。
is_need_pay — 充值开关false:关闭我方内置充值。用户余额不足时,不跳转 static/topup.html,如需充值应由贵方收银系统处理。true 不再支持,接入请求不得传入该值。在 magic-link 流程中,该值由后端
/authurl的is_need_pay=false回显进 URL(见 §2.4)。
parent_support_pay — 父级代充值开关yes:允许父级窗口代为处理充值。当应用被嵌入在 iframe 中时,点击充值按钮会通过 postMessage 向父窗口发送充值事件,由外层页面自行实现支付逻辑。no(默认):不允许父级代充值。适用场景:平台被嵌入到第三方站点或聚合平台;外层系统已有成熟的支付体系,希望统一收银台。
事件协议:当 parent_support_pay=yes 且检测到处于 iframe 环境时,点击充值按钮会触发:
window.parent.postMessage({ type: 'topup' }, '*');
父级页面监听示例:
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'topup') {
// 打开外层系统的充值弹窗或跳转至收银台
openYourOwnPaymentModal();
}
});
parent_support_pay不是后端 API 字段,我方后端不感知它;贵方需自行将其拼接到 H5 URL。
top_up_text — 自定义充值按钮文案充值按钮(含余额不足提示框的 CTA)的文案可由后端经 /authurl 的 top_up_text 字段下发(见 §5.1),前端从 /auth/:token 响应的 topUpText 读取:
top_up_text=Buy Credits 后,所有充值按钮将显示为 "Buy Credits" 而非默认的 "Top Up"。callback / share 订阅。is_need_pay |
parent_support_pay |
环境 | 充值按钮 | 点击行为 |
|---|---|---|---|---|
false |
no |
任意 | ❌ 隐藏 | — |
false |
yes |
iframe 内 | ✅ 显示 | 向父窗口 postMessage |
false |
yes |
独立页 | ❌ 隐藏 | —(无父窗口可通知) |
is_need_pay固定为false,不启用我方内置充值;是否显示充值入口仅由parent_support_pay和页面是否处于 iframe 决定。
以下页面会根据上述参数动态调整充值入口:
/pages/account/consumption-details):页面底部是否展示「充值」按钮由 canTopUp 计算属性控制。is_need_pay=false 时不跳转我方内置充值页;parent_support_pay=yes 且处于 iframe 时,点击向父窗口发送 topup 消息。/pages/publish/ai-generator):当用户积分不足(服务端返回错误码 8001)时弹出余额不足提示框。canTopUp 决定提示框是否展示「充值」选项——允许充值时,确认按钮文案默认为 "Top Up"(可通过 top_up_text 自定义),点击后按上述规则执行;不允许充值时仅展示「确认」按钮。① iframe 嵌入,由外层统一收银
<iframe
src="https://lite-stg.zeniaiai.com/?is_need_pay=false&parent_support_pay=yes"
width="100%"
height="100%"
></iframe>
<script>
window.addEventListener('message', (e) => {
if (e.data?.type === 'topup') {
// 调起外层支付组件
yourPaymentSDK.open({ userId: 'xxx' });
}
});
</script>
② 完全关闭充值(纯展示 / 内测场景)
https://lite-stg.zeniaiai.com/?is_need_pay=false&parent_support_pay=no
Q:参数修改后没有生效? A:参数仅在应用首次加载(onLaunch)时从 URL 解析一次。若需要变更,请刷新页面或重新打开 Webview。
Q:is_need_pay 可以传其他值吗?
A:不可以。当前仅接受 is_need_pay=false;需要充值时,请在 iframe 场景下使用 parent_support_pay=yes,由贵方收银系统处理。
Q:如何在原生 App(Capacitor)中控制充值? A:当前参数主要针对 Web / H5 / iframe 场景。原生 App 的充值开关通常由构建配置或后端接口控制,不在本文档范围内。
本节说明 Android 原生 App 在 WebView 中加载本 H5,下载文件被系统拦截的解决方案
H5 登录需要 JavaScript 和 DOM Storage。WebView 容器及其父布局应提供有界的实际高度(通常为 match_parent),否则 H5 的 100vh / 100dvh 布局可能得到零高度,表现为黑屏或空白。先使用 Android 系统 WebView;AndroidX WebKit 是 WebView 的扩展兼容库,不能替代或升级设备上的 Chromium 内核。
webView.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
}
// 在 loadUrl 前注册;加载 authUrl 与直接加载 H5 均适用。
webView.addJavascriptInterface(
ExternalBrowserBridge(webView, this),
"AndroidBridge",
)
webView.loadUrl(authUrl) // 或 https://lite-stg.zeniaiai.com/ 等 H5 地址
以上示例假设代码位于 Activity,this 是 Context。不要向不可信网页或混合了第三方内容的 WebView 暴露此接口;addJavascriptInterface 对页面 frame 可见,下面检查的是顶层页面 URL,并不能认证调用来自哪个 iframe。
在 Android 项目中新建 Kotlin 类(包名按项目调整)。示例只允许从测试/生产 H5 域名调用,并只接受目标 HTTP(S) URL;如果 H5 实际页面还会使用其他受控子域,请将经过确认的完整域名加入白名单。
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.widget.Toast
import java.util.Locale
class ExternalBrowserBridge(
private val webView: WebView,
context: Context,
) {
private val appContext = context.applicationContext
private val mainHandler = Handler(Looper.getMainLooper())
private val trustedHosts = setOf(
"lite-stg.zeniaiai.com",
"h5web.seseai.com",
)
@JavascriptInterface
fun openExternalBrowser(rawUrl: String?) {
// JavaScript bridge 回调不保证运行在 UI 线程。
mainHandler.post {
val page = webView.url?.let(Uri::parse) ?: return@post
val pageHost = page.host?.lowercase(Locale.ROOT)
if (page.scheme != "https" || page.port !in setOf(-1, 443) ||
page.userInfo != null || pageHost !in trustedHosts
) return@post
val target = rawUrl?.trim()?.takeIf { it.isNotEmpty() }
?.let(Uri::parse) ?: return@post
if (target.scheme?.lowercase(Locale.ROOT) !in setOf("http", "https") ||
target.host.isNullOrBlank() || target.userInfo != null
) return@post
val intent = Intent(Intent.ACTION_VIEW, target).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
try {
appContext.startActivity(intent)
} catch (_: ActivityNotFoundException) {
Toast.makeText(appContext, "没有可打开此链接的应用", Toast.LENGTH_SHORT).show()
} catch (_: SecurityException) {
Toast.makeText(appContext, "无法打开此链接", Toast.LENGTH_SHORT).show()
}
}
}
}
Activity 退出或销毁 WebView 时移除接口并销毁 WebView,避免持有过期对象:
webView.removeJavascriptInterface("AndroidBridge")
webView.stopLoading()
webView.destroy()
如启用 R8/ProGuard,请保留 JavaScript 接口注解方法:
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
该工具会尝试调用 window.AndroidBridge.openExternalBrowser(url);因此原生对象名和方法名须保持一致。Android 的 @JavascriptInterface 方法返回时并不表示外部 App 已成功打开,因为原生启动在主线程异步执行。若系统没有可处理目标地址的应用或系统拒绝启动,会显示原生提示。
ACTION_VIEW 与 CATEGORY_BROWSABLE 将链接交给 Android 的外部 URL 处理器。普通 HTTP(S) 链接通常由默认浏览器打开;若用户没有设置默认项,系统可能显示应用选择器;若其他 App 注册了匹配的 App Links,也可能由该 App 打开。该机制请求系统处理链接,不保证一定启动某个指定浏览器。
若未注册桥接或调用失败,H5 工具可能退回 window.open;WebView 内这通常只会打开 WebView 新窗口(还需宿主实现 WebChromeClient.onCreateWindow),不会自动启动手机默认浏览器。请在真机上从实际 H5 页面点击测试,并确认最终页面域名包含在 trustedHosts 中。