⌗ UPI SCAN

CDK-Scan 对外 API 对接文档

版本 v3.1(对外版) | 出具日期 2026-10-29 | Base URL https://jmlscan.cloud-ip.cc(TLS) 范围 本文档只覆盖对接方需要的接口(对外业务 / 公开只读 / 账号侧)。 供货商端、管理端等内部接口不在本文档内,也不对外提供。

验证状态:路由面(401/404 探针枚举)、限流阈值与运行时参数已于 2026-10-29 全量重新实测并据此更正(§3.4.1 单价、§8 频率限制、§4 路由清单)。


本文档与旧版(/api-doc)的差异

#差异说明
1移除内部接口旧页面曾公开列出管理端内部接口(含物理删除类操作)。本页面为公网免鉴权访问,这些内容已全部移除,只保留对接方需要的对外接口。
2补全字段级说明旧页面只有"路径 + 一行描述",无请求头表、字段表、校验规则、状态枚举、错误码。本文档全部补齐。
3错误示例为真实报文下文所有错误响应均为本实例实测原文,非手写。
4明确对外接口只有 2 个探针实测:对接方业务接口仅 POST/GET /api/v1/client/orders,其余 client/* 路径均为 404。
5参数标注"以实时接口为准"单价等运行时可变的字段,请读 GET /api/v1/public/scan/config,不要硬编码。

0. 一页速览

项目内容
是否已有对外接口✅ 已有,无需新开发
Base URLhttps://jmlscan.cloud-ip.cc(TLS)
兼容入口 https://jmlscan.cloud-ip.cc(明文,不建议用于生产)
对接方式两条路,按需选一条(详见 §3.0)<br>A. CDK 直连 —— 只凭 CDK,不用注册 / 邀请码 / API Key<br>B. 账号 API Key —— 需注册(邀请码制)+ 建 Key,可查订单 / 有账户体系
方式 A 端点POST /api/v1/public/scan/orders 凭证 = 请求体 cdk_codes 频率 1800 次/分(约 30 次/秒)
方式 B 端点POST /api/v1/client/orders(提交)、GET /api/v1/client/orders(查询) 凭证 = 请求头 X-API-Key 频率 60 次/分
开户方式(仅方式 B 需要)邀请码制:需联系站长获取 invite_code(见 §2.1)
🔴 AccessToken(AT)必填,每条链接对应一个 AT,见 §3.1
🔴 允许的支付链接仅 pay.nicepay.co.kr/* 与 payments.stripe.com/upi/instructions/*
单价0.5 / 条(实时值读 GET /api/v1/public/scan/config)
单批上限200 条
幂等强制 Idempotency-Key 请求头(8–80 字符)
频率限制对外业务 /client/* 60 次/分钟;认证接口 10 次/分钟;公开只读接口 1800 次/分钟(均按来源 IP)。详见 §8
订单超时回收claimed / processing 超 8 分钟未推进 → 自动取消并退费
回调❌ 不支持,需轮询
验证状态2026-09-23 全量实测通过,测试数据零残留

1. 基础约定

1.1 协议与地址

项值
对接地址https://jmlscan.cloud-ip.cc(TLS,推荐)
兼容入口 https://jmlscan.cloud-ip.cc(明文,不建议传输 API Key)
传输协议HTTP
数据格式JSON,UTF-8
Content-Typeapplication/json
请求体上限1 MiB(超出 → body 读取失败)

⚠️ `scan.fishmail.vip` 是另一套独立部署实例,不是本文档对接对象。 所有可变参数请运行时从 GET /api/v1/public/scan/config 读取,不要硬编码。

1.2 统一响应信封

成功

{ "code": "OK", "data": { }, "message": "success" }

失败(无 data 字段)

{ "code": "AT_REQUIRED", "message": "每个支付链接都必须提供对应的 AccessToken(AT)" }

判断依据:HTTP 状态码 + `code` 字段。code == "OK" 为成功,其余为业务错误。

1.3 时间与金额格式

类型格式示例
时间RFC3339 / UTC,秒级"2026-09-23T13:20:31Z"
时间(空值)null,或空字符串 ""(管理端部分字段)"completed_at": null
金额字符串十进制,最多 4 位小数"0.5"、"1.0"

⚠️ 金额是字符串,请用 Decimal / BigDecimal 解析,勿直接转浮点。

1.4 请求追踪

可选请求头 X-Request-ID(≤80 字符)。服务端原样回显;不传则生成 32 位十六进制串。

报障时请提供该值,可据此精确定位日志。

1.5 响应安全头(实测)

X-Request-ID: <32位hex>
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: same-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'

1.6 CORS

允许来源由服务端白名单控制(当前仅放行站长自用来源,第三方网页前端调用需先与站长确认加入白名单)。

允许的请求头:

Authorization, Content-Type, Idempotency-Key, X-API-Key, X-Request-ID

允许方法:GET, POST, OPTIONS。暴露响应头:X-Request-ID。预检返回 204。

💡 服务端到服务端调用不受 CORS 限制,推荐对接方走服务端调用。


2. 鉴权体系

凭证携带方式用途有效期
API KeyX-API-Key: csk_xxx对接方业务调用(提交/查询订单)长期,撤销即失效
Bearer TokenAuthorization: Bearer <token>账号管理(建 Key、查余额、兑换 CDK)30 天(expires_in ≈ 2591999 秒)

对接方业务代码只需 API Key;Bearer Token 仅开户/运维使用。

2.1 开户流程

Step 1|获取邀请码并注册

🔴 `invite_code` 为必填(v1.2 预告的"免邀请码"未上线)。

POST /api/v1/auth/register

字段类型必填说明
accountstring✅3–32 位,仅字母/数字/下划线
passwordstring✅6–72 字节
invite_codestring✅站长下发的邀请码
curl -X POST https://jmlscan.cloud-ip.cc/api/v1/auth/register \
  -H 'Content-Type: application/json' \
  -d '{"account":"your_account","password":"YourPass123456","invite_code":"CDK-XXXXXXXX"}'

成功 201:

{
  "code": "OK",
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIs...",
    "token_type": "Bearer",
    "expires_at": "2026-10-23T13:13:38Z",
    "expires_in": 2591999,
    "user": { "id": 414, "account": "your_account", "role": "user", "created_at": "2026-09-23T13:13:38Z" }
  },
  "message": "success"
}

相关错误

HTTPcode触发
400INVALID_REQUEST请求体不是合法 JSON
400INVALID_ACCOUNT账号格式不符
400INVALID_PASSWORD密码长度不符
400INVITE_CODE_REQUIRED未传邀请码(实测线上行为)
400INVALID_INVITE_CODE邀请码无效/过期
403REGISTRATION_CLOSED当前暂停注册(受 registration_open 开关控制)
409ACCOUNT_EXISTS账号已存在

⚠️ 区分两个开关:registration_open = true 表示"注册未暂停";它不代表"可以不传邀请码"。两者独立。

Step 2|创建 API Key

POST /api/v1/user/api-keys(Bearer)

curl -X POST https://jmlscan.cloud-ip.cc/api/v1/user/api-keys \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"my-integration"}'

成功 201(`secret` 仅此一次返回,请立即保存):

{
  "code": "OK",
  "data": {
    "id": 22,
    "name": "my-integration",
    "prefix": "csk_OkiZ7JfxdN2c…",
    "secret": "csk_OkiZ7JfxdN2clWZ27bO4nICIwv5HkjRfQbqFKX7LCrQ",
    "last_used_at": "",
    "created_at": "2026-09-23T13:20:31Z"
  },
  "message": "success"
}

每个账号最多 5 个 key(max_api_keys = 5),超出 → 409 API_KEY_LIMIT。

撤回:POST /api/v1/user/api-keys/:id/revoke → {"revoked": true}

Step 3|兑换 CDK 充值

POST /api/v1/user/cdks/redeem(Bearer)

字段类型必填说明
codesstring[]✅1–200 个充值码
{
  "code": "OK",
  "data": {
    "redeemed_count": 2,
    "credited_amount": "1.0",
    "balance": "1.0",
    "results": [
      { "code": "CDK-XXXXXXXXXXXXXXXX", "status": "redeemed", "message": "兑换成功", "amount": "0.5" }
    ]
  }
}

错误:400 CDK_REQUIRED(空数组)、400 TOO_MANY_CDKS(>200)、409 WALLET_NOT_FOUND。

Step 4|查询余额

GET /api/v1/user/overview(Bearer)→ {"balance":"1.0","frozen_balance":"0","unit_price":"0.5","cdk_purchase_url":"https://..."}


3. 接口详情

3.0 先看这个:两种提交方式,怎么选

对接方有两条路,凭证不同、扣费方式不同:

方式 A:CDK 直连(最省事)方式 B:账号 API Key
端点POST /api/v1/public/scan/ordersPOST /api/v1/client/orders
凭证CDK 本身(放在请求体 cdk_codes 里)请求头 X-API-Key: csk_...
要不要注册账号❌ 不用✅ 要(且注册需邀请码,见 §2.1)
要不要邀请码❌ 不用✅ 要
扣费来源扣 CDK 的剩余次数扣 账号余额(余额由 CDK 兑换而来)
频率限制1800 次/分钟(约 30 次/秒)60 次/分钟
查订单GET /api/v1/public/scan/orders/:receipt(凭回执号,免登录)GET /api/v1/client/orders(按账号列出)
余额查询查 CDK 剩余次数:POST /api/v1/public/scan/cdks/queryGET /api/v1/user/overview
适合一次性/低量对接、不想开户长期、高频、需要账号级账务

✅ 只想"拿卡密直接提交",用方式 A 就够 —— 不需要注册、不需要邀请码、不需要 API Key。

ℹ️ 两种方式的限流都不算严:方式 A 为 1800 次/分钟(约 30 次/秒),方式 B 为 60 次/分钟(均按来源 IP)。
方式 A 反而更宽松,方式 B 胜在能查订单、有账户与余额体系。选型请按业务需要,不必为了「更高频率」去注册。

方式 A:POST /api/v1/public/scan/orders(CDK 直连)

鉴权:无(不需要任何请求头凭证) 频率:1800 次/分钟(约 30 次/秒)

请求头必填说明
Content-Type✅application/json
Idempotency-Key✅8–80 字符。同一批重试必须复用同一个值
X-API-Key / Authorization❌不需要
字段类型必填说明
cdk_codesstring[]✅1–200 个。这些就是本次提交的"钱"
linksstring[]✅1–max_batch(当前 200)条,须匹配白名单(同 §3.1.2)
atsstring[]✅必须与 links 等长、一一对应(规则同 §3.1.3)
curl -X POST https://jmlscan.cloud-ip.cc/api/v1/public/scan/orders \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: my-order-20260927-0001' \
  -d '{
    "cdk_codes": ["CDK-XXXXX-XXXXX-XXXXX-XXXXX"],
    "links":     ["https://pay.nicepay.co.kr/v1/payment/abc123"],
    "ats":       ["eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."]
  }'

成功响应:结构与 §3.1.4 相同(batch_no / receipt / orders[] 等)。 差别:本入口扣的是 CDK 次数,响应里的 balance / frozen_balance 反映的是该 CDK 所属账号(无绑定账号时为 0);请以 `remaining_uses` 判断卡内剩余次数。

提交后请保存返回的 `receipt`(`R` 前缀) —— 后续凭它免登录查询订单状态。

错误码(均为本实例实测原文)

HTTPcode触发实测报文
400INVALID_REQUEST请求体不是对象/非法 JSON请求格式错误
400INVALID_PAYMENT_LINK存在不在白名单的链接存在无效的支付链接
400INVALID_LINK_COUNTlinks 空或超上限每次必须提交 1-200 条链接
400AT_REQUIRED未提供 ats每个支付链接都必须提供对应的 AccessToken(AT)
400AT_COUNT_MISMATCHats 与 links 数量不等AccessToken 行数必须与支付链接一一对应
400INVALID_IDEMPOTENCY_KEY幂等键缺失或长度非 8–80Idempotency-Key 长度必须为 8-80 个字符
400INVALID_CDK_COUNTcdk_codes 空或超 200每次必须提交 1-200 个卡密
400INVALID_CDK卡密不存在 / 格式错卡密不存在或格式错误
409CDK_UNAVAILABLE卡密已用尽 / 禁用 / 过期—
409CDK_USES_INSUFFICIENT卡内剩余次数 < 链接数—
409DUPLICATE_LINK链接已被提交过—
429RATE_LIMITED超 1800 次/分钟(方式 A)请求过于频繁,请稍后再试

★ 实测确认的校验顺序(排查问题时可据此定位卡在哪一步):

请求体结构 → links 白名单 → links 数量 → ats 数量
          → Idempotency-Key 长度 → cdk_codes 数量 → CDK 有效性
          → (之后)CDK 剩余次数 / 链接占用 / 派单

💡 例:同时写错链接和漏传幂等键时,会先报 INVALID_PAYMENT_LINK,不会报幂等键问题。


3.1 提交支付链接(核心接口)

POST /api/v1/client/orders 鉴权:`X-API-Key` 频率:60 次/分钟

POST /api/v1/user/orders 为同逻辑的 Bearer 版本,契约完全一致。

3.1.1 请求

请求头必填说明
X-API-Key✅csk_...
Content-Type✅application/json
Idempotency-Key✅8–80 字符,同批重试必须复用同一值
字段类型必填说明
linksstring[]✅1–max_batch(当前 200)条,须匹配白名单
atsstring[]✅必须与 `links` 等长、一一对应
curl -X POST https://jmlscan.cloud-ip.cc/api/v1/client/orders \
  -H "X-API-Key: csk_xxx" \
  -H "Idempotency-Key: order-20260923-0001" \
  -H 'Content-Type: application/json' \
  -d '{
    "links": ["https://pay.nicepay.co.kr/v1/payment/abc123"],
    "ats":   ["eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...."]
  }'

3.1.2 🔴 支付链接白名单(实测)

服务端正则:

^(?:https?://(?:www\.)?pay\.nicepay\.co\.kr/.+|https?://payments\.stripe\.com/upi/instructions/.+)$
链接是否通过
https://pay.nicepay.co.kr/v1/payment/xxx✅
https://payments.stripe.com/upi/instructions/xxx✅
https://www.pay.nicepay.co.kr/...✅
https://checkout.stripe.com/c/pay/cs_live_xxx🚫 400 INVALID_PAYMENT_LINK
其它任意域名🚫 400 INVALID_PAYMENT_LINK

3.1.3 🔴 AccessToken(AT)提交规则

场景结果(实测)
完全不传 ats400 AT_REQUIRED
ats 数量 ≠ links 数量400 AT_COUNT_MISMATCH
每条链接配一个 AT(1:1)✅ 通过校验,进入余额/CDK 扣减
ats 中夹空字符串按缺失处理,整批拒绝

AT 是 ChatGPT 的 access_token(JWT,形如 eyJhbGciOi...)。一个链接对应一个 AT,不可批量复用。

AT 会在订单被工人「完成」时自动验活:确定失效(invalid/expired/missing)= 支付成功 = 可结算;仍存活 = 支付失败 = 不结算。

3.1.4 响应(成功 201)

{
  "code": "OK",
  "data": {
    "batch_no": "B20260923MONSTERXXXXXXXXX",
    "order_count": 1,
    "total_amount": "0.5",
    "balance": "0.5",
    "frozen_balance": "0.5",
    "idempotent": false,
    "receipt": "R20260923DCVVXKWRDR6GB3",
    "remaining_uses": 9,
    "orders": [
      {
        "order_no": "O20260923LGUUI2BG7L62D42Q",
        "batch_id": 301,
        "source": "client",
        "fulfillment_mode": "supplier",
        "customer_id": 414,
        "supplier_id": null,
        "link": "https://pay.nicepay.co.kr/v1/payment/abc123",
        "status": "pending",
        "unit_price": "0.5",
        "charge_amount": "0.5",
        "supplier_amount": "0.6",
        "result": "",
        "failure_reason": "",
        "has_at": true,
        "at_email": "",
        "at_status": "",
        "at_probe_status": "",
        "at_probe_error": "",
        "at_proxy_used": "",
        "at_probe_count": 0,
        "at_checked_at": null,
        "settled_on_dead_at": false,
        "claimed_at": null,
        "started_at": null,
        "completed_at": null,
        "created_at": "2026-09-23T13:25:00Z"
      }
    ]
  },
  "message": "success"
}

字段说明

字段说明
batch_no批次号(前缀 B)
receipt★ 回执号(前缀 `R`),用于后续免登录查询,请保存
balance / frozen_balance提交后余额 / 冻结额(仅提交响应有)
remaining_usesCDK 提交后的剩余次数
idempotenttrue 表示命中幂等,未新建订单
orders[].at_email从 AT 解析出的邮箱(初始为空,验活后才回填)
orders[].settled_on_dead_attrue = 因 AT 确认失效而结算

3.1.5 提交相关错误码(实测)

HTTPcode触发条件
400INVALID_REQUEST请求体非法 JSON
400INVALID_LINK_COUNTlinks 为空或超上限
400INVALID_PAYMENT_LINK存在不在白名单的链接
400AT_REQUIRED未提供 `ats`
400AT_COUNT_MISMATCHats 与 links 数量不等
400INVALID_IDEMPOTENCY_KEY幂等键缺失或长度非 8–80
400INVALID_CDK卡密不存在/格式错误
400DUPLICATE_CDK本次提交中卡密重复
409INSUFFICIENT_BALANCE余额(含 CDK 折算)不足
409DUPLICATE_LINK链接已提交过(含本批内重复)
409CDK_UNAVAILABLE卡密已用尽/禁用/过期
409CDK_USES_INSUFFICIENT卡密剩余次数 < 链接数
409BALANCE_INCONSISTENT冻结余额异常(需联系管理员)
429RATE_LIMITED超频(响应含 Retry-After)

3.2 查询我的订单

GET /api/v1/client/orders?page=1&page_size=20(X-API-Key)

查询参数默认上限
page1—
page_size20100(超出被截断)

响应:

{
  "code": "OK",
  "data": {
    "items": [ { /* 同 3.1.4 的 order 结构 */ } ],
    "pagination": { "page": 1, "page_size": 20, "total": 0, "total_pages": 0 }
  },
  "message": "success"
}

⚠️ 本接口无搜索/筛选参数,只有 page / page_size。需按订单号精查请用下方回执查询或在工作台操作。


3.3 凭回执号查询提交结果(免登录)

GET /api/v1/public/scan/orders/:receipt 无需鉴权 频率:1800 次/分钟(约 30 次/秒)

:receipt 必须为提交时返回的 `R` 前缀回执号。

传入结果(实测)
R20260923...(正确回执)✅ 200,返回批次 + 订单列表
B20260923...(批次号)🚫 404 ORDER_NOT_FOUND
O20260923...(订单号)🚫 404 ORDER_NOT_FOUND
任意不存在值🚫 404 ORDER_NOT_FOUND

响应结构同 §3.1.4,但以下字段不返回(因其仅提交时计算):

字段提交响应回执查询
balance✅❌ 无
frozen_balance✅❌ 无
remaining_uses✅❌ 无
idempotent✅❌ 无
batch_no / receipt / orders[]✅✅

💡 这是 v1.2 文档未区分之处。回执查询只反映订单状态,不反映余额;查余额请用 GET /user/overview。


3.4 公开只读接口(无需鉴权)

3.4.1 提交配置

GET /api/v1/public/scan/config

{
  "code": "OK",
  "data": {
    "unit_price": "0.5",
    "max_batch": 200,
    "available": true,
    "accepting_supplier_count": 12,
    "cdk_purchase_url": "https://wzyp.cn/shop/66666SMS",
    "announcement": "<中文公告>",
    "announcements": { "zh": "...", "en": "...", "ja": "...", "hi": "..." },
    "countdown_seconds": 480,
    "at_required": true
  },
  "message": "success"
}

🔴 `at_required: true` — 对接方应在启动时读取此字段做自检;若为 true 而本地仍未传 ats,所有提交将被 400 拒绝。

3.4.2 提交统计

GET /api/v1/public/scan/stats → {"total_orders":2241,"completed_orders":1246}(数值随业务变化,仅示意)

3.4.3 查询 CDK 余额(免登录)

POST /api/v1/public/scan/cdks/query 频率:1800 次/分钟(约 30 次/秒)

字段类型必填
cdk_codesstring[]✅ 1–200 个
{
  "code": "OK",
  "data": {
    "cards": [
      {
        "code": "CDK-*****-*****-*****-NXOMT",
        "total_uses": 5,
        "remaining_uses": 3,
        "status": "active",
        "merged_into": ""
      }
    ],
    "total_remaining_uses": 3
  }
}

返回的 code 为掩码形式。status:active | redeemed | disabled。

merged_into:该卡若已被合并到新卡,此处为新卡的掩码;正常卡为空字符串。
合并后源卡的 status 会变为 disabled(表示已被吸收,并非作废),请按 merged_into 提示用户改用新卡。

错误:400 INVALID_CDK_COUNT(数量不在 1–200)、400 INVALID_CDK(卡密无效)。

3.4.4 合并 CDK(免登录)

POST /api/v1/public/scan/cdks/merge 频率:1800 次/分钟(约 30 次/秒)

把多张小额卡合并为一张。要求 2–20 个 CDK,且各项属性一致。

字段类型必填
cdk_codesstring[]✅ 2–20 个

成功 201:

{
  "code": "OK",
  "data": {
    "cdk": "CDK-XXXXXXXXXXXXXXXXXXXX",
    "masked_code": "CDK-*****-*****-*****-ABCDE",
    "remaining_uses": 8,
    "merged_count": 3,
    "source_cdks": ["CDK-*****-...", "CDK-*****-...", "CDK-*****-..."]
  },
  "message": "success"
}

错误:400 INVALID_CDK_MERGE(数量不在 2–20)、409 CDK_MERGE_FAILED(属性不一致等)。

⚠️ 合并后源卡进入 disabled 状态(表示已被合并吸收,并非失效作废)。

3.4.5 探活

GET /health → {"code":"OK","data":{"status":"up"},"message":"success"}

3.4.6 按 CDK 查历史记录(免登录)

POST /api/v1/public/scan/cdks/history 频率:60 次/分钟

凭 CDK 卡密查询「这张卡以前提交过哪些单」。卡被合并后,用旧卡或新卡查询都会返回整条合并链上的完整记录, 不会因为合并而丢历史(这是 2026-10-29 修复的行为:此前只返回被查询那一张卡自己的记录,旧卡会漏掉大部分订单)。

字段类型必填
cdk_codesstring[]✅ 1–200 个

响应:data.cards[] 与请求按下标一一对应,data.total_cards 为条数。每项包含:

字段说明
code卡号掩码(你查询的那张卡)
amount单次面额
total_uses / remaining_uses总次数 / 剩余次数
statusactive | redeemed | disabled
merged_into已合并时为新卡的掩码,否则空字符串
accounts[]历史订单列表:order_no、status、created_at、source_card(该单来自链上哪张卡的掩码)
error仅在卡号格式非法时出现,值为 INVALID_FORMAT

⚠️ 本接口无需登录,凭证就是 CDK 卡密本身。请勿把卡密提交给非官方地址;返回内容含历史订单信息,请自行妥善保管。

错误:400 INVALID_CDK_COUNT(数量不在 1–200);单张卡格式非法不会整体报错,而是在对应下标返回 error: INVALID_FORMAT。

3.4.7 按回执查接单人(免登录)

GET /api/v1/public/scan/receipt-supplier?receipt=<回执号> 频率:60 次/分钟

凭回执号查询该批次由哪位接单人处理,用于顾客侧展示「你的单被谁接了」。

字段说明
data.receipt回执号
data.batch_no该回执对应的批次号
data.suppliers[]{ id, account } 列表。一个批次可能由多人分抢,故为数组;按 id 分组统计各自成功/失败数
data.supplier仅向后兼容保留的单值(等于 suppliers[0],无人接单时为 null)。新对接请只用 suppliers

错误:400 INVALID_RECEIPT(回执号格式非法)、404 RECEIPT_NOT_FOUND(回执不存在)、503 DB_ERROR(查询失败,可重试)。


3.5 工作台(/user/)接口(Bearer)

接口方法说明
/api/v1/user/overviewGET余额 / 单价 / CDK 购买地址
/api/v1/user/ordersPOST提交订单(与 `/client/orders` 同契约,需 {links, ats})
/api/v1/user/ordersGET我的订单(page / page_size ≤100)
/api/v1/user/order-batchesGET我的批次列表(含 batch_no / input_count / order_count / total_amount)
/api/v1/user/orders/:order_no/cancelPOST取消订单(pending/claimed/processing 可取消,自动退费)
/api/v1/user/ledgerGET余额流水
/api/v1/user/withdrawalsPOST / GET申请 / 查询提现
/api/v1/user/cdks/redeemPOSTCDK 充值
/api/v1/user/api-keysGET / POST列出 / 创建 API Key
/api/v1/user/api-keys/:id/revokePOST撤销 API Key
/api/v1/user/order-availabilityGET接单可用性

提现请求体(POST /user/withdrawals):{"amount":"1.0","method":"...","account":"..."}


4. 完整路由清单(实测枚举)

以下为 2026-10-29 对该实例逐条探测得到的完整路由面(404 者不在表中)。

4.1 公开(无鉴权)

GET    /health
GET    /api/v1/public/scan/config
GET    /api/v1/public/scan/stats
GET    /api/v1/public/scan/receipt-supplier            60 次/分
POST   /api/v1/public/scan/orders                    1800 次/分
GET    /api/v1/public/scan/orders/:receipt           1800 次/分
POST   /api/v1/public/scan/cdks/query                1800 次/分
POST   /api/v1/public/scan/cdks/merge                1800 次/分
POST   /api/v1/public/scan/cdks/history                60 次/分

4.2 认证

POST   /api/v1/auth/register                           10 次/分
POST   /api/v1/auth/login                              10 次/分
GET    /api/v1/auth/me
POST   /api/v1/auth/logout
POST   /api/v1/auth/change-credentials

4.3 用户(Bearer,role ∈ {user, admin})

GET    /api/v1/user/overview
GET    /api/v1/user/ledger
POST   /api/v1/user/cdks/redeem
GET    /api/v1/user/withdrawals
POST   /api/v1/user/withdrawals
GET    /api/v1/user/order-availability
GET    /api/v1/user/api-keys
POST   /api/v1/user/api-keys
POST   /api/v1/user/api-keys/:id/revoke
POST   /api/v1/user/orders
GET    /api/v1/user/orders
GET    /api/v1/user/order-batches
POST   /api/v1/user/orders/:order_no/cancel

4.4 对外业务(API Key)

POST   /api/v1/client/orders                           60 次/分
GET    /api/v1/client/orders                           60 次/分

4.5 静态页面

路径内容
/工作台(免登录提交页)
/scan/免登录提交页(多语言 + CDK 融合面板)
/user/工作台(与 / 同一文件)
/admin/管理后台
/supplier/供货商端
/pay/支付/CDK 管理页(含 CDK 融合入口)
/api-doc本 API 文档页

5. 订单状态机

pending ──claim──▶ claimed ──start──▶ processing ──complete(AT判死)──▶ completed
   │                  │                    │
   │                  │                    └──fail──▶ failed
   │                  │
   └────cancel────────┴────────cancel──────┴──────────────▶ cancelled
状态含义可否取消
pending待接单✅
claimed已被工人接单✅
processing工人处理中✅
completed已完成(AT 确认失效 + 已结算)❌
failed失败(AT 仍存活 / 工人标记失败)❌
cancelled已取消(自动退费)❌

超时回收:claimed / processing 超过 480 秒未推进 → 自动取消并退费(order_timeout_minutes = 8)。

5.1 AT 状态枚举(8 值)

值含义是否算「确定失效」可否结算
missing无 AT(本地判定)✅✅
invalidJWT 不可解析 / HTTP 401✅✅
expiredJWT exp 已过期(本地判定)✅✅
normalAT 正常(200/204 或越过认证层的 4xx)❌(=存活)🚫
reviewHTTP 403,人工复核,不认定死亡❌🚫
rate_limitedHTTP 429❌🚫
network超时/连接失败/未知❌🚫
validation_error程序解析异常❌🚫

只有 `missing` / `invalid` / `expired` 三者算「确定失效」,才触发结算。其余状态订单保持 processing 供重试,对应错误码 409 AT_INDETERMINATE。


6. 关键机制说明

6.1 幂等

  • Idempotency-Key 强制,8–80 字符
  • 同 customer_id + 同 key 重复提交 → 返回首次结果,idempotent: true,不重复建单、不重复扣费
  • 重试必须复用同一 key;换 key 视为新提交

重试建议:网络超时/5xx → 用同 key 重试(最多 3 次,指数退避 1s/2s/4s);收到 429 → 按 Retry-After 等待;收到 4xx 业务错误 → 不要重试,修正参数。

6.2 CDK 计费

  • CDK 按「次数」计费,1 条链接消耗 1 次
  • 提交时原子扣减;订单被取消 → 自动返还(cdk_usages.refunded_uses 增加)
  • 某张卡剩余次数不足时,会自动跨卡凑足(多卡联合扣减)
  • 卡状态:active(可用)|redeemed(已用尽)|disabled(已合并到其它卡,非作废)

6.3 AT 验活链路(含本期修复)

工人点「完成」
   ↓
事务外:读取订单快照
   ↓
AT 验活(双层)
   ├─ 本地层:JWT 解析 + exp 检查(不联网)→ missing / invalid / expired
   └─ 在线层:GET 探针端点
        ↓ 经站内本地中继 http://站内中继(Chrome TLS 指纹)
        ↓ 转发至 https://chatgpt.com/backend-api/me
        ├─ 401 → invalid(确定失效 → 可结算)
        ├─ 200/204 → normal(存活 → 不结算)
        ├─ 403 → review(不认定死亡)
        ├─ 429 → rate_limited
        └─ 超时/连接失败 → network
   ↓
判定:确定失效 → 进事务加行锁 → settleFrozen + CreditOrder → completed
     仍存活   → 409 AT_STILL_ALIVE,保持 processing
     不确定   → 409 AT_INDETERMINATE,保持 processing

本期变更:Cloudflare 自 2026-09-23 起对 chatgpt.com 启用 TLS/JA3 指纹挑战,Go 标准库 net/http 的 TLS 指纹不像浏览器 → 一律 403,导致所有 AT 验活失效(换 IP 无效,实测新老代理池与直连全部 403)。

修复方式:站内新增本地中继(curl_cffi 的 Chrome 指纹),AT_PROBE_ENDPOINT 指向 站内中继端点。

对对接方接口契约零影响 —— 但若验活大面积返回 review,请勿反复调用 complete,应联系站长排查中继状态。

6.4 结算口径(重要)

  • 归属账户 = `COALESCE(上级 parent_id, 自己)`(即老板账号承接其名下工人的收益)
  • 恒等式:balance + frozen_balance + total_withdrawn == total_earned
  • 一 CDK 多账号提交时,只结算 1 单,不追讨工人余额
  • 冻结余额在提交时产生;订单终结(completed / failed / cancelled)时解冻

7. 错误码全表

7.1 通用 / 鉴权

HTTPcode说明
400INVALID_REQUEST请求格式错误
401UNAUTHORIZED未提供凭证
401INVALID_TOKENToken 失效,请重新登录
401INVALID_API_KEYAPI Key 无效或已撤销
403FORBIDDEN无权访问该资源
403USER_DISABLED账号已被禁用
404NOT_FOUND资源不存在
429RATE_LIMITED请求过于频繁(含 Retry-After)
500INTERNAL_ERROR服务端内部错误

7.2 注册 / 登录

HTTPcode说明
400INVALID_ACCOUNT账号格式不符(3–32 位字母/数字/下划线)
400INVALID_PASSWORD密码长度不符
400INVITE_CODE_REQUIRED未传邀请码
400INVALID_INVITE_CODE邀请码无效或过期
400CURRENT_CREDENTIALS_INVALID当前用户名或旧密码错误
401INVALID_CREDENTIALS账号或密码错误
403REGISTRATION_CLOSED当前已暂停注册
409ACCOUNT_EXISTS账号已存在

7.3 提交订单

HTTPcode说明
400INVALID_LINK_COUNT链接数不在 1–max_batch
400INVALID_PAYMENT_LINK链接不在白名单
400AT_REQUIRED未提供 `ats`
400AT_COUNT_MISMATCHats 与 links 数量不等
400INVALID_IDEMPOTENCY_KEY幂等键缺失/长度错误
400INVALID_CDK卡密不存在或格式错误
400DUPLICATE_CDK本批中卡密重复
400CDK_REQUIRED未提供充值码
400TOO_MANY_CDKS充值码超 200
400INVALID_CDK_COUNT卡密数不在 1–200
400INVALID_SUPPLIER_AMOUNT供货商金额格式错误(管理端)
409INSUFFICIENT_BALANCE余额不足
409DUPLICATE_LINK链接已提交过
409CDK_UNAVAILABLE卡密已用尽/禁用/过期
409CDK_USES_INSUFFICIENT卡密剩余次数不足
409CDK_MERGE_FAILEDCDK 合并失败
409CDK_REFUND_UNAVAILABLE卡密次数返还失败
409SUPPLIER_NOT_ACCEPTING未开启接单
409BALANCE_INCONSISTENT冻结余额异常

7.4 订单查询 / 操作

HTTPcode说明
404ORDER_NOT_FOUND订单/回执不存在
404AT_NOT_FOUND该订单未保存 AT
409ORDER_NOT_AVAILABLE订单已被其他供货商接走
409INVALID_ORDER_STATUS当前状态不允许此操作
403FORBIDDEN无权操作该订单
409AT_STILL_ALIVEAT 仍存活,判定支付失败,不予结算
409AT_INDETERMINATEAT 状态无法确认,请稍后重试
400FAILURE_REASON_REQUIRED未填写失败原因

7.5 提现

HTTPcode说明
400INVALID_AMOUNT提现金额格式错误
400INVALID_WITHDRAWAL提现信息错误
409WALLET_NOT_FOUND余额账户不存在
409SUPPLIER_ACCOUNT_NOT_FOUND供货商资金账户不存在
409WITHDRAWAL_REVIEWED提现单已处理
404WITHDRAWAL_NOT_FOUND提现单不存在
422INSUFFICIENT_BALANCE可用余额不足(用户端)

8. 频率限制

范围实际限制
POST/GET /api/v1/client/orders(含 /client/*)60 次/分钟(按来源 IP)
/api/v1/auth/login、register、change-credentials10 次/分钟
POST /api/v1/public/scan/orders1800 次/分钟(约 30 次/秒)
POST /api/v1/public/scan/cdks/query1800 次/分钟(约 30 次/秒)
POST /api/v1/public/scan/cdks/merge1800 次/分钟(约 30 次/秒)
GET /api/v1/public/scan/orders/:receipt1800 次/分钟(约 30 次/秒)
GET /api/v1/public/scan/receipt-supplier60 次/分钟
POST /api/v1/public/scan/cdks/history60 次/分钟
其余 /api/v1/user/*、/api/v1/admin/*、/api/v1/supplier/*1200 次/分钟(约 20 次/秒)

⚠️ 同一请求可能同时命中路径桶与全局桶,取最严的那一条生效。表中为 2026-10-29 实测的稳态速率。

超限响应 429,附 Retry-After: <秒>。

⚠️ 服务端判定客户端 IP 时信任 TRUSTED_PROXIES 白名单;伪造 `X-Forwarded-For` 无效。


9. 联调 Checklist

  • [ ] 已向站长获取 invite_code
  • [ ] 注册成功并保存 access_token
  • [ ] 创建 API Key 并保存 secret(仅一次可见)
  • [ ] 兑换 CDK,确认 GET /user/overview 余额 > 0
  • [ ] 读取 GET /public/scan/config,确认 at_required 与 unit_price
  • [ ] 构造 {links, ats},两者长度一致
  • [ ] 链接确认命中白名单(nicepay 或 stripe upi)
  • [ ] 每次提交带唯一 Idempotency-Key(8–80 字符)
  • [ ] 重试必须复用同一 Idempotency-Key
  • [ ] 保存返回的 receipt,用于后续查询
  • [ ] 轮询间隔建议 ≥ 5 秒,避免触发限流

10. cURL 全流程

BASE=https://jmlscan.cloud-ip.cc

# 1) 注册(invite_code 必填)
curl -s -X POST $BASE/api/v1/auth/register \
  -H 'Content-Type: application/json' \
  -d '{"account":"my_account","password":"MyPass123456","invite_code":"CDK-XXXXXXXX"}'

# 2) 创建 API Key
TOKEN=<上一步返回的 access_token>
curl -s -X POST $BASE/api/v1/user/api-keys \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"prod"}'

# 3) 兑换 CDK
curl -s -X POST $BASE/api/v1/user/cdks/redeem \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"codes":["CDK-XXXXXXXXXXXXXXXX"]}'

# 4) 提交(★ ats 必填且与 links 一一对应)
KEY=csk_xxxxxxxx
curl -s -X POST $BASE/api/v1/client/orders \
  -H "X-API-Key: $KEY" \
  -H "Idempotency-Key: mybatch-20260923-001" \
  -H 'Content-Type: application/json' \
  -d '{
    "links": ["https://pay.nicepay.co.kr/v1/payment/abc123"],
    "ats":   ["eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...."]
  }'

# 5) 查询(按回执号,免鉴权)
curl -s $BASE/api/v1/public/scan/orders/R20260923XXXXXXXXXXXXXX

# 6) 查询(按账号,API Key)
curl -s "$BASE/api/v1/client/orders?page=1&page_size=20" -H "X-API-Key: $KEY"

11. Python 示例(标准库,无第三方依赖)

import json, time, urllib.request, urllib.error

BASE = "https://jmlscan.cloud-ip.cc"

def call(method, path, body=None, token=None, api_key=None, headers=None):
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(BASE + path, data=data, method=method)
    req.add_header("Content-Type", "application/json")
    if token:
        req.add_header("Authorization", "Bearer " + token)
    if api_key:
        req.add_header("X-API-Key", api_key)
    for k, v in (headers or {}).items():
        req.add_header(k, v)
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            return r.status, json.loads(r.read().decode())
    except urllib.error.HTTPError as e:
        return e.code, json.loads(e.read().decode())

# 1) 提交(每条链接配一个 AT)
def submit(api_key, links, ats, idem_key):
    assert len(links) == len(ats), "links 与 ats 必须一一对应"
    status, data = call("POST", "/api/v1/client/orders",
                        {"links": links, "ats": ats},
                        api_key=api_key,
                        headers={"Idempotency-Key": idem_key})
    if status != 201:
        raise RuntimeError(f"提交失败 {status}: {data}")
    return data["data"]["receipt"]

# 2) 轮询(按回执号,免鉴权)
def poll(receipt, interval=5, timeout=480):
    deadline = time.time() + timeout
    while time.time() < deadline:
        status, data = call("GET", f"/api/v1/public/scan/orders/{receipt}")
        if status != 200:
            raise RuntimeError(f"查询失败 {status}: {data}")
        orders = data["data"]["orders"]
        if all(o["status"] in ("completed", "failed", "cancelled") for o in orders):
            return orders
        time.sleep(interval)
    raise TimeoutError("轮询超时")

if __name__ == "__main__":
    API_KEY = "csk_xxxxxxxx"
    links = ["https://pay.nicepay.co.kr/v1/payment/abc123"]
    ats   = ["eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...."]

    receipt = submit(API_KEY, links, ats, "mybatch-20260923-001")
    print("receipt:", receipt)
    for o in poll(receipt):
        print(o["order_no"], o["status"], "at_status=", o.get("at_status"))

12. 运行时参数(2026-10-29 实测)

以下值全部来自 `GET /api/v1/public/scan/config`,会随运营调整而变。

字段2026-10-29 实测说明
unit_price"0.5"每条链接的扣费(字符串,请用 Decimal 解析)
max_batch200单批链接上限
at_requiredtrueAT 强制:为 true 时必须传 ats
availabletrue当前是否可接单
accepting_supplier_count12在线接单人数(动态)
countdown_seconds480工人接单倒计时(秒)
cdk_purchase_urlhttps://wzyp.cn/shop/66666SMSCDK 购买地址
announcement(中文文本)站点公告
announcements{zh,en,ja,hi}多语言公告字典

🔴 以上任何一项都不要硬编码。 单价会调、接单人数会变、available 会翻转。

建议:每次提交前读一次 `/config`;available == false 时暂缓提交,避免无谓的 409。

账号级固定限制(不会变,可写死):

项值
每账号 API Key 上限5
Idempotency-Key 长度8–80 字符
单批链接 / 卡密上限200
CDK 合并数量2–20
订单超时回收8 分钟

13. 快速排障

现象原因与处理
400 AT_REQUIRED未传 ats。读 GET /public/scan/config 的 at_required 确认;补齐 AT
400 AT_COUNT_MISMATCHats 与 links 长度不等,逐条对齐
400 INVITE_CODE_REQUIRED注册未带邀请码;联系站长获取(此项并未放开)
400 INVALID_PAYMENT_LINK链接不在白名单;仅接受 pay.nicepay.co.kr/* 或 payments.stripe.com/upi/instructions/*
400 INVALID_IDEMPOTENCY_KEY幂等键缺失或长度不在 8–80
409 INSUFFICIENT_BALANCE余额/CDK 折算不足;兑换 CDK 或减少条数
409 AT_STILL_ALIVEAT 仍存活 = 支付未成功,属正常拒结算;确认该 AT 对应的支付是否真的失败
409 AT_INDETERMINATE验活网络异常/限流/403;稍后重试,勿频繁调用
404 ORDER_NOT_FOUND回执查询传了批次号/订单号;必须传 `R` 前缀回执号
400 INVALID_REQUEST(供货商)开关接单传了 accepting;应传 `enabled`
409 SUPPLIER_NOT_ACCEPTING未开启接单;先 POST /supplier/accepting {"enabled":true}
429 RATE_LIMITED超频;按 Retry-After 等待
探活请求被直接断开若用脚本探活,请带浏览器风格 User-Agent;服务端对异常 UA 会直接断开

14. 当前不支持的能力

能力状态
订单状态回调 / Webhook❌ 需轮询
订单搜索 / 高级筛选参数❌ 仅 page / page_size
HTTPS❌ 当前为 HTTP(如需请与站长确认)
批量取消订单❌ 单条取消
自助修改单价/超时❌ 管理端设置
免邀请码注册❌ 未上线(v1.2 预告作废)

附录 A:本文档的验证方法

为避免"凭记忆写文档",本文档所有条目经三重交叉验证:

1. 路由枚举:对该实例逐条发起真实请求(含不存在路径作对照), 以 HTTP 状态码区分「存在」(401/400/409/200)与「不存在」(404),得到 §4 的路由面。

探针必须带浏览器 UA —— 否则可能被网关直接断连,把全部路由误报成"不存在"。

2. 真实 E2E:注册账号 → 登录 → 建 Key → 提交 → 查询全链路跑通,逐项记录真实响应体。 本文档所有错误响应均为实际触发的报文原文,非手写推测。 3. 字段交叉核对:以线上实测行为为准,与服务端实现逐字段比对。 凡"实现与实测冲突",一律以实测为准并在此登记。

2026-09-27 复核结果(相对 09-23 版):

检查项结果
对外路由面✅ 无变化(对接方业务接口仍为 2 个)
公开只读接口✅ 全部可用
错误码✅ 关键错误报文重新抓取,与 09-23 一致
运行时参数⚠️ 与 09-23 不同(见 §10),再次印证不要硬编码

由此发现并纠正的历史错误:

项旧文档描述实测真相
注册邀请码"已改为可留空,勿传"必填,不传 400 INVITE_CODE_REQUIRED
支付链接白名单仅 nicepaynicepay + Stripe UPI 双前缀
supplier/accepting 字段名未记录`enabled`(非 accepting)
回执查询响应与提交响应同构不含 balance / frozen_balance / remaining_uses
对外 client 接口数量一度被写成 6~8 个实测只有 2 个,其余 client/* 均 404

2026-10-29 复核(v3.0 → v3.1):对全站重新做了路由探针与配置读取,更正与新增如下。

检查项v3.0 记载2026-10-29 实测处理
unit_price"0.8""0.5"已更正 §3.4.1 / §12 / 速览
supplier_unit_price"0.6"该字段不存在已从 §3.4.1 删除
accepting_supplier_count14 / 1812(动态)已更新并标注动态
/client/* 限流120 次/分60 次/分已更正 §8 / §4.4
/auth/login|register|change-credentials20 次/分10 次/分已更正 §8 / §4.2
/public/scan/orders 等公开接口10–120 次/分1800 次/分已更正 §8 / §4.1
POST /public/scan/cdks/history未收录存在,60 次/分已新增 §3.4.6
GET /public/scan/receipt-supplier未收录存在,60 次/分已新增 §3.4.7
/public/scan/cdks/query 响应4 个字段新增 merged_into(掩码)已补 §3.4.3
Base URLhttps://jmlscan.cloud-ip.cc另有 https://jmlscan.cloud-ip.cc已改为推荐 HTTPS

未变更项:max_batch=200、at_required=true、available=true、countdown_seconds=480、cdk_purchase_url、 §4 已列的 27 条路由全部可达,/user/withdrawals 的 {amount, method, account} 字段名无误。



Gopay 通道(独立大类)

补充章节 | 出具日期 2026-11-03 | 适用支付渠道 Midtrans(Gopay) | 与上文 §1–§14(UPI 通道)物理隔离、互不影响

📌 本大类是对上文的补充,上文 UPI 通道的内容一字未改。 两套通道在卡密前缀、支付链接、接口路径、订单标记上完全分开:UPI 的卡进不了 Gopay 通道,Gopay 的链接也进不了 UPI 通道。 选哪套只看一件事:你手上拿到的是哪种卡密。

G0. 两套通道对照(先看这张表)

维度UPI 通道(§1–§14)Gopay 通道(本大类)
卡密前缀CDK-XXXXX-XXXXX-XXXXX-XXXXX(27 字符)GOPAY-XXXXX-XXXXX-XXXXX-XXXXX(29 字符)
支付链接payments.stripe.com/upi/instructions/*
pay.nicepay.co.kr/*
app.midtrans.com/snap/v4/redirection/<uuid>
二维码有(Stripe 页面内展示)❌ 没有,只有跳转链接
接口前缀/api/v1/public/scan/*/api/v1/public/gopay/*
查卡余量POST /public/scan/cdks/query(§3.4.3)POST /public/gopay/cdks/query(G1.1)
提交订单POST /public/scan/orders(§3.0 方式 A)POST /public/gopay/orders(G1.2)
凭回执查询GET /public/scan/orders/:receipt(§3.3)GET /public/gopay/orders/:receipt(G1.3)
订单标记pay_method = "upi"pay_method = "gopay"
链接有效期按平台规则(不单独限制)15 分钟,到点自动判定(见 G3)
卡密合并✅ 支持(2–20 张,§3.4.4)❌ 不支持
鉴权无(凭证 = 卡密本身)无(凭证 = 卡密本身)
计费方式扣卡密剩余次数,1 条链接 = 1 次同
单价读 unit_price(当前 "0.5")同一 unit_price,不要硬编码

⚠️ `/api/v1/public/gopay/*` 是独立服务,与 UPI 通道不是同一套代码路径。 因此它们的错误码集合、响应字段有细微差异,本文已逐项标注;不要把 UPI 的排障经验直接套到 Gopay 上(最容易踩的是:Gopay 查卡响应没有 merged_into)。

G1. 三个公开接口

G1.0 路由一览

POST   /api/v1/public/gopay/cdks/query               1800 次/分
POST   /api/v1/public/gopay/orders                   1800 次/分
GET    /api/v1/public/gopay/orders/:receipt          1800 次/分

只有这三条。 /api/v1/public/gopay/ 下的其它任何路径一律 404 NOT_FOUND(响应体 {"code":"NOT_FOUND","message":"接口不存在"})。

频率限制与 UPI 公开接口一致:1800 次/分钟(约 30 次/秒,按来源 IP)。超限 429。详见 §8。

响应信封与 §1.2 完全相同:成功 {"code":"OK","data":{...},"message":"success"};失败 {"code":"...","message":"..."}(无 data)。

G1.1 查询 Gopay 卡余量

POST /api/v1/public/gopay/cdks/query 无需鉴权 频率:1800 次/分钟

字段类型必填说明
cdk_codesstring[]✅1–200 个,必须全部是 GOPAY- 开头的卡
curl -X POST https://jmlscan.cloud-ip.cc/api/v1/public/gopay/cdks/query \
  -H 'Content-Type: application/json' \
  -d '{"cdk_codes":["GOPAY-XXXXX-XXXXX-XXXXX-XXXXX"]}'

成功 200

{
  "code": "OK",
  "data": {
    "cards": [
      {
        "code": "GOPAY-*****-*****-*****-NXOMT",
        "total_uses": 5,
        "remaining_uses": 3,
        "status": "active"
      }
    ],
    "total_remaining_uses": 3
  },
  "message": "success"
}
字段说明
cards[]与请求 cdk_codes 顺序无关(按卡哈希排序返回),请以 code 匹配
cards[].code卡号掩码,前缀一定是 GOPAY-
cards[].total_uses / remaining_uses总次数 / 剩余次数(整数)
cards[].statusactive|redeemed(已用尽)|disabled(被禁用)
total_remaining_uses本次查询所有卡的剩余次数合计

⚠️ 与 UPI 查卡(§3.4.3)的唯一差异:本接口不返回 `merged_into`。 Gopay 卡不支持合并,所以无需判断「已合并到新卡」。若你的代码复用了 UPI 的解析逻辑,请把 merged_into 当可选字段处理。

错误

HTTPcode触发报文
400INVALID_REQUEST请求体不是合法 JSON请求格式错误
400INVALID_CDK_COUNT数量不在 1–200每次必须查询 1-200 个卡密
400INVALID_CDK格式不对 / 卡不存在 / 不是 Gopay 专用卡卡密格式不正确(Gopay 卡应为 GOPAY-XXXXX-XXXXX-XXXXX-XXXXX) · 卡密不存在 · 该卡密不是 Gopay 专用卡,请在 UPI 通道使用
400DUPLICATE_CDK本批内卡密重复卡密重复

G1.2 提交支付链接(核心接口)

POST /api/v1/public/gopay/orders 无需鉴权 频率:1800 次/分钟 成功返回 201

请求头必填说明
Content-Type✅application/json
Idempotency-Key✅8–80 字符。同一批重试必须复用同一个值
X-API-Key / Authorization❌不需要(Gopay 通道只走卡密直连)
字段类型必填说明
cdk_codesstring[]✅1–200 个,必须全部是 GOPAY- 卡。这就是本次提交的「钱」
linksstring[]✅1–max_batch(当前 200)条,必须全部是 Midtrans 链接(见 G3)
atsstring[]⚠️ 强烈建议与 links 等长、一一对应。详见下方说明
curl -X POST https://jmlscan.cloud-ip.cc/api/v1/public/gopay/orders \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: gopay-20261103-0001' \
  -d '{
    "cdk_codes": ["GOPAY-XXXXX-XXXXX-XXXXX-XXXXX"],
    "links":     ["https://app.midtrans.com/snap/v4/redirection/0f9dff28-55ed-4eba-b712-c2eb5a404c74"],
    "ats":       ["eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."]
  }'

🔴 关于 `ats`:业务上必须传,且一条链接一个 AT。 服务端不会因为 ats 为空而拒绝建单(与 UPI 通道的 400 AT_REQUIRED 不同), 但没有 AT 的订单无法做 G3 的到期判定,会一直停在处理中、只能人工处理。 请照 §3.1.3 的规则准备 AT,不要依赖这条「能过」。

成功 201

{
  "code": "OK",
  "data": {
    "batch_no": "B20261103ABCDEFGHIJKLMNOP",
    "order_count": 1,
    "total_amount": "0.5",
    "balance": "0",
    "frozen_balance": "0",
    "idempotent": false,
    "receipt": "R20261103ABCDEFGHIJKLMN",
    "remaining_uses": 4,
    "orders": [
      {
        "order_no": "O20261103QRSTUVWXYZABCDEF",
        "batch_id": 321,
        "source": "guest",
        "fulfillment_mode": "supplier",
        "customer_id": null,
        "supplier_id": null,
        "link": "https://app.midtrans.com/snap/v4/redirection/0f9dff28-55ed-4eba-b712-c2eb5a404c74",
        "status": "pending",
        "unit_price": "0.5",
        "charge_amount": "0.5",
        "supplier_amount": "0.7",
        "result": "",
        "failure_reason": "",
        "has_at": true,
        "at_email": "[email protected]",
        "at_status": "",
        "at_probe_status": "",
        "at_probe_error": "",
        "at_proxy_used": "",
        "at_probe_count": 0,
        "at_checked_at": null,
        "settled_on_dead_at": false,
        "claimed_at": null,
        "started_at": null,
        "completed_at": null,
        "created_at": "2026-11-03T12:30:00Z",
        "link_expires_at": "2026-11-03T12:45:00Z",
        "pay_method": "gopay"
      }
    ]
  },
  "message": "success"
}
字段说明
batch_no批次号(前缀 B)
receipt★ 回执号(前缀 `R`),用于免登录查询,请保存
balance / frozen_balance恒为 `"0"`。Gopay 通道按卡密次数计费,没有账户余额概念;请以 remaining_uses 判断卡内剩余
remaining_uses本批扣卡后,所选卡片的剩余次数合计
idempotenttrue = 命中幂等,未新建订单、未重复扣卡
orders[].pay_method★ 恒为 "gopay",可用于区分通道
orders[].link_expires_at★ Gopay 专有:链接失效时间 = created_at + 15 分钟(RFC3339)。可直接用来做倒计时
orders[].customer_idnull(卡密直连无账号)
orders[].sourceguest
orders[].supplier_amount接单人侧的结算额(与你无关,仅供排查时辨认)
orders[].at_email从 AT 解析出的邮箱,提交时即回填(UPI 通道是验活后才回填)

错误码(Gopay 通道实测原文)

HTTPcode触发报文
400INVALID_REQUEST请求体不是合法 JSON请求格式错误
400NO_LINKSlinks 全为空 / 全是空串请至少提交 1 条支付链接
400INVALID_LINK_COUNT链接数超 max_batch每次必须提交 1-200 条链接
400INVALID_PAYMENT_LINK含非 Midtrans 链接(Stripe / Nicepay / 其它域名都算)Gopay 通道只接受 Midtrans 链接(app.midtrans.com/snap/v4/redirection/...),当前链接识别为:upi
400DUPLICATE_LINK本批内链接重复存在重复的支付链接
400AT_COUNT_MISMATCHats 非空但与 links 数量不等AccessToken 行数必须与支付链接一一对应
400INVALID_IDEMPOTENCY_KEY幂等键缺失或长度非 8–80Idempotency-Key 长度必须为 8-80 个字符
400INVALID_CDK_COUNTcdk_codes 空或 >200每次必须提交 1-200 个卡密
400INVALID_CDK非 GOPAY- 格式 / 卡不存在 / 不是 Gopay 卡卡密格式不正确(Gopay 卡应为 GOPAY-XXXXX-XXXXX-XXXXX-XXXXX) · 卡密不存在或不是 Gopay 专用卡
400DUPLICATE_CDK本批内卡密重复卡密重复
400UNAVAILABLE_CDK卡已用尽 / 被禁用 / 已过期卡密不可用或次数不足
400CDK_USES_INSUFFICIENT卡内剩余次数 < 链接数卡密剩余次数不足
409DUPLICATE_LINK该链接历史上已被提交过(跨批次去重)存在已提交过的支付链接
409NO_ACCEPTING_SUPPLIERS当前无人可接单,或在途单已达接单容量当前没有可接单的供货商,请稍后再试
429—超 1800 次/分钟由网关返回,含 Retry-After
500INTERNAL_ERROR服务异常 / 平台单价未配置服务异常

★ 校验顺序(排查时据此定位卡在哪一步)

请求体结构 → links 去重/非空 → links 数量 → 链接白名单(必须 midtrans)
           → ats 数量 → Idempotency-Key 长度(网关层最前)
           → cdk_codes 数量 → 卡密格式 → 卡密重复
           → (之后)卡密存在性/通道归属 → 卡密可用性/次数 → 链接占用 → 派单

💡 注意与 UPI 的差异:幂等键在校验链最前端(网关直接判),所以同时写错链接和漏传幂等键时,Gopay 通道先报 INVALID_IDEMPOTENCY_KEY,而 UPI 通道先报 INVALID_PAYMENT_LINK。

G1.3 凭回执号查询(免登录)

GET /api/v1/public/gopay/orders/:receipt 无需鉴权 频率:1800 次/分钟

响应结构与 G1.2 完全一致(含 orders[] 与 link_expires_at),差异仅在于**不反映本次扣卡结果**:

字段提交响应回执查询
batch_no / receipt / orders[]✅✅
remaining_uses✅ 本次扣卡后余量⚠️ 恒为 0(无意义,别用)
balance / frozen_balance"0""0"
idempotent✅恒为 false

🔴 回执是通道绑定的。 拿 UPI 批次的回执来查会返回 404 ORDER_NOT_FOUND,反之亦然。两个通道的回执不能互换。

错误

HTTPcode触发
404ORDER_NOT_FOUND回执不存在,或该回执属于 UPI 批次

G2. 卡密格式(GOPAY-)

GOPAY-AAAAA-BBBBB-CCCCC-DDDDD
└──┬─┘ └──────┬───────────┘
   │          └─ 20 位 base32 主体(RFC4648 标准字母表 A–Z2–7),5-5-5-5 分组
   └─ 固定前缀 "GOPAY-",共 29 字符
项说明
总长度29 字符(GOPAY- 6 + 20 位主体 + 3 个连字符)
字符集大写字母 A–Z 与数字 2–7(不含 0、1、8、9)
大小写服务端统一转大写处理,小写输入也能识别;但请统一用大写
获取方式① 管理端「生成卡密」选 Gopay 类型;② 站长把已有 CDK 卡批量标记为 Gopay
能否合并❌ 不支持。Gopay 卡没有合并功能

🔴 前缀即通道。 GOPAY- 卡只能进 Gopay 通道,CDK- 卡只能进 UPI 通道,互相提交一律 400 INVALID_CDK。这是刻意设计的硬隔离,不是 bug。

G3. 链接格式与 15 分钟有效期(★ Gopay 最重要的一条)

G3.1 链接白名单

服务端正则:

^https?://app\.midtrans\.com/snap/v4/redirection/[0-9a-fA-F-]{8,}(?:[?#].*)?$
链接是否通过
https://app.midtrans.com/snap/v4/redirection/0f9dff28-55ed-4eba-b712-c2eb5a404c74✅
https://app.midtrans.com/snap/v4/redirection/<uuid>?lang=id✅(允许 ?/# 后缀)
https://payments.stripe.com/upi/instructions/xxx🚫 400 INVALID_PAYMENT_LINK
https://pay.nicepay.co.kr/v1/payment/xxx🚫 400 INVALID_PAYMENT_LINK
https://app.midtrans.com/snap/v1/xxx(非 v4/redirection)🚫 400 INVALID_PAYMENT_LINK
其它任意域名🚫 400 INVALID_PAYMENT_LINK

Gopay 没有二维码,只有链接。 不要试图把二维码内容(qr... 之类的字符串)当链接提交,会被判为 INVALID_PAYMENT_LINK。

G3.2 🔴 15 分钟有效期与到期自动判定

Midtrans 支付链接自生成起只有 15 分钟有效,过期后链接即失效——供货商再操作也没有意义,AT 也不会因为付款而失效。因此:

  • 提交响应里的 orders[].link_expires_at = created_at + 15 分钟,可直接用于倒计时;
  • 请务必在 15 分钟内完成支付,不要卡点提交;
  • 15 分钟到点后,系统会自动再探一次 AT 并给出结论(扫描周期 60 秒)。
订单创建(pending / claimed / processing)
   ↓ 等待 15 分钟(每 60 秒扫一轮在途 Gopay 单)
再探一次 AT(经站内中继访问 chatgpt.com,Chrome 指纹)
   ├─ AT 仍存活(HTTP 200/204)→ 判 failed,failure_reason 注明「链接已过期,AT 仍存活」
   ├─ AT 已失效(HTTP 401)    → 不动作 → 说明付款确实发生过,交由正常流程结算
   └─ 判定不了(403 / 超时)   → 不动作 → 下一轮再试,绝不误判
判定结果订单状态说明
AT 仍存活failed到点后 AT 还能用 = 没人真正付过款 → 判失败。failure_reason = Gopay 链接已过期(15 分钟),AT 仍存活,判定支付未到账
AT 已失效不变AT 失效是付款成功的信号(同 §3.1.3 / §5.1),交给人正常完成
判定不了不变403(Cloudflare 挑战 / 账号 review)、超时、网络异常都不算失败,只会下一轮重试
无 AT不变没有判据,不猜。这类单只能人工处理 —— 所以请务必传 ats

⚠️ 判失败 ≠ 退款。 15 分钟到点判失败属「未到账」情形,卡密次数不自动返还(与站内公告的未到账规则一致)。 若你确认该笔实际已到账(例如 AT 失效但状态没走到 completed),请凭 receipt 联系站长人工复核,不要重复提交同一链接(会被 409 DUPLICATE_LINK 拦住)。

G4. 错误码速查(Gopay 专属差异)

下表只列与 UPI 通道不同或 Gopay 特有的项;未列出的沿用 §7。

codeUPI 通道Gopay 通道
AT_REQUIRED有(未传 ats → 400)❌ 没有(不传也能过,但无 AT 无法判定,见 G3.2)
NO_LINKS—✅ Gopay 特有(links 全空)
UNAVAILABLE_CDK有有,但返回 400(UPI 是 409)
CDK_USES_INSUFFICIENT409400
NO_ACCEPTING_SUPPLIERS—(用 SUPPLIER_NOT_ACCEPTING)✅ 409,Gopay 特有
INVALID_PAYMENT_LINK只认 Stripe/Nicepay只认 Midtrans,报文里会回显识别结果
merged_into(字段)查卡响应有❌ 没有
link_expires_at(字段)❌ 没有✅ Gopay 专有
pay_method(字段)"upi""gopay"

⚠️ 同一份代码接两个通道时,务必按 `code` 分支处理,不要按 HTTP 状态码猜。 例如 CDK_USES_INSUFFICIENT 在 UPI 是 409、在 Gopay 是 400 —— 按状态码写重试逻辑会错。

G5. cURL 全流程

BASE=https://jmlscan.cloud-ip.cc
CARD=GOPAY-XXXXX-XXXXX-XXXXX-XXXXX
LINK=https://app.midtrans.com/snap/v4/redirection/0f9dff28-55ed-4eba-b712-c2eb5a404c74

# 0) 读实时参数(单价 / 单批上限)
curl -s $BASE/api/v1/public/scan/config

# 1) 查卡余量(确认是 Gopay 卡且次数够)
curl -s -X POST $BASE/api/v1/public/gopay/cdks/query \
  -H 'Content-Type: application/json' \
  -d "{\"cdk_codes\":[\"$CARD\"]}"

# 2) 提交(★ ats 与 links 一一对应)
curl -s -X POST $BASE/api/v1/public/gopay/orders \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: gopay-20261103-0001' \
  -d "{
    \"cdk_codes\": [\"$CARD\"],
    \"links\":     [\"$LINK\"],
    \"ats\":       [\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\"]
  }"

# 3) 凭回执查询(免鉴权,★ 必须 R 前缀)
curl -s $BASE/api/v1/public/gopay/orders/R20261103XXXXXXXXXXXXXX

G6. Python 示例(标准库,无第三方依赖)

import json, time, urllib.request, urllib.error

BASE = "https://jmlscan.cloud-ip.cc"

def call(method, path, body=None, headers=None):
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(BASE + path, data=data, method=method)
    req.add_header("Content-Type", "application/json")
    for k, v in (headers or {}).items():
        req.add_header(k, v)
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            return r.status, json.loads(r.read().decode())
    except urllib.error.HTTPError as e:
        return e.code, json.loads(e.read().decode())

# 1) 提交(每条链接配一个 AT)
def submit_gopay(cdk_codes, links, ats, idem_key):
    assert len(links) == len(ats), "links 与 ats 必须一一对应"
    status, data = call("POST", "/api/v1/public/gopay/orders",
                        {"cdk_codes": cdk_codes, "links": links, "ats": ats},
                        headers={"Idempotency-Key": idem_key})
    if status != 201:
        raise RuntimeError("提交失败 %s: %s" % (status, data))
    return data["data"]["receipt"]

# 2) 轮询(按回执号,免鉴权)
#    ★ 终止条件只有 completed / failed / cancelled
#    ★ failed 有两种来源:供货商判失败,或 15 分钟到点 AT 仍存活
def poll(receipt, interval=5, timeout=1200):
    deadline = time.time() + timeout
    while time.time() < deadline:
        status, data = call("GET", "/api/v1/public/gopay/orders/" + receipt)
        if status != 200:
            raise RuntimeError("查询失败 %s: %s" % (status, data))
        orders = data["data"]["orders"]
        if all(o["status"] in ("completed", "failed", "cancelled") for o in orders):
            return orders
        time.sleep(interval)
    raise TimeoutError("轮询超时")

if __name__ == "__main__":
    receipt = submit_gopay(
        ["GOPAY-XXXXX-XXXXX-XXXXX-XXXXX"],
        ["https://app.midtrans.com/snap/v4/redirection/0f9dff28-55ed-4eba-b712-c2eb5a404c74"],
        ["eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."],
        "gopay-20261103-0001",
    )
    print("receipt:", receipt)
    for o in poll(receipt):
        print(o["order_no"], o["status"], "pay_method=", o["pay_method"],
              "expires_at=", o["link_expires_at"], "reason=", o["failure_reason"])

G7. 两通道边界矩阵(防误用)

你做的事结果
GOPAY- 卡查 Gopay 通道✅ 正常
GOPAY- 卡查 UPI 通道(/scan/cdks/query)🚫 400 INVALID_CDK(格式不符)
CDK- 卡查 Gopay 通道🚫 400 INVALID_CDK(格式不符,提示应为 GOPAY-)
Midtrans 链接提交到 Gopay 通道✅ 正常
Midtrans 链接提交到 UPI 通道🚫 400 INVALID_PAYMENT_LINK
Stripe / Nicepay 链接提交到 Gopay 通道🚫 400 INVALID_PAYMENT_LINK(报文会写「当前链接识别为:upi」)
一批里混两种链接🚫 提交到哪个通道都会被拒(Gopay 侧直接 INVALID_PAYMENT_LINK)
用 UPI 回执查 Gopay 通道🚫 404 ORDER_NOT_FOUND
用 Gopay 回执查 UPI 通道🚫 404 ORDER_NOT_FOUND
同一个 Idempotency-Key 重试✅ 返回首次结果,idempotent: true,不重复扣卡
换 Idempotency-Key 重复提交同一链接🚫 409 DUPLICATE_LINK

G8. 联调 Checklist(Gopay 专用)

  • [ ] 确认拿到的是 GOPAY- 开头的卡(不是 CDK-)
  • [ ] 先跑一次 POST /public/gopay/cdks/query,确认 status=active 且 remaining_uses 够用
  • [ ] 链接确认是 https://app.midtrans.com/snap/v4/redirection/...(不是 Stripe / Nicepay)
  • [ ] ats 与 links 等长、一一对应,一条链接一个 AT
  • [ ] 每次提交带唯一 Idempotency-Key(8–80 字符),重试复用同一值
  • [ ] 保存返回的 receipt(R 前缀)与 link_expires_at
  • [ ] 在 link_expires_at 前完成支付(15 分钟窗口,别卡点)
  • [ ] 轮询间隔 ≥ 5 秒,终止条件为 completed / failed / cancelled
  • [ ] 读到 failed 时先看 failure_reason:是否含「链接已过期(15 分钟),AT 仍存活」
  • [ ] 同一链接不要换幂等键重复提交(会 409 DUPLICATE_LINK)

G9. 快速排障(Gopay)

现象原因与处理
400 INVALID_PAYMENT_LINK链接不是 Midtrans。看报文末句「当前链接识别为:xxx」——upi = 你拿 Stripe/Nicepay 链接来了;unknown = 域名/路径不匹配
400 INVALID_CDK(提示应为 GOPAY-)卡是 CDK- 的,或长度/字符集不对(必须是 29 字符、A–Z2–7)
400 INVALID_CDK(「卡密不存在或不是 Gopay 专用卡」)卡不存在,或这张卡还没被标记成 Gopay 类型 → 找站长确认
400 UNAVAILABLE_CDK / CDK_USES_INSUFFICIENT卡用尽/禁用/过期,或剩余次数少于链接数。先跑 G1.1 查余量
409 NO_ACCEPTING_SUPPLIERS当前没有在线接单人,或在途单已满。稍后重试,换幂等键重试前请先确认上一批是否已建单
409 DUPLICATE_LINK这条链接已经被提交过(含你自己上一批)。换链接,不要换幂等键硬重试
404 ORDER_NOT_FOUND(回执查不到)① 传了批次号 B... 或订单号 O...(必须 R...);② 这个回执属于 UPI 批次
订单 20 分钟了还停在 processing说明既没被供货商完成,也没走到期判定。多半是没传 AT(无判据不动作)→ 联系站长人工处理
订单变 failed,reason 含「AT 仍存活」到点后 AT 还能用 = 没付成功。这是预期行为,不是系统故障。请检查该笔是否真的扣款
订单变 failed,但客户说已付款凭 receipt 找站长复核(可查链接 + AT 轨迹)。不要重复提交同一链接

📎 本大类与上文的关系:Gopay 通道只覆盖卡密直连这一条路(不支持 API Key / 账号余额 / 批次列表)。需要账号体系、余额、批次管理、提现等能力,请继续用 UPI 通道(§2 / §3.1 / §3.2 / §3.5)。

*文档结束 | 正文 v3.1(对外版)| 2026-10-29 | 增补「Gopay 通道」大类 | 2026-11-03 | Base URL https://jmlscan.cloud-ip.cc*