API 接口文档

Card Consume API · v1 · HMAC

📘
基础信息
v1.0 · 2026-09-16

发卡宝 API v1 只有两个接口:幂等核销(consume)对账查询(query)。核销为不可自动回退的资产消耗,跨系统一致性靠「幂等单号 + 可查询 + 接入方本地订单」保证,可彻底解决“卡已烧、权益未到账”的掉单问题。

Base URLhttps://fakabao.net
统一入口POST /api/index.php
GET /api/index.php
独立入口(等价)POST /api/v1/consume.php
GET /api/v1/query.php
鉴权HMAC-SHA256 三签名头
频率限制60 次/分钟/Key · 120 次/分钟/IP
Key / Secret用户中心 → API 设置(重置 24h 一次)
老协议已下线(2026-09-16):body 传 apikeymethod=check/setUsed 的请求一律返回 410 GONE,未升级的集成请尽快切换到本页协议。
🔐
HMAC 签名鉴权
每个请求必带三个 Header
Header说明
X-API-Key商户 API Key(身份标识,重置后同步变化)
X-TimestampUnix 秒级时间戳,与服务端偏差 ≤ ±300 秒;只取一次,签名与发送必须同值
X-Sign按下述规则计算的签名(小写 hex),服务端常量时间比较
签名规则
POST: Sign = HMAC_SHA256( Secret, Timestamp + "." + 请求体原文(raw body) )
GET : Sign = HMAC_SHA256( Secret, Timestamp + "." + URL查询串原文(QUERY_STRING) )

要点:
1. POST 参与签名的是"实际发出的字节"——先 json_encode 得到 $body,签名用 $body,发送也用 $body,中间不得重排/格式化;
2. GET 的查询串自行拼接(如 http_build_query 的结果),签什么就发什么,避免 curl/代理二次编码改变顺序;
3. 签名失败返回 401 INVALID_SIGN,可用示例 Demo 对照排查。
🧾
幂等与对账原则(必读)
规则说明
clientTxnId 全局唯一由接入方生成,并先写入你自己的订单表再发请求;建议格式 商户前缀_时间_随机(勿嵌卡号,单号会进日志)
重试必须复用同一个 clientTxnId超时/断网属于“结果未知”:拿原单号重发 consume(命中重放语义)或调 query 确认。禁止换新单号重试同一张卡,否则失去幂等保护
重放返回同一结果同单号重复请求返回 replay:true 且 pid 与首次一致,不会二次扣卡;同单号换卡号请求被拒(INVALID_PARAM)
一卡一销平台唯一台账;卡被别的单烧掉后,你的新单收到 ALREADY_USED
接入方本地订单三态推荐 pending / success / failed:发放权益与订单翻 success 在同一个本地事务内原子完成(只允许翻转一次);“结果未知”保持 pending,由你的定时任务 query 收敛
补发唯一依据query 返回 usedTxn == 自己的单号 才可补发;不等 = 卡经其他渠道消耗,严禁发放,转人工
🚥
错误码总表
统一响应包 {code,data} / {code,err,msg}
errHTTP含义接入方动作
NOT_FOUND404卡密不存在终态失败,提示换卡
ALREADY_USED409卡已被核销(含被别的单烧掉)终态失败,提示换卡
REFUNDED409该核销单已被客服退款,拒绝重放终态失败
INVALID_PARAM400参数缺失/非法,或单号与卡号不符修正后重试(未烧卡,安全)
INVALID_AUTH / INVALID_KEY / INVALID_SIGN / INVALID_TIMESTAMP401鉴权头缺失 / Key不存在 / 签名错 / 时间偏差>5min检查 Key、Secret、服务器时钟;校时后用原单号重试
ACCOUNT_DISABLED / API_DISABLED / IP_DENIED403账号或 API 被禁用 / IP 不在白名单联系管理员
RATE_LIMITED429触发限流退避后原单号重试
BUSY / 5xx / 超时 / 非JSON响应500结果未知(不是失败!)订单保持 pending,返回用户“处理中”;稍后 query 对账收敛,不得判死
GONE410调用了已下线的老协议升级到 v1
POST
1. 卡密核销 consume(唯一写接口)

行锁原子操作:校验 + 登记台账 + 标记已用一次完成,是整条链路唯一需要同步等待的调用(建议连接 3s / 总 6s 超时)。幂等:同 clientTxnId 重放返回相同结果。

请求参数(JSON Body)
参数必填说明
method是*固定 "consume"(*独立入口 /api/v1/consume.php 无需 method 字段)
cardNo卡号(本平台 17 位:ABCDE-FGHIJ-KLMNO)
clientTxnId接入方订单号,字符集 A-Za-z0-9_.:-,长度 1–64,全局唯一,重试复用
POST /api/index.php
Content-Type: application/json
X-API-Key: 9ffa60a9c4aa4eb512312d680e2974dc
X-Timestamp: 1789502400
X-Sign: hmac_sha256(secret, "1789502400." + 请求体原文)

{"method":"consume","cardNo":"BQ7WK-T9U4W-NGRTJ","clientTxnId":"m51_20260916_3f9a27de01c8b4f5"}
响应示例 / 处理
首次核销成功{"code":1,"data":{"pid":38,"replay":false}}
同单号重放{"code":1,"data":{"pid":38,"replay":true}}(pid 与首次一致)
pid 为发卡宝商品ID(卡种),到账换算由接入方映射(例:38=30天、39=90天、40=永久)。收到映射外的未知 pid:卡已核销,绝不可判死,转人工核对发放。
GET
2. 对账查询 query(只读)

查询核销台账,供超时收敛、定时对账、人工补单前核对。先按 clientTxnId 查,未命中自动回落按 cardNo 查(并可追溯卡表兜底),单参数也可查,两个都带最稳。

参数(querystring)必填说明
clientTxnId二选一核销时携带的接入方订单号
cardNo二选一卡号
GET /api/index.php?clientTxnId=m51_20260916_3f9a27de01c8b4f5
(签名原文 = Timestamp + "." + "clientTxnId=m51_20260916_3f9a27de01c8b4f5")

已核销:{"code":1,"data":{"used":true, "pid":38,"usedTxn":"m51_20260916_3f9a27de01c8b4f5","usedAt":"2026-09-16 03:21:56"}}
未核销:{"code":1,"data":{"used":false,"pid":0, "usedTxn":"","usedAt":""}}
补发唯一依据:used==true && usedTxn==本单 → 允许执行本地发放(只发一次);usedTxn≠本单 → 他渠道消耗,禁止发放转人工;used==false 且订单已超时(如>30分钟)→ 可安全置失败,允许用户复用原单号重新提交。
🔁
推荐充值流程

平台不提供自动回滚(卡状态单调向前)。“本地没办成”的兜底不是退卡,而是本地重试发放

1 本地落单 pending(生成 clientTxnId,落库成功后才发请求)
2 POST consume(唯一同步阻塞,1 次 HTTP)
   ├─ code=1 → 3
   ├─ NOT_FOUND / ALREADY_USED / REFUNDED → 订单 failed,提示换卡
   └─ 超时 / 429 / BUSY / 5xx / 鉴权错 → 保持 pending,返回"处理中"
3 本地事务:按 pid 发放权益 + 订单翻 success(同一事务,原子只翻转一次)
4 定时任务(每分钟):扫 pending 超时单 → GET query
   ├─ used=false 超30分钟 → failed(用户可复用原单号重提)
   ├─ used=true  usedTxn==本单 → 补执行第3步(自愈)
   └─ used=true  usedTxn!=本单 → 转人工,禁止发放
5 客服通道:误核销人工 refund,卡恢复可用;你的历史单号再重放会收到 REFUNDED
🐘
PHP 调用 Demo(原生 cURL + HMAC)
零依赖 · PHP ≥ 7.4
<?php
class FakabaoAPI
{
    public function __construct(
        private string $baseUrl,    // 例 https://fakabao.net
        private string $apiKey,
        private string $secret,
        private int $connTimeout = 3,
        private int $timeout = 6
    ) {}

    /** 核销:['ok'=>bool,'pid'=>int,'replay'=>bool] 或 ['ok'=>false,'err'=>..,'msg'=>..] */
    public function consume(string $cardNo, string $clientTxnId): array
    {
        $body = json_encode(
            ['method' => 'consume', 'cardNo' => $cardNo, 'clientTxnId' => $clientTxnId],
            JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
        );
        return $this->request('POST', '/api/index.php', $body);
    }

    /** 对账查询:主查单号,可带卡号回落 */
    public function query(string $clientTxnId, string $cardNo = ''): array
    {
        $qs = http_build_query(array_filter(
            ['clientTxnId' => $clientTxnId, 'cardNo' => $cardNo],
            function ($v) { return $v !== '' && $v !== null; }
        ));
        // GET 签名原文 = 实际发出的 QUERY_STRING,自行拼接保证一致
        return $this->request('GET', '/api/index.php?' . $qs, null, $qs);
    }

    private function request(string $method, string $path, ?string $body, ?string $signBase = null): array
    {
        $ts = (string)time();                    // 只取一次,签名/发送同值
        $signBase = $body ?? $signBase;
        $ch = curl_init($this->baseUrl . $path);
        $headers = [
            'X-API-Key: ' . $this->apiKey,
            'X-Timestamp: ' . $ts,
            'X-Sign: ' . hash_hmac('sha256', $ts . '.' . $signBase, $this->secret),
        ];
        if ($method === 'POST') {
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
            $headers[] = 'Content-Type: application/json';
        }
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => $this->connTimeout,
            CURLOPT_TIMEOUT        => $this->timeout,
            CURLOPT_SSL_VERIFYPEER => true,      // 生产严禁关闭
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_HTTPHEADER     => $headers,
        ]);
        $raw   = curl_exec($ch);
        $errno = curl_errno($ch);
        curl_close($ch);

        if ($errno !== 0 || $raw === false || $raw === '') {
            return ['ok' => false, 'err' => 'TIMEOUT',
                    'msg' => '结果未知:订单保持pending,稍后用同一clientTxnId调query收敛'];
        }
        $j = json_decode($raw, true);
        if (!is_array($j)) {
            return ['ok' => false, 'err' => 'BAD_RESPONSE', 'msg' => '响应不可解析,按未知结果处理'];
        }
        if ((int)($j['code'] ?? 0) === 1) {
            $d = $j['data'];
            if (isset($d['used'])) {
                return ['ok' => true, 'used' => (bool)$d['used'], 'pid' => (int)$d['pid'],
                        'usedTxn' => (string)$d['usedTxn'], 'usedAt' => (string)$d['usedAt']];
            }
            return ['ok' => true, 'pid' => (int)$d['pid'], 'replay' => (bool)$d['replay']];
        }
        return ['ok' => false, 'err' => (string)($j['err'] ?? 'BUSY'), 'msg' => (string)($j['msg'] ?? '')];
    }
}

// ============ 使用 ============
$api  = new FakabaoAPI('https://fakabao.net', '你的KEY', '你的SECRET');
$card = 'ABCDE-FGHIJ-KLMNO';
$txn  = 'm51_' . date('YmdHis') . '_' . bin2hex(random_bytes(8));
// TODO 先把 $txn 以 pending 写入你自己的订单表,再发起核销

$r = $api->consume($card, $txn);
if ($r['ok']) {
    // TODO 本地事务:按 $r['pid'] 发放权益 + 订单翻 success(同一事务,只翻转一次)
} elseif (in_array($r['err'], ['NOT_FOUND', 'ALREADY_USED', 'REFUNDED'], true)) {
    // 终态失败:订单置 failed,提示换卡
} else {
    // TIMEOUT/BUSY/RATE_LIMITED/BAD_RESPONSE/鉴权错:
    // 保持 pending,前端文案"处理中,1分钟后自动到账",定时任务调 query 收敛
}
?>
📦
官方 PHP SDK
零依赖单文件 · PHP ≥ 8.0

已封装签名、超时语义、错误分类与补发核对,含充值闭环 + reconcile 模板。

⬇ 下载 PHP SDK fakabao-sdk-php.zip · 约 6 KB · 内含 FakabaoClient.php / example.php / README.md

require 'FakabaoClient.php';
$c = new FakabaoClient('https://fakabao.net', 'API_KEY', 'API_SECRET');

try {
    $r = $c->consume($cardNo, $txn);            // ['pid'=>38,'replay'=>false]
} catch (FakabaoException $e) {
    if ($e->isRetryable()) { /* 保持pending,query收敛 */ }
    /* 否则 NOT_FOUND / ALREADY_USED / REFUNDED 终态失败 */
}

$q = $c->query($cardNo, $txn);                   // ['used','pid','usedTxn','usedAt']
if ($c->isUsedByMe($cardNo, $txn)) { /* 唯一允许自动补发的情况 */ }
常见问题
问题回答
一直 401 INVALID_SIGN?三点排查:① POST 签的是请求体原文(encode 后不许再改动);② GET 签的是 QUERY_STRING 原文(自行拼接,别让库改写);③ Secret 重置后未同步
INVALID_TIMESTAMP?时钟偏差超5分钟,校准 NTP 后用原单号重试即可,不会重复扣卡
consume 超时,卡到底烧没烧?未知,别猜。等1分钟拿原单号 query:used=false → 可安全重发;used=true 且 usedTxn=自己 → 直接补发放。也可用同一单号再 consume,命中重放返回相同结果
能回滚已核销的卡吗?API 不提供回滚(防“退款重烧”双花)。误核销走客服人工 refund:卡恢复可用,但旧单号重放会收到 REFUNDED
用户重复提交同一张卡?你侧落订单表并复用同一 clientTxnId;即便发了不同单号,平台一卡一销兜底,败者收 ALREADY_USED
还在用老 apikey/setUsed 代码?老协议已 410 下线,请尽快升级;怀疑泄漏的旧 Key 请在商户中心重置作废