服务地址 https://ver.cupzz.cn · 所有接口使用 HTTPS + JSON,字符编码 UTF-8。
askKey(应用标识)与 AppSecret(应用密钥)。/api/v1/card/verify 验证卡密,拿到 carMyToken。/api/v1/card/heartbeat 维持在线,长时间不心跳令牌自动失效。| Header | 必填 | 说明 |
|---|---|---|
Content-Type | 是 | application/json;charset=utf-8 |
askKey | 是 | 应用标识,后台「应用管理」中查看 |
time | 验签时必填 | 毫秒时间戳,与服务端偏差不得超过 180 秒 |
sign | 验签时必填 | 请求签名,32 位小写 MD5 |
nonce | V3 必填 | 随机串,6 分钟内不可重复 |
X-App-Key | 否 | 应用密钥的另一种传法,见 2.2 |
X-Card-Token | 否 | 卡密令牌的另一种传法,见 2.2 |
Authorization | 否 | Bearer <askKey 或 carMyToken>,见 2.2 |
应用密钥与卡密令牌都可以放在请求头、请求体/查询参数或 Authorization 里,服务端按固定优先级取值,命中即停。客户端可自由选择,推荐统一走 header。
| 令牌 | 取值顺序(先命中先用) |
|---|---|
应用密钥askKey |
header askKey → header appKey → header X-App-Key → header X-App-Token → header Api-Key →
参数 askKey → 参数 appKey → 参数 appToken →
Authorization: Bearer(值非 JWT 时)→
header token / header X-Token(值非 JWT 时)→
参数 token(值非 JWT 时)
|
卡密令牌carMyToken |
参数 carMyToken → 参数 cardToken →
参数 token(值是 JWT 时)→
header X-Card-Token →
header token / header X-Token(值是 JWT 时)→
Authorization: Bearer(值是 JWT 时)
|
卡密令牌是三段式 JWT(形如 aaa.bbb.ccc),应用密钥是 ak_ 开头的不透明串,服务端靠这个形状区分两者,因此同名的 token 项也能自动路由到正确的令牌。
Authorization 一次只能带一个令牌。需要同时传应用密钥与卡密令牌时,用 X-App-Key + X-Card-Token 两个头,或都放请求体。
签名只覆盖请求体/查询参数中的字段,请求头不参与签名,所以把参数改到 header 里不会影响 sign 的计算结果。反之,若你走请求体,签名必须包含这些字段。
/api/v1/card/verify 还额外接受 X-Card-Key(卡密字符串)与 X-Device-Id(设备号)两个头,配合 X-App-Key 就能做到「零请求体」调用。
示例一 · 全部走 header
POST /api/v1/card/heartbeat HTTP/1.1
Host: ver.cupzz.cn
X-App-Key: ak_7F3KQ9ZMB2XTPL4W
X-Card-Token: eyJhbGciOiJIUzI1NiJ9.eyJjaWQiOjEyfQ.9Fk2sT
Content-Type: application/json
{}
示例二 · 全部走请求体
curl -s https://ver.cupzz.cn/api/v1/card/heartbeat \
-H 'Content-Type: application/json' \
-d '{"askKey":"ak_7F3KQ9ZMB2XTPL4W","carMyToken":"eyJhbGciOiJIUzI1NiJ9.eyJjaWQiOjEyfQ.9Fk2sT"}'
示例三 · 只有应用密钥时用 Authorization
curl -s "https://ver.cupzz.cn/api/v1/sys/time" -H 'Authorization: Bearer ak_7F3KQ9ZMB2XTPL4W'
示例四 · 查询参数
curl -s "https://ver.cupzz.cn/api/v1/app/vars?askKey=ak_7F3KQ9ZMB2XTPL4W"
所有接口返回统一信封,字段固定存在(值可能为 null):
{
"code": "200",
"message": "",
"status": true,
"data": { },
"pc": null,
"time": null,
"sign": null,
"nonce": null,
"reqSign": null
}
| code | 含义 |
|---|---|
"200" | 成功,业务结果在 data 中 |
"400" | 参数缺失或格式错误 |
"403" | 无权限:askKey 无效、验签失败、令牌过期 |
"404" | 资源不存在 |
"429" | 请求过于频繁 |
"500" | 业务失败:卡密不存在、已过期、已封禁、次数用尽等 |
签名模式在后台应用设置中切换,共四档:0 不验签 / 1 V1 / 2 V2 / 3 V3。
sign = MD5( AppSecret + time ) // 32 位小写
示例(AppSecret = 4f3a9c11d8e2b706,time = 1767225600000):
MD5("4f3a9c11d8e2b7061767225600000") = 8f0c5d1e...
sign、time、nonce)。null 或空字符串的项。&k1=v1&k2=v2...,末尾追加 &AppSecret&time。对象与数组参数先做紧凑 JSON 序列化(UTF-8、无多余空格)再参与拼接。
参数: cardStr=ABCD1234 mac=E4-54-E8-11-22-33
time: 1767225600000
拼接: &cardStr=ABCD1234&mac=E4-54-E8-11-22-33&4f3a9c11d8e2b706&1767225600000
sign: md5(上面这串)
在 V2 拼接串末尾再追加 &nonce,其余一致。nonce 建议使用 UUID,6 分钟内重复使用会被拒绝。
后台把加密模式设为 AES-256-ECB 后:
askKey / time / sign / nonce 保持明文(半加密)。Content-Type: text/plain。AppSecret 右补字符 0 到 32 字节。兼容别名:/api/verifyCardV1、/api/verifyCardV2
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
cardStr | string | 是 | 卡密 |
mac | string | 否 | 设备唯一标识;不传则不做设备绑定限制 |
成功响应:
{
"code": "200",
"status": true,
"data": {
"verify": true,
"cardStr": "VIP-8KQ2M4XW7B3N",
"type": 1,
"typeName": "月卡",
"exTime": "2026-12-01 00:00:00",
"remark": "渠道A",
"carMyToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"mac": "E4-54-E8-11-22-33",
"heartbeat": 600,
"serverTime": 1767225600
}
}
type:1 时长卡、2 次数卡、3 永久卡。次数卡额外返回 quota / usedQuota / remainQuota。
兼容别名:/api/heartbeat
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
carMyToken | string | 是 | 验证接口返回的令牌 |
建议每 60 秒调用一次。心跳周期内无请求,令牌自动失效,客户端需重新验证。同一设备号只保留一个有效令牌,新登录会顶掉旧令牌。
{ "code": "200", "status": true,
"data": { "verify": true, "expireAt": 1767226260, "heartbeat": 600 } }
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
cardStr | string | 是 | 卡密 |
mac | string | 否 | 不传则解绑全部设备 |
需应用开启「自助解绑」,且未超过卡密的可解绑次数。
参数 cardStr。返回卡密状态、到期时间、剩余次数、已绑定设备列表。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
cardStr | string | 是 | 被续期的目标卡密 |
sourceCard | string | 是 | 作为续费来源的未使用时长卡 |
参数 carMyToken,立即释放该设备的令牌。
返回应用名称、公告 notice、版本号、更新地址、强制更新标记、心跳周期等。
返回后台配置的键值对,可用于远程下发接口地址、开关、提示文案等。
{ "code": "200", "data": { "api_endpoint": "https://api.example.com", "enable_x": "1" } }
只读探测,不会激活、不会绑定设备、不会扣减次数。适合在登录框输入卡密后即时校验。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
cardStr | string | 二选一 | 卡密字符串 |
carMyToken | string | 二选一 | 已登录会话令牌 |
{ "verify": true, "state": 2, "stateText": "已激活", "exTime": "2026-12-01 00:00:00",
"deviceCount": 1, "maxDevices": 2, "unbindCount": 0, "maxUnbind": 3, "serverTime": 1793475695 }
别名 /api/cardState。
语义化的验证入口,规则与 /api/v1/card/verify 完全一致:首个请求激活卡密、次数卡扣一次、按上限绑定设备,返回 carMyToken。
参数 cardStr(或 card)、mac。别名 /api/cardLogin。
参数 carMyToken 或 cardStr。返回该卡已绑定的全部设备,以及每台设备是否在线(按心跳窗口判定)。
{ "cardStr": "XXXXXXXX", "count": 2, "maxDevices": 2, "devices": [
{ "mac": "DEV-A", "ip": "1.2.3.4", "firstBindAt": "...", "lastSeenAt": "...", "online": true, "current": true }
] }
别名 /api/getDevices。
参数 targetMac(或 mac)以及 carMyToken / cardStr。不消耗解绑次数,用于挤掉异常设备。要求应用开启自助解绑(unbindMode=1)。
{ "cardStr": "XXXXXXXX", "kicked": "DEV-A", "remain": 1 }
别名 /api/kickDevice。
参数 carMyToken 或 cardStr,可选 page(默认 1)、size(默认 20,上限 100)。返回验证、心跳、解绑、续费等动作轨迹。
别名 /api/cardLogs。
返回后台配置的公告正文 notice 与当前版本号。可用于启动时弹窗。
别名 /api/getNotice。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
version | string | 否 | 客户端当前版本,如 1.0.0 |
{ "version": "1.2.0", "currentVersion": "1.0.0", "needUpdate": true, "forceUpdate": true,
"url": "https://example.com/app.zip", "note": "修复若干问题", "notice": "..." }
版本号按点分段逐段比较,前缀 v 会被忽略。别名 /api/checkUpdate。
返回后台配置的卡密模板(商品目录):id / name / kind / kindText / duration / quota / price。可直接用于商城页展示。别名 /api/getCardTypes。
参数 key。只取一个变量,避免拉取全量键值对。
{ "key": "server", "value": "https://game.example.com", "exists": true }
别名 /api/getVar。
返回 serverTime(秒)、millis(毫秒)、formatted、timezone。用于校正客户端时钟,避免签名因本机时间偏移被拒。别名 /api/time。
{ "onlineDevices": 12, "activeCards": 30, "totalCards": 100,
"verify24h": 240, "reject24h": 6, "heartbeat": 600, "serverTime": 1793475695 }
仅统计当前应用。onlineDevices 按心跳窗口计算。别名 /api/online。
以下路径与上表接口一一对应,参数与返回完全一致,方便直接套用旧版客户端代码。
| 别名 | 等价接口 |
|---|---|
/api/verifyCardV1 · /api/verifyCardV2 · /api/verifyCardV3 · /api/cardLogin | /api/v1/card/verify |
/api/heartbeat | /api/v1/card/heartbeat |
/api/unbind | /api/v1/card/unbind |
/api/logout | /api/v1/card/logout |
/api/cardInfo · /api/cardState | /api/v1/card/info · /api/v1/card/state |
/api/renew · /api/renewByCard | /api/v1/card/renew |
/api/getDevices · /api/kickDevice · /api/cardLogs | 设备与记录接口 |
/api/appInfo · /api/getNotice · /api/checkUpdate · /api/getCardTypes | 应用信息族 |
/api/getVars · /api/getVar · /api/time · /api/online | 变量与系统族 |
package main
import (
"bytes"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"time"
)
const (
baseURL = "https://ver.cupzz.cn"
askKey = "ak_你的应用标识"
appSecret = "你的应用密钥"
)
func sign(params map[string]string, ts string) string {
keys := make([]string, 0, len(params))
for k := range params {
if k == "sign" || k == "time" || k == "nonce" {
continue
}
if params[k] == "" {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
var sb strings.Builder
for _, k := range keys {
sb.WriteString("&")
sb.WriteString(k)
sb.WriteString("=")
sb.WriteString(params[k])
}
sb.WriteString("&")
sb.WriteString(appSecret)
sb.WriteString("&")
sb.WriteString(ts)
sum := md5.Sum([]byte(sb.String()))
return hex.EncodeToString(sum[:])
}
func main() {
params := map[string]string{"cardStr": "你的卡密", "mac": "DEVICE-001"}
ts := strconv.FormatInt(time.Now().UnixMilli(), 10)
body, _ := json.Marshal(params)
req, _ := http.NewRequest("POST", baseURL+"/api/v1/card/verify", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json;charset=utf-8")
req.Header.Set("askKey", askKey)
req.Header.Set("time", ts)
req.Header.Set("sign", sign(params, ts))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var out struct {
Code string `json:"code"`
Message string `json:"message"`
Data struct {
Verify bool `json:"verify"`
ExTime string `json:"exTime"`
CarMyToken string `json:"carMyToken"`
} `json:"data"`
}
json.NewDecoder(res.Body).Decode(&out)
fmt.Printf("code=%s msg=%s token=%s\n", out.Code, out.Message, out.Data.CarMyToken)
}
import hashlib, json, time, requests
BASE = "https://ver.cupzz.cn"
ASK_KEY = "ak_你的应用标识"
SECRET = "你的应用密钥"
def sign(params, ts):
items = [(k, v) for k, v in params.items()
if k not in ("sign", "time", "nonce") and v not in (None, "")]
items.sort(key=lambda kv: kv[0])
raw = "".join(f"&{k}={v}" for k, v in items) + f"&{SECRET}&{ts}"
return hashlib.md5(raw.encode()).hexdigest()
params = {"cardStr": "你的卡密", "mac": "DEVICE-001"}
ts = str(int(time.time() * 1000))
headers = {
"Content-Type": "application/json;charset=utf-8",
"askKey": ASK_KEY,
"time": ts,
"sign": sign(params, ts),
}
r = requests.post(BASE + "/api/v1/card/verify", json=params, headers=headers, timeout=10)
print(r.json())
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
static string Sign(Dictionary<string, string> p, string ts, string secret)
{
var keys = new List<string>();
foreach (var kv in p)
if (kv.Key != "sign" && kv.Key != "time" && kv.Key != "nonce" && !string.IsNullOrEmpty(kv.Value))
keys.Add(kv.Key);
keys.Sort(StringComparer.Ordinal);
var sb = new StringBuilder();
foreach (var k in keys) sb.Append("&").Append(k).Append("=").Append(p[k]);
sb.Append("&").Append(secret).Append("&").Append(ts);
using (var md5 = MD5.Create())
{
var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(sb.ToString()));
var sb2 = new StringBuilder();
foreach (var b in hash) sb2.Append(b.ToString("x2"));
return sb2.ToString();
}
}
| 提示 | 原因与处理 |
|---|---|
| 缺少 askKey 应用标识 | 请求头未带 askKey |
| 应用不存在或已被删除 | askKey 拼错,或应用在后台被删除 |
| 应用已被禁用 | 后台把应用状态改成了停用 |
| 缺少验签参数 time/sign | 应用开启了验签,但请求头缺字段 |
| 请求时间戳已过期 | 本机时钟不准,或与服务端时差超 180 秒 |
| nonce 已被使用 | V3 模式重复用了同一个随机串 |
| 验签失败 | 签名拼接顺序、空值过滤或密钥不一致 |
| 卡密不存在或不属于当前应用 | 卡密是在别的应用下生成的 |
| 卡密已在其他设备使用 | 达到设备上限,先在后台或客户端解绑 |
| 心跳超时,请重新验证卡密 | 超过 heartbeat_ttl 秒没有心跳 |
| 请求过于频繁 | 触发限流,降低调用频率 |
NVer v1.0 · 本站接口仅用于自有软件授权校验。