PartnerShare 开放接口(/api/open/v1/*)采用 API Key + API Secret 签名鉴权。
每次请求需携带 API Key、秒级时间戳和签名。服务端校验通过后处理请求。
签名算法分为 v1、v2 两个版本,由 API Key 决定,与调用接口无关。
- v2:当前版本。v2 发布后创建的 API Key 默认使用 v2。
- v1:历史版本。仅适用于 v2 发布前创建的 API Key。
接入前,请在品牌主后台「产品管理 → 基础设置 → 开发者集成」查看 API Key 的签名版本,并使用对应算法生成签名完成集成。
1. 鉴权概览 #
| 项目 | 说明 |
|---|---|
| 请求域名 | https://api-service.partnershare.net |
| 鉴权方式 | 请求头 X-Api-Key + X-Api-Timestamp + X-Api-Sign。API Secret 只用于本地计算签名,不随请求发送 |
| 时间窗 | X-Api-Timestamp 与服务端时间相差不超过 300 秒,超出则拒绝请求 |
| 签名版本 | v2(当前版本):HMAC-SHA256,签名覆盖请求方法、路径、query、时间戳和请求体。v1(旧版本):仅请求参数字段名参与 SHA256 计算,仍可使用 |
| 版本判定 | 由 API Key 决定。每组密钥的版本在「产品管理 → 基础设置 → 开发者集成」中显示;新创建的密钥均为 v2,v2 发布前创建的密钥为 v1 |
| 接口支持范围 | 部分接口仅接受 v2 密钥,以各接口文档中的「支持的 API Key 版本」为准。v1 密钥调用这类接口返回 This endpoint requires signature version 2 |
| IP 白名单 | 密钥配置了白名单时,只放行名单内的来源 IP,支持单个 IP 和 CIDR;未配置则不限制 |
| 接口权限 | 密钥配置了权限列表时,调用接口需包含该接口要求的权限(如 conversion:adjust、promoter:read);未配置则不限制 |
| 响应格式 | 业务失败和鉴权失败均返回 HTTP 200,以响应体 code 判断,0 为成功 |
2. 获取 API Key 和 API Secret #
在品牌主后台「产品管理 → 基础设置 → 开发者集成」中创建和管理密钥。API Key 以 pk_ 开头,API Secret 以 sk_ 开头,每组密钥的签名版本在列表的「签名版本」列显示。

- Secret 只在创建成功时明文返回一次,之后列表和详情只显示掩码。丢失后无法找回,需新建一组密钥。
- 新创建的密钥签名版本为 v2。调用仅接受 v2 的接口时,需使用 v2 密钥。
- 删除密钥后立即失效,后续请求返回
The API key was not found。 - Secret 只保存在服务端,不写入浏览器、移动端包体、公开仓库或共享的调试环境。
v2 是当前的签名版本,对请求内容提供更完整的校验,新接口仅支持 v2。建议将仍在使用 v1 的密钥全部轮换为 v2。
轮换步骤:在「开发者集成」新建一组密钥(默认 v2),服务端切换到 v2 算法并验证调用正常后,删除原有的 v1 密钥。轮换期间新旧密钥可并存,删除后旧密钥立即失效。
3. 请求头 #
| Header | 必填 | 说明 |
|---|---|---|
X-Api-Key | 是 | 产品的 API Key |
X-Api-Timestamp | 是 | 秒级 Unix 时间戳。v2 密钥缺失时拒绝请求;v1 密钥缺失时服务端以当前时间参与校验,签名将无法通过 |
X-Api-Sign | 是 | 按密钥版本计算的签名,小写十六进制 |
Content-Type | 是 | POST 请求使用 application/json |
4. 签名 v2 #
4.1 待签名字符串 #
将以下 6 行按顺序以 \n(LF)拼接,末尾不含换行:
PS-HMAC-SHA256
{METHOD}
{PATH}
{QUERY}
{TIMESTAMP}
{BODY_SHA256}
| 行 | 取值 | 说明 |
|---|---|---|
| 1 | PS-HMAC-SHA256 | 固定字符串 |
| 2 | HTTP 方法 | 大写,如 GET、POST |
| 3 | 请求路径 | URL 中域名之后、? 之前的部分,如 /api/open/v1/track/refund。路径含编码字符时按实际发送的编码形式 |
| 4 | 原始 query 字符串 | URL 中 ? 之后的部分,不含 ?,与实际发送的参数顺序和编码逐字一致。没有 query 时为空行 |
| 5 | 时间戳 | 与 X-Api-Timestamp 相同 |
| 6 | 请求体 SHA256 | 对实际发送的原始请求体字节计算 SHA256,小写十六进制。GET 或空请求体时为 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
4.2 计算签名 #
signature = hex( HMAC-SHA256( key = api_secret, message = 待签名字符串 ) )
结果写入 X-Api-Sign。请求头不参与签名。
4.3 示例推导 #
以下两个示例均使用 api_secret = sk_your_api_secret、timestamp = 1776677721,可用于校验本地实现。
示例一:POST 请求,带 JSON 请求体
POST /api/open/v1/track/refund HTTP/1.1
Host: api-service.partnershare.net
Content-Type: application/json
X-Api-Key: pk_xxxxxxxxxxxxxxxxxxxxx
X-Api-Timestamp: 1776677721
X-Api-Sign: 66e6795c27b7b1cfc60ac1b2474d83524bc1db23074501afa39ce228a92446be
请求体原文(参与哈希的就是这段字节,发送时保持完全一致):
{"transaction_id":"order_202604220001","reason":"customer refund"}
请求体 SHA256:771425a965e46ff8d3e685b0deba9d6642965d3c93db4fe95b02d6885c22b9b6
待签名字符串(无 query,第 4 行为空行):
PS-HMAC-SHA256
POST
/api/open/v1/track/refund
1776677721
771425a965e46ff8d3e685b0deba9d6642965d3c93db4fe95b02d6885c22b9b6
签名:66e6795c27b7b1cfc60ac1b2474d83524bc1db23074501afa39ce228a92446be
示例二:GET 请求,带 query,无请求体
GET /api/open/v1/promoters/user_10001/statistics?start_date=2026-04-01&end_date=2026-04-30 HTTP/1.1
Host: api-service.partnershare.net
X-Api-Key: pk_xxxxxxxxxxxxxxxxxxxxx
X-Api-Timestamp: 1776677721
X-Api-Sign: fd1b625fc2c1580f21b87ecf08e7ddb21824730d043b746fd843efbc8f23d847
请求体为空,SHA256 为空字符串的哈希:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
待签名字符串:
PS-HMAC-SHA256
GET
/api/open/v1/promoters/user_10001/statistics
start_date=2026-04-01&end_date=2026-04-30
1776677721
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
签名:fd1b625fc2c1580f21b87ecf08e7ddb21824730d043b746fd843efbc8f23d847
4.4 代码示例 #
各示例中 body 为最终发送的原始请求体字符串,GET 传空字符串;query 为 URL 中 ? 之后的原始字符串,无 query 传空字符串。
<?php
function signV2(string $method, string $path, string $query, int $timestamp, string $body, string $apiSecret): string
{
$canonical = implode("\n", [
'PS-HMAC-SHA256',
strtoupper($method),
$path,
$query,
(string) $timestamp,
hash('sha256', $body),
]);
return hash_hmac('sha256', $canonical, $apiSecret);
}
$timestamp = time();
$body = json_encode([
'transaction_id' => 'order_202604220001',
'reason' => 'customer refund',
]);
$sign = signV2('POST', '/api/open/v1/track/refund', '', $timestamp, $body, $apiSecret);
// 发送时使用同一个 $body 字符串
const crypto = require('crypto');
function signV2(method, path, query, timestamp, body, apiSecret) {
const bodyHash = crypto.createHash('sha256').update(body, 'utf8').digest('hex');
const canonical = [
'PS-HMAC-SHA256',
method.toUpperCase(),
path,
query,
String(timestamp),
bodyHash,
].join('\n');
return crypto.createHmac('sha256', apiSecret).update(canonical, 'utf8').digest('hex');
}
const timestamp = Math.floor(Date.now() / 1000);
const body = JSON.stringify({ transaction_id: 'order_202604220001', reason: 'customer refund' });
const sign = signV2('POST', '/api/open/v1/track/refund', '', timestamp, body, apiSecret);
// 发送时使用同一个 body 字符串
import hashlib
import hmac
import json
import time
def sign_v2(method: str, path: str, query: str, timestamp: int, body: str, api_secret: str) -> str:
body_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
canonical = "\n".join([
"PS-HMAC-SHA256", method.upper(), path, query, str(timestamp), body_hash,
])
return hmac.new(api_secret.encode("utf-8"), canonical.encode("utf-8"), hashlib.sha256).hexdigest()
timestamp = int(time.time())
body = json.dumps(
{"transaction_id": "order_202604220001", "reason": "customer refund"},
separators=(",", ":"),
)
sign = sign_v2("POST", "/api/open/v1/track/refund", "", timestamp, body, api_secret)
# 发送时使用同一个 body 字符串
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strconv"
"strings"
)
func SignV2(method, path, query string, timestamp int64, body []byte, apiSecret string) string {
bodyHash := sha256.Sum256(body)
canonical := strings.Join([]string{
"PS-HMAC-SHA256",
strings.ToUpper(method),
path,
query,
strconv.FormatInt(timestamp, 10),
hex.EncodeToString(bodyHash[:]),
}, "\n")
mac := hmac.New(sha256.New, []byte(apiSecret))
mac.Write([]byte(canonical))
return hex.EncodeToString(mac.Sum(nil))
}
4.5 注意事项 #
- 先序列化请求体,再使用同一字符串计算哈希并发送。若哈希后重新序列化,键顺序、空白或 Unicode 转义的差异会导致签名不匹配。
- 网关、SDK 或代理层修改请求体、调整 query 参数顺序或编码,均会导致签名不匹配。排查时以实际发出的请求行和请求体为准。
- query 参数值需要 URL 编码时,签名使用编码后的字符串。
- 时间戳取服务器当前时间,并保持时钟与标准时间同步。
5. 签名 v1(旧版) #
v1 密钥仍可使用,算法保持不变:仅请求参数的字段名参与计算,字段值和请求头均不参与。新接口不再支持 v1,建议按第 2 节的安全建议轮换为 v2。
5.1 算法 #
- 收集 URL query、表单 body、JSON body 的顶层字段名,嵌套字段不参与。
- 字段名统一转小写。
- 按自然顺序排序(PHP
SORT_NATURAL)。 - 用
&连接字段名。 - 追加时间戳和 API Secret,计算 SHA256,小写十六进制。
signature = sha256( sorted_lowercase_keys_joined_by_& + timestamp + api_secret )
5.2 示例推导 #
请求体:
{"event_name":"signup","invited_user_id":"user_10001","invite_code":"abc123"}
字段名排序拼接:event_name&invite_code&invited_user_id
待签名字符串(timestamp = 1776677721,api_secret = sk_your_api_secret):
event_name&invite_code&invited_user_id1776677721sk_your_api_secret
签名:a01880c87f291e09edf069d0fae34d2243879a4612f2d5eed7147b73ca5d61f7
5.3 代码示例 #
<?php
function signV1(array $params, int $timestamp, string $apiSecret): string
{
$keys = array_map('strtolower', array_keys($params));
sort($keys, SORT_NATURAL);
return hash('sha256', implode('&', $keys) . $timestamp . $apiSecret);
}
const crypto = require('crypto');
function signV1(params, timestamp, apiSecret) {
const keys = Object.keys(params)
.map((key) => key.toLowerCase())
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
return crypto.createHash('sha256').update(keys.join('&') + timestamp + apiSecret).digest('hex');
}
import hashlib
def sign_v1(params: dict, timestamp: int, api_secret: str) -> str:
keys = sorted(key.lower() for key in params.keys())
return hashlib.sha256(("&".join(keys) + str(timestamp) + api_secret).encode("utf-8")).hexdigest()
func SignV1(params map[string]interface{}, timestamp int64, apiSecret string) string {
keys := make([]string, 0, len(params))
for key := range params {
keys = append(keys, strings.ToLower(key))
}
sort.Strings(keys)
sum := sha256.Sum256([]byte(strings.Join(keys, "&") + strconv.FormatInt(timestamp, 10) + apiSecret))
return hex.EncodeToString(sum[:])
}
6. 错误响应 #
鉴权失败返回 HTTP 200,code 为 1000004,message 为英文,data 为 null。
{
"code": 1000004,
"message": "Signature verification failed",
"data": null
}
| message | 原因 | 处理 |
|---|---|---|
The API key and signature are required | 缺少 X-Api-Key 或 X-Api-Sign | 检查请求头是否被网关或代理移除 |
The API key was not found | API Key 不存在或已删除 | 核对 API Key,必要时在后台重新创建 |
This endpoint requires signature version 2 | v1 密钥调用了仅接受 v2 的接口 | 使用 v2 版本的密钥 |
The request timestamp is required | v2 密钥请求缺少 X-Api-Timestamp | 补充 X-Api-Timestamp 请求头 |
The request timestamp is invalid or expired | 时间戳与服务端相差超过 300 秒 | 使用秒级时间戳,校准服务器时间 |
Signature verification failed | 签名不匹配 | 确认密钥版本与算法一致;v2 核对方法、路径、query、请求体原文;v1 核对字段名与排序 |
The API key is disabled | 密钥已禁用 | 在后台启用或更换密钥 |
The API key has expired | 密钥已过期 | 创建新密钥 |
The client IP address is not allowlisted | 来源 IP 不在密钥白名单内 | 在后台更新白名单,确认服务器出口 IP |
Missing required permission: {permission} | 密钥权限列表未包含该接口要求的权限 | 为密钥补充权限或更换密钥 |
7. 排查清单 #
- 在「开发者集成」页面确认密钥版本,v1 与 v2 算法不能混用。
- 签名密钥为 API Secret,而非 API Key。
- v2:输出待签名字符串逐行比对,重点检查 query 与请求体原文。
- v1:输出参与签名的字段名列表,确认与实际发送的顶层字段一致。
- 时间戳为秒级且在 300 秒窗口内。
- 生产环境与测试环境使用不同的密钥;怀疑 Secret 泄露时立即删除该密钥并重新创建。