前端访问 /api/pinnacle 时,需要使用动态签名。请求中不会直接传密钥。
公网访问:
https://cb.long.bid
内网访问:
http://172.27.74.243
接口路径统一以 /api/pinnacle 开头,例如:
https://cb.long.bid/api/pinnacle/events
http://172.27.74.243/api/pinnacle/events
本地数据接口:
GET /api/pinnacle/events
POST /api/pinnacle/events
Pinnacle Lines API:
GET /api/pinnacle/v2/line
POST /api/pinnacle/v3/line/parlay
POST /api/pinnacle/v1/line/teaser
GET /api/pinnacle/v2/line/special
GET /api/pinnacle/v3/sports
GET /api/pinnacle/v3/leagues
GET /api/pinnacle/v1/periods
GET /api/pinnacle/v2/inrunning
GET /api/pinnacle/v1/cancellationreasons
GET /api/pinnacle/v2/currencies
GET /api/pinnacle/v1/client/balance
Pinnacle Bets API:
POST /api/pinnacle/v4/bets/place
POST /api/pinnacle/v4/bets/parlay
POST /api/pinnacle/v4/bets/teaser
POST /api/pinnacle/v4/bets/special
GET /api/pinnacle/v3/bets
GET /api/pinnacle/v3/bets/settled
GET /api/pinnacle/v1/regrades/wager-history
GET /api/pinnacle/v1/bets/betting-status
签名中的 PATH 不包含 /api/pinnacle 前缀。例如请求 /api/pinnacle/events,签名时 PATH 使用 /events。
/events 是本地数据接口,不请求上游 Pinnacle,直接读取服务端内存中的 GLOBAL_DATA.gamesMap。
GET 支持 query 参数:
GET /api/pinnacle/events
GET /api/pinnacle/events?id=123
GET /api/pinnacle/events?ids=123,456
GET /api/pinnacle/events?eventId=123
GET /api/pinnacle/events?eventIds=123,456
POST 支持 JSON body:
{ "id": 123 }
{ "ids": [123, 456] }
{ "eventId": 123 }
{ "eventIds": "123,456" }
参数优先级:
ids -> id -> eventIds -> eventId
返回说明:
GLOBAL_DATA.gamesMap 中的全部赛事。每次请求都需要带:
X-Api-User: alice
X-Api-Timestamp: 1710000000000
X-Api-Nonce: random-string
X-Api-Signature: hmac-sha256-hex
说明:
X-Api-User:服务端 .env 中配置的用户名。X-Api-Timestamp:当前毫秒时间戳,允许和服务端相差 5 分钟。X-Api-Nonce:每次请求都要不同,防止重放。X-Api-Signature:用用户密钥对签名内容做 HMAC-SHA256。签名字符串由 6 行组成:
METHOD
PATH
QUERY_JSON
BODY_SHA256
TIMESTAMP
NONCE
示例:
GET
/events
{"id":"123"}
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
1710000000000
abc123
其中:
METHOD:大写 HTTP 方法,例如 GET、POSTPATH:/api/pinnacle 后面的路径,例如 /events、/v4/bets/placeQUERY_JSON:query 参数按 key 排序后的 JSON 字符串BODY_SHA256:body JSON 按 key 排序后做 SHA256;空 body 使用空字符串的 SHA256TIMESTAMP:同 X-Api-TimestampNONCE:同 X-Api-Nonce浏览器端可使用 Web Crypto API:
const stableStringify = (value) => {
if (Array.isArray(value)) {
return `[${value.map(stableStringify).join(',')}]`;
}
if (value && typeof value === 'object') {
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
}
return JSON.stringify(value);
};
const toHex = (buffer) => {
return [...new Uint8Array(buffer)]
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('');
};
const sha256 = async (text) => {
const data = new TextEncoder().encode(text);
const hash = await crypto.subtle.digest('SHA-256', data);
return toHex(hash);
};
const hmacSha256 = async (secret, text) => {
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign'],
);
const signature = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(text));
return toHex(signature);
};
const createPinnacleHeaders = async ({ method, path, query = {}, body, user, secret }) => {
const timestamp = String(Date.now());
const nonce = crypto.randomUUID();
const bodyText = body === undefined || body === null ? '' : stableStringify(body);
const bodyHash = await sha256(bodyText);
const payload = [
method.toUpperCase(),
path,
stableStringify(query),
bodyHash,
timestamp,
nonce,
].join('\n');
const signature = await hmacSha256(secret, payload);
return {
'X-Api-User': user,
'X-Api-Timestamp': timestamp,
'X-Api-Nonce': nonce,
'X-Api-Signature': signature,
};
};
const user = 'alice';
const secret = 'aliceSecret';
const path = '/events';
const query = { ids: '123,456' };
const search = new URLSearchParams(query).toString();
const headers = await createPinnacleHeaders({
method: 'GET',
path,
query,
user,
secret,
});
const res = await fetch(`/api/pinnacle${path}?${search}`, { headers });
const data = await res.json();
const user = 'trader';
const secret = 'traderSecret';
const path = '/v4/bets/place';
const body = {
uniqueRequestId: crypto.randomUUID(),
stake: 100,
winRiskStake: 'RISK',
oddsFormat: 'DECIMAL',
lineId: 123456,
};
const headers = await createPinnacleHeaders({
method: 'POST',
path,
body,
user,
secret,
});
const res = await fetch(`/api/pinnacle${path}`, {
method: 'POST',
headers: {
...headers,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
const data = await res.json();
401 Missing API signature headers:缺少签名请求头。401 Invalid API timestamp:时间戳过期或不是毫秒时间戳。401 API nonce has already been used:nonce 重复。401 Invalid API signature:签名不匹配。403 Forbidden:用户没有该接口权限。