NVer 卡密验证接口文档 v1.0

服务地址 https://ver.cupzz.cn · 所有接口使用 HTTPS + JSON,字符编码 UTF-8。

一、接入流程

  1. 登录管理后台,在「应用管理」新建一个应用,得到 askKey(应用标识)与 AppSecret(应用密钥)。
  2. 在「卡密管理」生成卡密,分发给用户。
  3. 客户端启动时调用 /api/v1/card/verify 验证卡密,拿到 carMyToken。
  4. 运行期间每分钟调用一次 /api/v1/card/heartbeat 维持在线,长时间不心跳令牌自动失效。

二、公共约定

2.1 请求头

Header必填说明
Content-Type是application/json;charset=utf-8
askKey是应用标识,后台「应用管理」中查看
time验签时必填毫秒时间戳,与服务端偏差不得超过 180 秒
sign验签时必填请求签名,32 位小写 MD5
nonceV3 必填随机串,6 分钟内不可重复
X-App-Key否应用密钥的另一种传法,见 2.2
X-Card-Token否卡密令牌的另一种传法,见 2.2
Authorization否Bearer <askKey 或 carMyToken>,见 2.2

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"

2.3 响应格式

所有接口返回统一信封,字段固定存在(值可能为 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。

3.1 V1:时间戳签名

sign = MD5( AppSecret + time )   // 32 位小写

示例(AppSecret = 4f3a9c11d8e2b706,time = 1767225600000):

MD5("4f3a9c11d8e2b7061767225600000") = 8f0c5d1e...

3.2 V2:参数签名

  1. 取全部请求参数(不含 sign、time、nonce)。
  2. 丢弃值为 null 或空字符串的项。
  3. 按参数名 ASCII 字典序升序排列。
  4. 拼接成 &k1=v1&k2=v2...,末尾追加 &AppSecret&time。
  5. 对整串做 MD5,取 32 位小写。

对象与数组参数先做紧凑 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(上面这串)

3.3 V3:V2 + 防重放

在 V2 拼接串末尾再追加 &nonce,其余一致。nonce 建议使用 UUID,6 分钟内重复使用会被拒绝。

四、传输加密(可选)

后台把加密模式设为 AES-256-ECB 后:

五、接口列表

5.1 验证卡密 · POST /api/v1/card/verify

兼容别名:/api/verifyCardV1、/api/verifyCardV2

参数类型必填说明
cardStrstring是卡密
macstring否设备唯一标识;不传则不做设备绑定限制

成功响应:

{
  "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。

5.2 心跳续期 · POST /api/v1/card/heartbeat

兼容别名:/api/heartbeat

参数类型必填说明
carMyTokenstring是验证接口返回的令牌

建议每 60 秒调用一次。心跳周期内无请求,令牌自动失效,客户端需重新验证。同一设备号只保留一个有效令牌,新登录会顶掉旧令牌。

{ "code": "200", "status": true,
  "data": { "verify": true, "expireAt": 1767226260, "heartbeat": 600 } }

5.3 解绑设备 · POST /api/v1/card/unbind

参数类型必填说明
cardStrstring是卡密
macstring否不传则解绑全部设备

需应用开启「自助解绑」,且未超过卡密的可解绑次数。

5.4 查询卡密 · POST /api/v1/card/info

参数 cardStr。返回卡密状态、到期时间、剩余次数、已绑定设备列表。

5.5 以卡续卡 · POST /api/v1/card/renew

参数类型必填说明
cardStrstring是被续期的目标卡密
sourceCardstring是作为续费来源的未使用时长卡

5.6 退出登录 · POST /api/v1/card/logout

参数 carMyToken,立即释放该设备的令牌。

5.7 应用信息 · GET /api/v1/app/info

返回应用名称、公告 notice、版本号、更新地址、强制更新标记、心跳周期等。

5.8 远程变量 · GET /api/v1/app/vars

返回后台配置的键值对,可用于远程下发接口地址、开关、提示文案等。

{ "code": "200", "data": { "api_endpoint": "https://api.example.com", "enable_x": "1" } }

5.9 卡密状态探测 · POST /api/v1/card/state

只读探测,不会激活、不会绑定设备、不会扣减次数。适合在登录框输入卡密后即时校验。

参数类型必填说明
cardStrstring二选一卡密字符串
carMyTokenstring二选一已登录会话令牌
{ "verify": true, "state": 2, "stateText": "已激活", "exTime": "2026-12-01 00:00:00",
  "deviceCount": 1, "maxDevices": 2, "unbindCount": 0, "maxUnbind": 3, "serverTime": 1793475695 }

别名 /api/cardState。

5.10 卡密登录 · POST /api/v1/card/login

语义化的验证入口,规则与 /api/v1/card/verify 完全一致:首个请求激活卡密、次数卡扣一次、按上限绑定设备,返回 carMyToken。

参数 cardStr(或 card)、mac。别名 /api/cardLogin。

5.11 设备列表 · POST /api/v1/card/devices

参数 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。

5.12 设备下线 · POST /api/v1/card/kick

参数 targetMac(或 mac)以及 carMyToken / cardStr。不消耗解绑次数,用于挤掉异常设备。要求应用开启自助解绑(unbindMode=1)。

{ "cardStr": "XXXXXXXX", "kicked": "DEV-A", "remain": 1 }

别名 /api/kickDevice。

5.13 卡密使用记录 · POST /api/v1/card/logs

参数 carMyToken 或 cardStr,可选 page(默认 1)、size(默认 20,上限 100)。返回验证、心跳、解绑、续费等动作轨迹。

别名 /api/cardLogs。

5.14 公告 · POST /api/v1/app/notice

返回后台配置的公告正文 notice 与当前版本号。可用于启动时弹窗。

别名 /api/getNotice。

5.15 更新检查 · POST /api/v1/app/version

参数类型必填说明
versionstring否客户端当前版本,如 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。

5.16 卡型列表 · POST /api/v1/app/types

返回后台配置的卡密模板(商品目录):id / name / kind / kindText / duration / quota / price。可直接用于商城页展示。别名 /api/getCardTypes。

5.17 单个变量 · POST /api/v1/app/var

参数 key。只取一个变量,避免拉取全量键值对。

{ "key": "server", "value": "https://game.example.com", "exists": true }

别名 /api/getVar。

5.18 服务器时间 · POST /api/v1/sys/time

返回 serverTime(秒)、millis(毫秒)、formatted、timezone。用于校正客户端时钟,避免签名因本机时间偏移被拒。别名 /api/time。

5.19 在线统计 · POST /api/v1/sys/online

{ "onlineDevices": 12, "activeCards": 30, "totalCards": 100,
  "verify24h": 240, "reject24h": 6, "heartbeat": 600, "serverTime": 1793475695 }

仅统计当前应用。onlineDevices 按心跳窗口计算。别名 /api/online。

5.20 兼容别名一览

以下路径与上表接口一一对应,参数与返回完全一致,方便直接套用旧版客户端代码。

别名等价接口
/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变量与系统族

六、调用示例

6.1 Go

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)
}

6.2 Python

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())

6.3 C# (Unity / WinForms)

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 · 本站接口仅用于自有软件授权校验。