Appearance
开放平台鉴权
商户账号同步与客服账号同步均使用相同的 HMAC 鉴权规则。开放接口均为服务端接口:请只在后端保存密钥并发起请求,不要在浏览器、App 或小程序中暴露密钥。
完成鉴权后,按业务需要调用商户账号同步或客服账号同步;两类接口都支持同步后查询。
启用同步并获取密钥
使用总后台管理员账号进入 系统设置 → 开放平台:启用“商户账号同步”,生成并保存“开放接口密钥”。密钥默认隐藏,可显示或复制。

开放接口密钥仅用于 /open/* 接口的 HMAC 签名,不能替代商户 API 密钥、访客 Token 或客服 Token。重新生成密钥会立即使所有使用旧密钥的调用失效,第三方服务端必须同步更新配置。
请求头与签名原文
所有请求使用 JSON 请求体,并携带以下请求头:
http
Content-Type: application/json
X-99KF-Timestamp: <当前 Unix 秒级时间戳>
X-99KF-Signature: <小写十六进制 HMAC-SHA256 签名>服务端只接受与当前时间相差不超过 60 秒的时间戳。签名原文按以下四行拼接,其中 PATH_AND_QUERY 是请求路径与原始查询串;无查询串时仅为路径。
text
METHOD
PATH_AND_QUERY
TIMESTAMP
SHA256(RAW_BODY)计算方式:
text
signature = hex(HMAC-SHA256(open_api_secret, canonical_request))例如,请求 POST /open/business/sync 时,签名原文为:
text
POST
/open/business/sync
1735689600
<请求 JSON 原文的 SHA256 十六进制摘要>RAW_BODY 必须是最终发送的原始 JSON 字节;签名前后不要重新格式化、补空格或调整字段顺序。接口不使用 nonce,时间戳仅用于限制过期请求,因此调用方应始终通过 HTTPS 直连目标服务端。
签名示例
PHP
以下示例使用 PHP 内置哈希函数和 cURL:
php
<?php
$baseUrl = 'https://99kf.example.com';
$secret = getenv('OPEN_API_SECRET');
$path = '/open/business/sync';
$payload = [
'bid' => 'merchant_10001',
'account_state' => 'normal',
'username' => 'merchant_10001',
'nickname' => '示例商户',
'password' => 'ChangeMe_2026',
];
// 该字符串就是最终发送的请求体,签名与 cURL 必须共用它。
$rawBody = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
$timestamp = (string) time();
$canonical = "POST\n{$path}\n{$timestamp}\n" . hash('sha256', $rawBody);
$signature = hash_hmac('sha256', $canonical, $secret);
$curl = curl_init($baseUrl . $path);
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $rawBody,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-99KF-Timestamp: ' . $timestamp,
'X-99KF-Signature: ' . $signature,
],
]);
$response = curl_exec($curl);
curl_close($curl);Java
以下示例使用 Java 11 的标准库 HttpClient,无须第三方依赖:
java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class OpenApiExample {
public static void main(String[] args) throws Exception {
String baseUrl = "https://99kf.example.com";
String secret = System.getenv("OPEN_API_SECRET");
String path = "/open/business/sync";
String rawBody = "{\"bid\":\"merchant_10001\",\"account_state\":\"normal\","
+ "\"username\":\"merchant_10001\",\"nickname\":\"示例商户\","
+ "\"password\":\"ChangeMe_2026\"}";
String timestamp = Long.toString(Instant.now().getEpochSecond());
String bodyHash = hex(MessageDigest.getInstance("SHA-256")
.digest(rawBody.getBytes(StandardCharsets.UTF_8)));
String canonical = "POST\n" + path + "\n" + timestamp + "\n" + bodyHash;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String signature = hex(mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8)));
HttpRequest request = HttpRequest.newBuilder(URI.create(baseUrl + path))
.header("Content-Type", "application/json")
.header("X-99KF-Timestamp", timestamp)
.header("X-99KF-Signature", signature)
.POST(HttpRequest.BodyPublishers.ofString(rawBody, StandardCharsets.UTF_8))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
}
private static String hex(byte[] bytes) {
StringBuilder output = new StringBuilder(bytes.length * 2);
for (byte item : bytes) {
output.append(String.format("%02x", item & 0xff));
}
return output.toString();
}
}Go
以下示例仅使用 Go 标准库:
go
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func main() {
baseURL := "https://99kf.example.com"
secret := os.Getenv("OPEN_API_SECRET")
path := "/open/business/sync"
payload := map[string]string{
"bid": "merchant_10001",
"account_state": "normal",
"username": "merchant_10001",
"nickname": "示例商户",
"password": "ChangeMe_2026",
}
// rawBody 是签名和 HTTP 请求共用的同一份字节。
rawBody, err := json.Marshal(payload)
if err != nil {
panic(err)
}
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
bodyHash := sha256.Sum256(rawBody)
canonical := strings.Join([]string{
"POST",
path,
timestamp,
hex.EncodeToString(bodyHash[:]),
}, "\n")
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(canonical))
signature := hex.EncodeToString(mac.Sum(nil))
request, err := http.NewRequest(http.MethodPost, baseURL+path, bytes.NewReader(rawBody))
if err != nil {
panic(err)
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("X-99KF-Timestamp", timestamp)
request.Header.Set("X-99KF-Signature", signature)
response, err := http.DefaultClient.Do(request)
if err != nil {
panic(err)
}
defer response.Body.Close()
}响应与鉴权错误
统一成功响应:
json
{
"code": 0,
"msg": "ok",
"data": {}
}失败时 code 为稳定错误码,msg 为说明,data 为空对象。
| 错误码 | HTTP 状态 | 处理建议 |
|---|---|---|
OPEN_API_TIMESTAMP_INVALID | 401 | 校准服务器时间,并在发送前重新生成时间戳与签名。 |
OPEN_API_SIGNATURE_INVALID | 401 | 确认密钥正确,且签名使用最终请求体、路径与查询串计算。 |