Skip to content

开放平台鉴权

商户账号同步与客服账号同步均使用相同的 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_INVALID401校准服务器时间,并在发送前重新生成时间戳与签名。
OPEN_API_SIGNATURE_INVALID401确认密钥正确,且签名使用最终请求体、路径与查询串计算。