Skip to content

开发者自助 OAuth 应用 API

面向普通开发者(非管理员)的自助 OAuth 应用接口:创建自己的 OAuth App、以开发模式自测、 提交人工审核、审核通过后发布为公众版本。路由前缀为 /user/oauth-apps,源码位于 apps/backend/src/routes/user/oauth-apps.ts(挂载点 apps/backend/src/app.ts)。

鉴权与安全边界

  • 只接受网站 JWTAuthorization: Bearer <JWT>):API Key、OAuth Token 不代表"这个人", 匿名 JWT 同样被拒绝;凭证缺失或无效返回 401,认证方式不符返回 403 self_service_requires_user_session
  • 自助入口只能看到并操作 management_mode = 'self_service'owner_user_id 为本人的 App;即使当前用户是管理员,也看不到 admin_managed App(那条路径在 /admin/oauth-apps)。
  • 相关数据表为 service-only RLS,读写一律经 service-role 客户端;归属隔离由路由层的 owner_user_id + management_mode 双重过滤与各 RPC 内部的 owner 校验共同保证,路由文件 本身不直接写任何业务表。
  • 数据库 RAISE EXCEPTION 抛出的 snake_case 业务码是前后端共享的稳定契约;下文错误表中 的错误码均按 apps/backend/src/utils/oauth-self-service-errors.ts 的统一映射选择 HTTP 状态码。

重要:写操作已迁移至 Team 工作区

新建、编辑、审核和发布已统一迁移到 Team 工作区(/teams/:teamId/oauth-apps/...)。为避免 滚动发布期间旧前端继续制造没有 Team/Project 归属的数据,本模块对所有非 GET/HEAD 请求 统一返回 409 oauth_app_team_context_required(在通过 JWT 校验之后拦截)。

因此下文中的 POST / PATCH / DELETE 路由的契约仍按实现保留描述,但当前调用会先命中该 409 拦截;查询类路由(GET)不受影响。

通用请求头

适用路由说明
Authorization: Bearer <JWT>全部必填,仅接受非匿名 JWT
Idempotency-Key幂等写路由可选,≤128 字符;缺省时后端生成 UUID。同键重试不会重复计次/扣款

通用错误

HTTP 状态错误码说明
401Missing or invalid authorization header未携带或凭证无效
403self_service_requires_user_session使用了 API Key / OAuth Token / 匿名 JWT
409oauth_app_team_context_required非 GET/HEAD 请求被 Team 迁移拦截(见上文)
500操作失败,请稍后重试服务端异常(已脱敏)

应用与草稿字段

创建/保存草稿的 payload 由数据库 RPC create_self_service_oauth_app / save_self_service_oauth_app_draft 校验,允许的字段固定为以下八个(含未知字段会被 400 invalid_self_service_oauth_app_payload 拒绝):

字段类型约束
namestring必填(草稿更新时可选提供),trim 后 1–120 字节
descriptionstring | null可选,≤4000 字节
logo_urlstring | null可选,须为合法 URI
homepage_urlstring | null可选,须为合法 URI
redirect_urisstring[]必填,非空数组且每项为非空字符串
scopesstring[]必填,非空数组且每项为非空字符串
authorization_configobject可选,序列化后 ≤32KB
test_evidenceobject可选,序列化后 ≤64KB

路由总览

方法路径说明
GET/user/oauth-apps我的自助 OAuth 应用列表
POST/user/oauth-apps创建自助 OAuth 应用(幂等,当前被 409 拦截)
GET/user/oauth-apps/:id应用详情(草稿 / 公众版本 / 审核记录)
PATCH/user/oauth-apps/:id保存草稿(乐观锁,当前被 409 拦截)
DELETE/user/oauth-apps/:id软删除(当前被 409 拦截)
GET/user/oauth-apps/limits自助限额与审核费用配置
GET/user/oauth-apps/products我的产品级 OAuth App 列表
GET/user/oauth-apps/products/:productId产品与客户端列表
GET/user/oauth-apps/legacy-products待迁移的历史 OAuth Product
POST/user/oauth-apps/products/:productId/clients在产品内新建客户端(当前被 409 拦截)
POST/user/oauth-apps/products/:productId/clients/attach关联既有单客户端 App(当前被 409 拦截)
PATCH/user/oauth-apps/products/:productId/clients/:clientRowId修改客户端标签(当前被 409 拦截)
POST/user/oauth-apps/:id/development-ticket签发一次性开发模式票据(当前被 409 拦截)
POST/user/oauth-apps/:id/review提交人工审核(当前被 409 拦截)
POST/user/oauth-apps/:id/review/withdraw撤回进行中的审核(当前被 409 拦截)
GET/user/oauth-apps/:id/review/precheck提交审核前预检(只读)

注:/limits/products/legacy-products 等静态子路径在源码中声明于 /:id 之前, 避免被 App 详情路由吞掉。

GET /user/oauth-apps

返回当前用户名下全部自助 OAuth App,按 created_at 倒序。每条记录附带草稿版本摘要与 进行中的审核单摘要。

http
GET /user/oauth-apps
Authorization: Bearer <JWT>

成功响应

json
{
  "success": true,
  "apps": [
    {
      "id": "0f2f7d3e-1111-4a2b-9c3d-000000000001",
      "product_id": "0f2f7d3e-1111-4a2b-9c3d-000000000001",
      "client_id": "Ab3fK9pQ2wSxYz0aBcDeFgHiJkLmNoPq",
      "client_label": null,
      "name": "示例应用",
      "current_public_revision_id": null,
      "disabled_at": null,
      "created_at": "2026-08-20T08:00:00.000Z",
      "updated_at": "2026-08-20T08:00:00.000Z",
      "published": false,
      "draft_revision": {
        "id": "3c1a...-revision-id",
        "app_id": "0f2f7d3e-1111-4a2b-9c3d-000000000001",
        "revision_number": 1,
        "name": "示例应用",
        "updated_at": "2026-08-20T08:00:00.000Z"
      },
      "active_submission": null
    }
  ]
}

publishedcurrent_public_revision_id 派生;active_submission 仅在存在 queued / in_review 状态的审核单时非空。没有应用时返回 { "success": true, "apps": [] }

常见错误

HTTP 状态错误码说明
401 / 403见「通用错误」认证失败或认证方式不符

POST /user/oauth-apps

创建自助 OAuth App(幂等)。client_id 由后端从「用户 + 幂等键」确定性派生,客户端不能 指定,防止抢注他人已知的 client_id。首次创建返回 201;同一 Idempotency-Key 且内容 一致的重放返回 200idempotent: true;同键但内容不同会被拒绝。

当前状态:该写接口被 Team 迁移中间件拦截,直接返回 409 oauth_app_team_context_required, 契约描述如下(供迁移前的客户端与兼容层参考)。

http
POST /user/oauth-apps
Authorization: Bearer <JWT>
Idempotency-Key: 8f14e45f-ea9a-4b3c-9d2e-000000000001
Content-Type: application/json

请求体

见「应用与草稿字段」一节的八个字段,其中 nameredirect_urisscopes 必填。

json
{
  "name": "示例应用",
  "description": "一个示例应用",
  "logo_url": "https://example.com/logo.png",
  "homepage_url": "https://example.com",
  "redirect_uris": ["https://example.com/callback"],
  "scopes": ["profile:read"],
  "authorization_config": {},
  "test_evidence": {}
}

成功响应

json
{
  "success": true,
  "app": {
    "id": "0f2f7d3e-1111-4a2b-9c3d-000000000002",
    "product_id": "0f2f7d3e-1111-4a2b-9c3d-000000000002",
    "client_id": "Bc4gD0qR3xTzA1bCdEfGhIjKlMnOpQrS",
    "draft_revision_id": "3c1a...-revision-id",
    "idempotent": false
  }
}

单客户端时代产品行与 App 行同源,故 product_id 回退为 id

常见错误

HTTP 状态错误码说明
400invalid_self_service_oauth_app_create请求体不是 JSON 对象
400invalid_self_service_oauth_app_payload字段缺失、类型不符或含未知字段
400oauth_self_service_redirect_uri_invalidredirect_uri / logo_url / homepage_url 不合法
409oauth_self_service_app_limit_exceeded活跃 App 数超过 max_active_apps
409oauth_self_service_create_idempotency_conflict同幂等键但请求内容不同
429oauth_self_service_create_rate_limited命中 create_per_hour 防滥用窗口
503oauth_self_service_input_limits_invalid输入超长或站点限额配置损坏
409oauth_app_team_context_required当前写操作被 Team 迁移拦截

GET /user/oauth-apps/:id

读取单个自助 App 的完整详情:App 行、草稿版本、当前公众版本、全部版本历史与最近 20 条 审核提交记录。

http
GET /user/oauth-apps/:id
Authorization: Bearer <JWT>

路径参数

参数类型说明
idUUIDApp(oauth_apps.id)ID;须属于当前用户且 management_mode = 'self_service'

成功响应

json
{
  "success": true,
  "app": {
    "id": "0f2f7d3e-1111-4a2b-9c3d-000000000001",
    "product_id": "0f2f7d3e-1111-4a2b-9c3d-000000000001",
    "client_id": "Ab3fK9pQ2wSxYz0aBcDeFgHiJkLmNoPq",
    "client_label": null,
    "name": "示例应用",
    "management_mode": "self_service",
    "owner_user_id": "1e0e...-user-id",
    "current_public_revision_id": null,
    "disabled_at": null,
    "deleted_at": null,
    "created_at": "2026-08-20T08:00:00.000Z",
    "updated_at": "2026-08-20T08:00:00.000Z"
  },
  "draft_revision": {
    "id": "3c1a...-revision-id",
    "revision_number": 1,
    "is_draft": true,
    "is_frozen": false,
    "name": "示例应用",
    "description": "一个示例应用",
    "logo_url": "https://example.com/logo.png",
    "homepage_url": "https://example.com",
    "redirect_uris": ["https://example.com/callback"],
    "scopes": ["profile:read"],
    "authorization_config": {},
    "test_evidence": {},
    "frozen_at": null,
    "created_at": "2026-08-20T08:00:00.000Z",
    "updated_at": "2026-08-20T08:00:00.000Z"
  },
  "public_revision": null,
  "revisions": [],
  "submissions": []
}

revisionsrevision_number 倒序返回全部版本;submissionscreated_at 倒序最多 20 条,字段为 id, revision_id, status, started_at, decision_reason, decided_at, quota_period, created_at, updated_at。 无草稿时 draft_revisionnull;未发布时 public_revisionnull

常见错误

HTTP 状态错误码说明
404oauth_app_not_foundApp 不存在、已删除或不属于当前用户(含非 self_service)

PATCH /user/oauth-apps/:id

保存草稿,使用乐观锁:客户端必须回传它看到的草稿 expected_updated_at,与数据库当前值 不一致时拒绝,避免覆盖并发编辑。

当前状态:被 Team 迁移中间件拦截,返回 409 oauth_app_team_context_required

http
PATCH /user/oauth-apps/:id
Authorization: Bearer <JWT>
Content-Type: application/json

请求体

字段类型约束
expected_updated_atstring(ISO 8601)必填;缺失或不可解析直接 400
其余字段见「应用与草稿字段」至少一个草稿字段;空对象、未知字段会被拒绝
json
{
  "expected_updated_at": "2026-08-20T08:00:00.000Z",
  "name": "示例应用(改)",
  "description": "更新后的描述"
}

成功响应

json
{
  "success": true,
  "draft": {
    "id": "3c1a...-revision-id",
    "app_id": "0f2f7d3e-1111-4a2b-9c3d-000000000001",
    "revision_number": 1,
    "is_draft": true,
    "name": "示例应用(改)",
    "updated_at": "2026-08-21T09:30:00.000Z"
  }
}

draft 为更新后的完整草稿版本行。

常见错误

HTTP 状态错误码说明
400invalid_self_service_oauth_app_payload请求体不是对象、为空对象或含未知字段
400oauth_app_draft_conflict缺少 expected_updated_at 或不可解析
404oauth_app_not_foundApp 不存在或不属于当前用户
409oauth_app_draft_conflict乐观锁不匹配(updated_at 已被并发修改)
409oauth_app_draft_changed_retry草稿并发变更,需刷新后重试
409oauth_app_team_context_required当前写操作被 Team 迁移拦截

DELETE /user/oauth-apps/:id

软删除自助 App:作废未消费授权码、撤销全部开发模式 token,并写入审计日志与通知 outbox。 存在进行中的审核单(queued / pending / in_review)时必须先撤回。

当前状态:被 Team 迁移中间件拦截,返回 409 oauth_app_team_context_required

http
DELETE /user/oauth-apps/:id
Authorization: Bearer <JWT>
Content-Type: application/json

请求体

字段类型约束
reasonstring数据库要求非空且 ≤4000 字符;缺失会被 400 oauth_app_delete_request_invalid 拒绝
json
{
  "reason": "不再需要该应用"
}

成功响应

json
{
  "success": true,
  "result": {
    "app_id": "0f2f7d3e-1111-4a2b-9c3d-000000000001",
    "deleted": true,
    "consumed_code_count": 2,
    "revoked_token_count": 1,
    "idempotent": false
  }
}

常见错误

HTTP 状态错误码说明
400oauth_app_delete_request_invalid缺少 reason 或超长(>4000 字符)
404oauth_app_not_foundApp 不存在、已删除或不属于当前用户
409oauth_app_review_active_withdraw_required存在进行中的审核单,需先撤回
409oauth_self_service_app_already_deletedApp 已被删除
409oauth_product_has_clients该 App 是产品主客户端且还有其它客户端
409oauth_app_team_context_required当前写操作被 Team 迁移拦截

GET /user/oauth-apps/limits

返回自助限额与费用配置,读取自 site_settings.oauth_app_self_service_limits。配置缺失或 字段非法时直接报错(不给默认值兜底),保证计费口径不会静默漂移。

http
GET /user/oauth-apps/limits
Authorization: Bearer <JWT>

成功响应

json
{
  "success": true,
  "limits": {
    "max_active_apps": 5,
    "create_per_hour": 10,
    "development_tickets_per_hour": 20,
    "free_reviews_per_month": 2,
    "review_fee_milli_credits": 1000000,
    "development_ticket_ttl_seconds": 600,
    "timezone": "Asia/Shanghai"
  }
}
字段说明
max_active_apps单用户最多活跃 App 数
create_per_hour每小时创建次数上限
development_tickets_per_hour每小时开发票据签发上限
free_reviews_per_month每月免费审核次数(按 timezone 自然月计)
review_fee_milli_credits免费额度用尽后的单次审核费用(毫积分)
development_ticket_ttl_seconds开发票据有效期(秒)
timezone免费额度结算时区

常见错误

HTTP 状态错误码说明
503oauth_app_self_service_limits_invalid站点限额配置缺失或非法,重试无效,需管理员修复

GET /user/oauth-apps/products

返回当前用户的产品级 OAuth App 聚合视图:每个产品(oauth_productsself_service 且 未删除)附其名下全部客户端摘要。没有任何活跃客户端的产品一律隐藏。产品按 created_at 倒序,客户端按 created_atid 升序。

http
GET /user/oauth-apps/products
Authorization: Bearer <JWT>

成功响应

json
{
  "success": true,
  "products": [
    {
      "id": "5d9c...-product-id",
      "primary_client_id": "0f2f7d3e-1111-4a2b-9c3d-000000000001",
      "owner_user_id": "1e0e...-user-id",
      "team_id": null,
      "management_mode": "self_service",
      "created_at": "2026-08-20T08:00:00.000Z",
      "updated_at": "2026-08-20T08:00:00.000Z",
      "name": "示例应用",
      "client_count": 2,
      "primary_client": {
        "id": "0f2f7d3e-1111-4a2b-9c3d-000000000001",
        "product_id": "5d9c...-product-id",
        "client_id": "Ab3fK9pQ2wSxYz0aBcDeFgHiJkLmNoPq",
        "client_label": "主客户端",
        "name": "示例应用",
        "current_public_revision_id": null,
        "disabled_at": null,
        "created_at": "2026-08-20T08:00:00.000Z",
        "updated_at": "2026-08-20T08:00:00.000Z",
        "published": false,
        "draft_revision": null,
        "active_submission": null
      },
      "clients": []
    }
  ]
}

产品 name 取主客户端(primary_client_id 命中者,缺省取第一个)的 nameclients 数组元素结构与 primary_client 相同。

常见错误

HTTP 状态错误码说明
401 / 403见「通用错误」认证失败或认证方式不符

GET /user/oauth-apps/products/:productId

返回单个产品及其客户端列表,聚合逻辑与列表接口一致。

http
GET /user/oauth-apps/products/:productId
Authorization: Bearer <JWT>

路径参数

参数类型说明
productIdUUID产品(oauth_products.id)ID;须属于当前用户且 self_service、未删除

成功响应

json
{
  "success": true,
  "product": {
    "id": "5d9c...-product-id",
    "primary_client_id": "0f2f7d3e-1111-4a2b-9c3d-000000000001",
    "owner_user_id": "1e0e...-user-id",
    "team_id": null,
    "management_mode": "self_service",
    "created_at": "2026-08-20T08:00:00.000Z",
    "updated_at": "2026-08-20T08:00:00.000Z",
    "name": "示例应用",
    "client_count": 1,
    "primary_client": {},
    "clients": []
  }
}

product 的字段结构同列表接口(此处 primary_client / clients 内容省略)。

常见错误

HTTP 状态错误码说明
404oauth_product_not_found产品不存在、不属于当前用户、非 self_service 或已删除

GET /user/oauth-apps/legacy-products

返回当前用户 ownership_state = 'legacy_unassigned' 且未删除的历史 OAuth Product,供前端 提示迁移到 Team 工作区。按 created_at 倒序。

http
GET /user/oauth-apps/legacy-products
Authorization: Bearer <JWT>

成功响应

json
{
  "success": true,
  "products": [
    {
      "id": "5d9c...-product-id",
      "primary_client_id": "0f2f7d3e-1111-4a2b-9c3d-000000000001",
      "owner_user_id": "1e0e...-user-id",
      "created_at": "2026-08-18T08:00:00.000Z",
      "updated_at": "2026-08-18T08:00:00.000Z"
    }
  ],
  "migration_endpoint": "/teams/:teamId/oauth-apps/products/:productId/migrate"
}

migration_endpoint 指示迁移目标端点(Team 工作区接口),:teamId / :productId 由 客户端替换。

常见错误

HTTP 状态错误码说明
401 / 403见「通用错误」认证失败或认证方式不符

POST /user/oauth-apps/products/:productId/clients

在既有产品内新建一个独立 OAuth 客户端。幂等键同时绑定 owner 与 product(后端对 Idempotency-Key 做 scope 派生),避免两个产品复用同一 Key 时撞上 owner 级唯一约束。

当前状态:被 Team 迁移中间件拦截,返回 409 oauth_app_team_context_required

http
POST /user/oauth-apps/products/:productId/clients
Authorization: Bearer <JWT>
Idempotency-Key: 2b7e...-idempotency-key
Content-Type: application/json

路径参数

参数类型说明
productIdUUID目标产品 ID,须属于当前用户且 self_service

请求体

字段类型约束
client_labelstring必填,trim 后 1–80 字符
其余字段见「应用与草稿字段」作为新客户端初始草稿 payload(nameredirect_urisscopes 必填)
json
{
  "client_label": "测试客户端",
  "name": "示例应用·测试端",
  "redirect_uris": ["https://staging.example.com/callback"],
  "scopes": ["profile:read"]
}

成功响应

首次创建 201 Created,幂等重放 200 OK

json
{
  "success": true,
  "client": {
    "id": "0f2f7d3e-1111-4a2b-9c3d-000000000003",
    "client_id": "Cd5hE1sT4yUaB2dEeFfGgHiJkLmOnPqR",
    "draft_revision_id": "3c1a...-revision-id",
    "product_id": "5d9c...-product-id",
    "client_label": "测试客户端",
    "idempotent": false
  }
}

常见错误

HTTP 状态错误码说明
400invalid_oauth_product_client_create请求体不是对象或 client_label 缺失/为空
400invalid_self_service_oauth_app_payload初始 payload 字段不合法
404oauth_product_not_found目标产品不存在或不属于当前用户
409oauth_product_client_idempotency_conflict同幂等键但内容(含 client_label)不同
409oauth_self_service_app_limit_exceeded超过 max_active_apps
429oauth_self_service_create_rate_limited命中 create_per_hour 窗口
409oauth_app_team_context_required当前写操作被 Team 迁移拦截

POST /user/oauth-apps/products/:productId/clients/attach

把一个既有的单客户端自助 App(产品行与 App 行同源的那种)并入目标产品,成为它的一个 客户端,并打上展示标签。

当前状态:被 Team 迁移中间件拦截,返回 409 oauth_app_team_context_required

http
POST /user/oauth-apps/products/:productId/clients/attach
Authorization: Bearer <JWT>
Content-Type: application/json

路径参数

参数类型说明
productIdUUID目标产品 ID,须属于当前用户且 self_service

请求体

字段类型约束
source_client_row_idstring(UUID)必填,待并入的单客户端 App 的 oauth_apps.id
client_labelstring必填,trim 后 1–80 字符
json
{
  "source_client_row_id": "0f2f7d3e-1111-4a2b-9c3d-000000000004",
  "client_label": "并入的旧客户端"
}

成功响应

json
{
  "success": true,
  "client": {
    "product_id": "5d9c...-product-id",
    "client_id": "0f2f7d3e-1111-4a2b-9c3d-000000000004",
    "client_label": "并入的旧客户端",
    "idempotent": false
  }
}

常见错误

HTTP 状态错误码说明
400invalid_oauth_product_client_attach请求体字段缺失或 client_label 为空
404oauth_product_not_found / oauth_app_not_found目标产品或来源 App 不存在/不属于当前用户
409oauth_product_source_not_standalone来源不是单客户端 App,无法并入
409oauth_product_client_idempotency_conflict同幂等键但内容不同
409oauth_app_team_context_required当前写操作被 Team 迁移拦截

PATCH /user/oauth-apps/products/:productId/clients/:clientRowId

修改产品内某个客户端的展示标签。

当前状态:被 Team 迁移中间件拦截,返回 409 oauth_app_team_context_required

http
PATCH /user/oauth-apps/products/:productId/clients/:clientRowId
Authorization: Bearer <JWT>
Content-Type: application/json

路径参数

参数类型说明
productIdUUID客户端所属产品 ID
clientRowIdUUID客户端行 ID(oauth_apps.id),须属于该产品且属于当前用户

请求体

字段类型约束
client_labelstring必填,trim 后 1–80 字符
json
{
  "client_label": "新标签"
}

成功响应

json
{
  "success": true,
  "client": {
    "product_id": "5d9c...-product-id",
    "client_id": "0f2f7d3e-1111-4a2b-9c3d-000000000003",
    "client_label": "新标签"
  }
}

常见错误

HTTP 状态错误码说明
400invalid_oauth_product_client_labelclient_label 缺失、为空或超长
404oauth_app_not_found客户端不存在、不在该产品下或不属于当前用户
409oauth_app_team_context_required当前写操作被 Team 迁移拦截

POST /user/oauth-apps/:id/development-ticket

为未首次发布的 App 签发一次性开发模式票据:未发布 App 只有 owner 本人能走授权流程, 凭据就是这张票据。后端只存票据 hash,明文仅在本次响应中返回一次,不落任何日志。票据需 带到 /oauth/authorize 使用。

有效期上限由 limits.development_ticket_ttl_seconds 下发,后端额外预留 5 秒时钟偏差 余量。scopes 会被后端去重并按字典序排序(数据库按数组顺序比对,授权端同样会归一化, 避免书写顺序差异被误报为"票据已失效")。

当前状态:被 Team 迁移中间件拦截,返回 409 oauth_app_team_context_required

http
POST /user/oauth-apps/:id/development-ticket
Authorization: Bearer <JWT>
Idempotency-Key: 9a2b...-idempotency-key
Content-Type: application/json

路径参数

参数类型说明
idUUIDApp ID,须属于当前用户且 self_service、未删除未禁用

请求体

字段类型约束
redirect_uristring必填,必须是当前草稿 redirect_uris 之一
scopesstring[]必填字符串数组;须为草稿 scopes 子集且属于允许自助申请的 scope
json
{
  "redirect_uri": "https://example.com/callback",
  "scopes": ["profile:read"]
}

成功响应

json
{
  "success": true,
  "development_ticket": "wn_dev_一次性明文票据(仅此一次返回)",
  "expires_at": "2026-08-29T08:10:00.000Z",
  "result": {
    "ticket_id": "7f3d...-ticket-id",
    "app_id": "0f2f7d3e-1111-4a2b-9c3d-000000000001",
    "revision_id": "3c1a...-revision-id",
    "redirect_uri": "https://example.com/callback",
    "scopes": ["profile:read"],
    "expires_at": "2026-08-29T08:10:00.000Z",
    "idempotent": false
  }
}

常见错误

HTTP 状态错误码说明
400oauth_development_ticket_invalid请求体不是对象、redirect_uri / scopes 形状不合法,或不在草稿 URI 与 scope 范围内
400oauth_self_service_scope_not_allowed含不允许自助申请的 scope
400oauth_development_ticket_expiry_invalid过期时间越界
409oauth_development_ticket_idempotency_conflict同幂等键但请求内容不同
429oauth_development_ticket_rate_limited命中 development_tickets_per_hour 窗口
503oauth_app_self_service_limits_invalid站点限额配置损坏
409oauth_app_team_context_required当前写操作被 Team 迁移拦截

POST /user/oauth-apps/:id/review

把当前草稿提交人工审核。免费额度未用完时直接入队;用完时客户端必须带上 confirm_paid=true 与预检(GET /:id/review/precheck)给出的费用和报价指纹——指纹对不上 说明用户看到的报价已过期,服务端拒绝并要求重新预检,保证「用户确认的金额」与「实际扣的 金额」永远一致。

当前状态:被 Team 迁移中间件拦截,返回 409 oauth_app_team_context_required

http
POST /user/oauth-apps/:id/review
Authorization: Bearer <JWT>
Idempotency-Key: 6c8e...-idempotency-key
Content-Type: application/json

路径参数

参数类型说明
idUUIDApp ID,须属于当前用户且 self_service

请求体

字段类型约束
confirm_paidboolean可选;付费审核时必须为 true
expected_fee_milli_creditsnumberconfirm_paid=true 时必填,安全正整数
expected_paid_quote_fingerprintstringconfirm_paid=true 时必填,预检返回的报价指纹
json
{
  "confirm_paid": true,
  "expected_fee_milli_credits": 1000000,
  "expected_paid_quote_fingerprint": "sha256-hex-fingerprint"
}

成功响应

201 Created(幂等重放同样返回 201,凭 idempotent 区分):

json
{
  "success": true,
  "submission": {
    "submission_id": "8b4f...-submission-id",
    "revision_id": "3c1a...-revision-id",
    "allocation_id": "9c5a...-allocation-id",
    "funding_type": "free",
    "status": "queued",
    "idempotent": false
  }
}

funding_typefree(免费额度)或 paid(付费)。

常见错误

HTTP 状态错误码说明
400oauth_review_fee_invalidconfirm_paid=true 但费用缺失、非正整数
400oauth_review_paid_confirmation_context_invalid缺少报价指纹或指纹为空
402insufficient_credits余额不足,先充值
404oauth_app_not_foundApp 不存在或不属于当前用户
404balance_not_found付费审核缺少可用计费账户
409quota_exhausted_requires_confirmation免费额度用尽但未带付费确认
409oauth_review_fee_changed / oauth_review_paid_confirmation_stale报价已过期,需重新预检
409oauth_app_review_already_pending已有进行中的审核单
409oauth_app_not_reviewableApp 当前状态不可提交审核(如无可用草稿)
409oauth_review_submit_idempotency_conflict同幂等键但提交内容不同
409oauth_app_team_context_required当前写操作被 Team 迁移拦截

POST /user/oauth-apps/:id/review/withdraw

撤回一个进行中的审核(queued / in_review)。归属双重校验:审核单必须属于该 App,且 该 App 必须属于当前用户。

当前状态:被 Team 迁移中间件拦截,返回 409 oauth_app_team_context_required

http
POST /user/oauth-apps/:id/review/withdraw
Authorization: Bearer <JWT>
Idempotency-Key: 4d7a...-idempotency-key
Content-Type: application/json

路径参数

参数类型说明
idUUIDApp ID,须属于当前用户且 self_service

请求体

字段类型约束
submission_idstring(UUID)必填,要撤回的审核单 ID
json
{
  "submission_id": "8b4f...-submission-id"
}

成功响应

json
{
  "success": true,
  "result": {
    "id": "8b4f...-submission-id",
    "app_id": "0f2f7d3e-1111-4a2b-9c3d-000000000001",
    "revision_id": "3c1a...-revision-id",
    "status": "withdrawn",
    "idempotent": false
  }
}

result 为撤回后的审核单完整记录附加 idempotent 标记(幂等重放时为 true)。

常见错误

HTTP 状态错误码说明
400oauth_review_submission_not_found请求体缺失 submission_id
404oauth_app_not_foundApp 不存在或不属于当前用户
404oauth_review_submission_not_found审核单不存在
409oauth_review_submission_not_withdrawable审核单不在可撤回状态
409oauth_review_withdraw_idempotency_conflict同幂等键但内容不同
409oauth_app_team_context_required当前写操作被 Team 迁移拦截

GET /user/oauth-apps/:id/review/precheck

提交审核前的只读预检:返回本月剩余免费次数、本次费用、当前余额与扣后余额,供前端在收费 前让用户显式确认。预检不建审核单、不计次、不扣款,也绝不隐式创建个人计费账户。

http
GET /user/oauth-apps/:id/review/precheck
Authorization: Bearer <JWT>

路径参数

参数类型说明
idUUIDApp ID,须属于当前用户且 self_service、未删除未禁用,且有未冻结草稿

成功响应

json
{
  "success": true,
  "precheck": {
    "app_id": "0f2f7d3e-1111-4a2b-9c3d-000000000001",
    "draft_revision_id": "3c1a...-revision-id",
    "period_start": "2026-08-01",
    "free_limit": 2,
    "free_used": 1,
    "free_remaining": 1,
    "requires_paid_confirmation": false,
    "review_fee_milli_credits": 1000000,
    "paid_confirmation": null,
    "billing": {
      "status": "ready"
    }
  }
}

requires_paid_confirmationtrue(免费额度用尽)且计费上下文就绪时, paid_confirmation 包含完整报价与指纹,结构如下(提交时须原样回传费用与指纹):

json
{
  "quote": {
    "schema_version": 1,
    "user_id": "1e0e...-user-id",
    "app_id": "0f2f7d3e-1111-4a2b-9c3d-000000000001",
    "draft_revision_id": "3c1a...-revision-id",
    "draft_updated_at": "2026-08-20T08:00:00.000000Z",
    "policy_config": {},
    "policy_timezone": "Asia/Shanghai",
    "policy_period": "2026-08-01",
    "free_used": 2,
    "free_limit": 2,
    "review_fee_milli_credits": 1000000,
    "billing_project_id": "2f1f...-project-id",
    "billing_project_updated_at": "2026-08-20T08:00:00.000000Z",
    "billing_team_id": "3e2e...-team-id",
    "billing_team_updated_at": "2026-08-20T08:00:00.000000Z",
    "billing_balance_id": "4f3f...-balance-id",
    "billing_balance_updated_at": "2026-08-20T08:00:00.000000Z",
    "balance_milli_credits": 5000000,
    "grant_balance_milli_credits": 2000000,
    "purchase_balance_milli_credits": 3000000,
    "balance_after_milli_credits": 4000000
  },
  "fingerprint": "sha256-hex-fingerprint"
}

常见错误

HTTP 状态错误码说明
404oauth_app_not_foundApp 不存在、不属于当前用户或没有可用草稿
404balance_not_found缺少计费账户(付费场景)
409oauth_app_review_already_pending已有进行中的审核单,先撤回
422balance_inconsistent计费数据一致性异常,需人工介入
503oauth_app_self_service_limits_invalid站点限额配置损坏

AI Workflow Editor