Skip to content

認證與簽章 ​

租戶 API 的每個請求都以 HMAC-SHA256 簽章。伺服器依序檢查金鑰、時間戳、nonce、簽章、租戶狀態、IP 白名單與限流,全部通過才會處理請求。

請求標頭 ​

標頭內容
X-Api-Key金鑰 ID:沙箱 ek_s_…、正式 ek_l_…,例如 ek_s_1a_XXXXXXXXXXXXXXXX
X-Timestamp目前的 Unix 時間,秒(9–11 位數字);與伺服器時間差須在 ±300 秒內
X-Nonce每個請求都不同的隨機字串,16–64 個英數字;10 分鐘內不可重複
X-Signature簽章,64 個十六進位字元(建議小寫;大小寫都接受)
Content-Type有本文時為 application/json

金鑰 ​

  • 金鑰 ID(X-Api-Key)格式是 ek_<s|l>_<租戶代號>_<16 碼>:s 為沙箱、l 為正式。金鑰 ID 本身就指出是哪個租戶,所以請求不需要另外帶公司代碼。
  • Secret 是 es_ 加 40 個英數字。整個字串(包含 es_)以 UTF-8 位元組作為 HMAC 金鑰。
  • 金鑰在 Console 上線與串接 → API 金鑰 管理:
    • 每個環境(沙箱、正式)最多 10 把有效金鑰。
    • 「顯示」「複製」Secret 需要先驗證雙因子(TOTP),每次都會留下稽核紀錄。
    • 輪替:產生新金鑰,舊金鑰轉為「輪替中」,24 小時後失效;請在期限內把新 Secret 部署到你的系統。
    • 停用:立即失效,無法復原。
  • 沙箱金鑰連到你的沙箱租戶(測試幣、模擬器桌),正式金鑰連到正式租戶。兩者使用同一個主機與同一套 API。

簽章演算法 ​

text
string_to_sign = METHOD         + "\n" +
                 PATH_AND_QUERY + "\n" +
                 X-Timestamp    + "\n" +
                 X-Nonce        + "\n" +
                 hex(SHA-256(body))

X-Signature    = hex(HMAC-SHA256(key = secret, message = string_to_sign))
部分規則
METHOD大寫的 HTTP 方法:GET 或 POST
PATH_AND_QUERY從 /api/tenant/v1/… 開始的完整路徑,有查詢字串時加上 ? 與查詢字串。不含通訊協定、主機與 # 之後的部分。查詢字串必須和實際送出的完全相同(參數順序、百分比編碼都一樣)
X-Timestamp、X-Nonce與標頭中的字串完全相同
hex(SHA-256(body))實際送出的本文位元組(UTF-8)的 SHA-256,小寫十六進位。沒有本文時是空字串的雜湊:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
換行只用 \n(LF),最後一行後面沒有換行

GET 請求

伺服器處理 GET 請求時一律忽略本文,本文雜湊固定是空字串的雜湊。GET 的參數全部放在查詢字串,並且會被簽進 PATH_AND_QUERY。

範例 ​

用下列(僅供示範的)金鑰與輸入計算:

項目值
金鑰 IDek_s_1a_XXXXXXXXXXXXXXXX
Secretes_0123456789abcdefghijABCDEFGHIJ0123456789
方法與路徑POST /api/tenant/v1/player/launch
X-Timestamp1790218800(2026-09-24T03:00:00Z)
X-Noncen0nce8H2kQ9xYz4LmP0aBcDe
本文{"username":"alice","lang":"ENG","device":"mobile"}
  1. 本文的 SHA-256:

    text
    a0c1cfba44962853266340b6610d542620041fae49e6bcb10e6446ca0f73ad6a
  2. 簽章字串(五行):

    text
    POST
    /api/tenant/v1/player/launch
    1790218800
    n0nce8H2kQ9xYz4LmP0aBcDe
    a0c1cfba44962853266340b6610d542620041fae49e6bcb10e6446ca0f73ad6a
  3. X-Signature:

    text
    6e4885c5d9f191b2e4ee5f4cb40ae148b13b277e606181bf853e040a06a0ac80

完整的請求:

http
POST /api/tenant/v1/player/launch HTTP/1.1
Host: elite.ewin888.com
X-Api-Key: ek_s_1a_XXXXXXXXXXXXXXXX
X-Timestamp: 1790218800
X-Nonce: n0nce8H2kQ9xYz4LmP0aBcDe
X-Signature: 6e4885c5d9f191b2e4ee5f4cb40ae148b13b277e606181bf853e040a06a0ac80
Content-Type: application/json

{"username":"alice","lang":"ENG","device":"mobile"}

GET 的範例:GET /api/tenant/v1/wallet/balance?username=alice,時間戳相同、nonce 為 n0nce8H2kQ9xYz4LmP0aBcDf、沒有本文,簽章為 273b761bda9ca095a8c7ae22908844bc1ce728212fceda3d0b2129fc9bb03032。

這兩組數字可以直接貼到簽章除錯器驗算。

程式碼範例 ​

每個範例都包含 sign 函式與一個送出請求的小工具,直接執行檔案會印出上面範例的簽章 6e4885c5…0ac80。

js
// elite tenant API: request signing (Node.js 18+, ES module, no dependencies)
import { createHash, createHmac, randomBytes } from 'node:crypto';
import { pathToFileURL } from 'node:url';

/**
 * X-Signature = hex(HMAC-SHA256(secret,
 *   METHOD + "\n" + PATH_AND_QUERY + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + hex(SHA-256(body))))
 */
export function sign(secret, method, pathAndQuery, timestamp, nonce, body = '') {
  const bodyHash = createHash('sha256').update(body, 'utf8').digest('hex');
  const stringToSign = [method.toUpperCase(), pathAndQuery, timestamp, nonce, bodyHash].join('\n');
  return createHmac('sha256', secret).update(stringToSign, 'utf8').digest('hex');
}

/** 32 hex characters; use a new nonce for every request, including retries */
export function newNonce() {
  return randomBytes(16).toString('hex');
}

/** Sends a signed request. Returns `data`, or throws { status, code, message }. */
export async function call(method, pathAndQuery, payload, { keyId, secret, baseUrl = 'https://elite.ewin-soft.com' }) {
  // Sign exactly the bytes you send: serialize once and reuse the string.
  const body = payload === undefined ? '' : JSON.stringify(payload);
  const timestamp = String(Math.floor(Date.now() / 1000)); // seconds, not milliseconds
  const nonce = newNonce();
  const headers = {
    'X-Api-Key': keyId,
    'X-Timestamp': timestamp,
    'X-Nonce': nonce,
    'X-Signature': sign(secret, method, pathAndQuery, timestamp, nonce, body),
  };
  if (body) headers['Content-Type'] = 'application/json';
  const res = await fetch(baseUrl + pathAndQuery, { method, headers, body: body || undefined, signal: AbortSignal.timeout(10_000) });
  const json = await res.json().catch(() => null);
  if (!res.ok || !json?.ok) {
    throw { status: res.status, code: json?.error?.code ?? `HTTP_${res.status}`, message: json?.error?.message ?? res.statusText };
  }
  return json.data;
}

// Example:
//   const auth = { keyId: process.env.ELITE_KEY_ID, secret: process.env.ELITE_SECRET };
//   const { url } = await call('POST', '/api/tenant/v1/player/launch', { username: 'alice' }, auth);
//   const { balance } = await call('GET', '/api/tenant/v1/wallet/balance?username=alice', undefined, auth);

// Worked example from the docs: prints 6e4885c5d9f191b2e4ee5f4cb40ae148b13b277e606181bf853e040a06a0ac80
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  console.log(
    sign(
      'es_0123456789abcdefghijABCDEFGHIJ0123456789',
      'POST',
      '/api/tenant/v1/player/launch',
      '1790218800',
      'n0nce8H2kQ9xYz4LmP0aBcDe',
      '{"username":"alice","lang":"ENG","device":"mobile"}',
    ),
  );
}
php
<?php
// elite tenant API: request signing (PHP 7.2+; the request helper needs ext-curl)

/**
 * hex(HMAC-SHA256(secret, METHOD \n PATH_AND_QUERY \n TIMESTAMP \n NONCE \n hex(SHA-256(body))))
 */
function elite_sign(string $secret, string $method, string $pathAndQuery, string $timestamp, string $nonce, string $body = ''): string
{
    $stringToSign = strtoupper($method) . "\n" . $pathAndQuery . "\n" . $timestamp . "\n" . $nonce . "\n" . hash('sha256', $body);
    return hash_hmac('sha256', $stringToSign, $secret);
}

/**
 * Sends a signed request and returns `data`. Throws RuntimeException with the error code on failure.
 */
function elite_call(string $method, string $pathAndQuery, ?array $payload, string $keyId, string $secret, string $baseUrl = 'https://elite.ewin-soft.com'): array
{
    // Sign exactly the bytes you send: encode once and reuse the string.
    $body = $payload === null ? '' : json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $timestamp = (string) time();         // seconds
    $nonce = bin2hex(random_bytes(16));   // new nonce for every request, including retries
    $headers = [
        'X-Api-Key: ' . $keyId,
        'X-Timestamp: ' . $timestamp,
        'X-Nonce: ' . $nonce,
        'X-Signature: ' . elite_sign($secret, $method, $pathAndQuery, $timestamp, $nonce, $body),
    ];
    if ($body !== '') {
        $headers[] = 'Content-Type: application/json';
    }
    $ch = curl_init($baseUrl . $pathAndQuery);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 10,
    ]);
    if ($body !== '') {
        curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
    }
    $raw = curl_exec($ch);
    $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    $json = json_decode((string) $raw, true);
    if ($status < 200 || $status >= 300 || empty($json['ok'])) {
        $code = $json['error']['code'] ?? ('HTTP_' . $status);
        throw new RuntimeException($code . ': ' . ($json['error']['message'] ?? ''), $status);
    }
    return $json['data'];
}

// Example:
//   $url = elite_call('POST', '/api/tenant/v1/player/launch', ['username' => 'alice'], $keyId, $secret)['url'];
//   $balance = elite_call('GET', '/api/tenant/v1/wallet/balance?username=alice', null, $keyId, $secret)['balance'];

// Worked example from the docs: prints 6e4885c5d9f191b2e4ee5f4cb40ae148b13b277e606181bf853e040a06a0ac80
if (PHP_SAPI === 'cli' && isset($argv[0]) && realpath($argv[0]) === __FILE__) {
    echo elite_sign('es_0123456789abcdefghijABCDEFGHIJ0123456789', 'POST', '/api/tenant/v1/player/launch',
        '1790218800', 'n0nce8H2kQ9xYz4LmP0aBcDe', '{"username":"alice","lang":"ENG","device":"mobile"}'), PHP_EOL;
}
cs
// elite tenant API: request signing (.NET 6+)
using System;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

// Worked example from the docs: prints 6e4885c5d9f191b2e4ee5f4cb40ae148b13b277e606181bf853e040a06a0ac80
Console.WriteLine(EliteSigner.Sign("es_0123456789abcdefghijABCDEFGHIJ0123456789", "POST", "/api/tenant/v1/player/launch",
    "1790218800", "n0nce8H2kQ9xYz4LmP0aBcDe", "{\"username\":\"alice\",\"lang\":\"ENG\",\"device\":\"mobile\"}"));

public static class EliteSigner
{
    /// hex(HMAC-SHA256(secret, METHOD \n PATH_AND_QUERY \n TIMESTAMP \n NONCE \n hex(SHA-256(body))))
    public static string Sign(string secret, string method, string pathAndQuery, string timestamp, string nonce, string body)
    {
        var bodyHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(body))).ToLowerInvariant();
        var stringToSign = $"{method.ToUpperInvariant()}\n{pathAndQuery}\n{timestamp}\n{nonce}\n{bodyHash}";
        var mac = HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), Encoding.UTF8.GetBytes(stringToSign));
        return Convert.ToHexString(mac).ToLowerInvariant();
    }

    /// Sends a signed request and returns `data`. Throws HttpRequestException on failure.
    public static async Task<JsonElement> CallAsync(HttpClient http, string method, string pathAndQuery, object? payload,
        string keyId, string secret, string baseUrl = "https://elite.ewin-soft.com")
    {
        // Sign exactly the bytes you send: serialize once and reuse the string.
        var body = payload is null ? "" : JsonSerializer.Serialize(payload);
        var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();                 // seconds
        var nonce = Convert.ToHexString(RandomNumberGenerator.GetBytes(16)).ToLowerInvariant(); // new for every request
        using var request = new HttpRequestMessage(new HttpMethod(method), baseUrl + pathAndQuery);
        request.Headers.Add("X-Api-Key", keyId);
        request.Headers.Add("X-Timestamp", timestamp);
        request.Headers.Add("X-Nonce", nonce);
        request.Headers.Add("X-Signature", Sign(secret, method, pathAndQuery, timestamp, nonce, body));
        if (body.Length > 0) request.Content = new StringContent(body, Encoding.UTF8, "application/json");
        using var response = await http.SendAsync(request);
        var text = await response.Content.ReadAsStringAsync();
        using var doc = JsonDocument.Parse(text);
        var root = doc.RootElement;
        if (!response.IsSuccessStatusCode || !root.GetProperty("ok").GetBoolean())
        {
            var error = root.GetProperty("error");
            throw new HttpRequestException($"{error.GetProperty("code").GetString()}: {error.GetProperty("message").GetString()}");
        }
        return root.GetProperty("data").Clone();
    }
}
java
// elite tenant API: request signing (Java 11+, no dependencies)
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.security.SecureRandom;
import java.time.Duration;
import java.util.Locale;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public final class EliteSigner {
    private static final SecureRandom RANDOM = new SecureRandom();

    /** hex(HMAC-SHA256(secret, METHOD \n PATH_AND_QUERY \n TIMESTAMP \n NONCE \n hex(SHA-256(body)))) */
    public static String sign(String secret, String method, String pathAndQuery, String timestamp, String nonce, String body) throws Exception {
        String bodyHash = hex(MessageDigest.getInstance("SHA-256").digest(body.getBytes(StandardCharsets.UTF_8)));
        String stringToSign = method.toUpperCase(Locale.ROOT) + "\n" + pathAndQuery + "\n" + timestamp + "\n" + nonce + "\n" + bodyHash;
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        return hex(mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)));
    }

    /** 32 hex characters; use a new nonce for every request, including retries */
    public static String newNonce() {
        byte[] bytes = new byte[16];
        RANDOM.nextBytes(bytes);
        return hex(bytes);
    }

    /** Sends a signed request and returns the JSON response body; throws on a non-2xx status. */
    public static String call(HttpClient http, String method, String pathAndQuery, String jsonBody,
                              String keyId, String secret, String baseUrl) throws Exception {
        // Sign exactly the bytes you send: build the JSON string once and reuse it.
        String body = jsonBody == null ? "" : jsonBody;
        String timestamp = Long.toString(System.currentTimeMillis() / 1000); // seconds
        String nonce = newNonce();
        HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(baseUrl + pathAndQuery))
            .timeout(Duration.ofSeconds(10))
            .header("X-Api-Key", keyId)
            .header("X-Timestamp", timestamp)
            .header("X-Nonce", nonce)
            .header("X-Signature", sign(secret, method, pathAndQuery, timestamp, nonce, body));
        if (body.isEmpty()) {
            builder.method(method, HttpRequest.BodyPublishers.noBody());
        } else {
            builder.header("Content-Type", "application/json")
                .method(method, HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8));
        }
        HttpResponse<String> response = http.send(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
        if (response.statusCode() / 100 != 2) {
            throw new RuntimeException("HTTP " + response.statusCode() + ": " + response.body());
        }
        return response.body();
    }

    private static String hex(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            sb.append(Character.forDigit((b >> 4) & 0xf, 16)).append(Character.forDigit(b & 0xf, 16));
        }
        return sb.toString();
    }

    // Worked example from the docs: prints 6e4885c5d9f191b2e4ee5f4cb40ae148b13b277e606181bf853e040a06a0ac80
    public static void main(String[] args) throws Exception {
        System.out.println(sign("es_0123456789abcdefghijABCDEFGHIJ0123456789", "POST", "/api/tenant/v1/player/launch",
            "1790218800", "n0nce8H2kQ9xYz4LmP0aBcDe", "{\"username\":\"alice\",\"lang\":\"ENG\",\"device\":\"mobile\"}"));
    }
}
py
"""elite tenant API: request signing (Python 3.8+, standard library only)."""
import hashlib
import hmac
import json
import secrets
import time
import urllib.request


def sign(secret: str, method: str, path_and_query: str, timestamp: str, nonce: str, body: bytes = b"") -> str:
    """hex(HMAC-SHA256(secret, METHOD \\n PATH_AND_QUERY \\n TIMESTAMP \\n NONCE \\n hex(SHA-256(body))))"""
    body_hash = hashlib.sha256(body).hexdigest()
    string_to_sign = "\n".join([method.upper(), path_and_query, timestamp, nonce, body_hash])
    return hmac.new(secret.encode("utf-8"), string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()


def call(method, path_and_query, payload, key_id, secret, base_url="https://elite.ewin-soft.com"):
    """Sends a signed request and returns `data`. Raises urllib.error.HTTPError on 4xx/5xx."""
    # Sign exactly the bytes you send: serialize once and reuse them.
    body = b"" if payload is None else json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
    timestamp = str(int(time.time()))  # seconds
    nonce = secrets.token_hex(16)  # new nonce for every request, including retries
    headers = {
        "X-Api-Key": key_id,
        "X-Timestamp": timestamp,
        "X-Nonce": nonce,
        "X-Signature": sign(secret, method, path_and_query, timestamp, nonce, body),
        "User-Agent": "my-backend/1.0",
    }
    if body:
        headers["Content-Type"] = "application/json"
    request = urllib.request.Request(base_url + path_and_query, data=body or None, method=method, headers=headers)
    with urllib.request.urlopen(request, timeout=10) as response:
        return json.loads(response.read())["data"]


# Example:
#   url = call("POST", "/api/tenant/v1/player/launch", {"username": "alice"}, KEY_ID, SECRET)["url"]
#   balance = call("GET", "/api/tenant/v1/wallet/balance?username=alice", None, KEY_ID, SECRET)["balance"]

if __name__ == "__main__":
    # Worked example from the docs: prints 6e4885c5d9f191b2e4ee5f4cb40ae148b13b277e606181bf853e040a06a0ac80
    print(sign("es_0123456789abcdefghijABCDEFGHIJ0123456789", "POST", "/api/tenant/v1/player/launch",
               "1790218800", "n0nce8H2kQ9xYz4LmP0aBcDe", b'{"username":"alice","lang":"ENG","device":"mobile"}'))
go
// elite tenant API: request signing (Go 1.20+, standard library only)
package main

import (
	"bytes"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"
	"time"
)

// Sign returns hex(HMAC-SHA256(secret, METHOD \n PATH_AND_QUERY \n TIMESTAMP \n NONCE \n hex(SHA-256(body)))).
func Sign(secret, method, pathAndQuery, timestamp, nonce string, body []byte) string {
	sum := sha256.Sum256(body)
	stringToSign := strings.Join([]string{strings.ToUpper(method), pathAndQuery, timestamp, nonce, hex.EncodeToString(sum[:])}, "\n")
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(stringToSign))
	return hex.EncodeToString(mac.Sum(nil))
}

// NewNonce returns 32 hex characters; use a new nonce for every request, including retries.
func NewNonce() string {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		panic(err)
	}
	return hex.EncodeToString(b)
}

// Call sends a signed request and returns the JSON response body; it returns an error on a non-2xx status.
// Sign exactly the bytes you send: marshal the JSON once and pass the same slice here.
func Call(client *http.Client, method, pathAndQuery string, body []byte, keyID, secret, baseURL string) ([]byte, error) {
	timestamp := strconv.FormatInt(time.Now().Unix(), 10) // seconds
	nonce := NewNonce()
	req, err := http.NewRequest(method, baseURL+pathAndQuery, bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	req.Header.Set("X-Api-Key", keyID)
	req.Header.Set("X-Timestamp", timestamp)
	req.Header.Set("X-Nonce", nonce)
	req.Header.Set("X-Signature", Sign(secret, method, pathAndQuery, timestamp, nonce, body))
	if len(body) > 0 {
		req.Header.Set("Content-Type", "application/json")
	}
	res, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	data, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}
	if res.StatusCode/100 != 2 {
		return data, fmt.Errorf("HTTP %d: %s", res.StatusCode, data)
	}
	return data, nil
}

// Worked example from the docs: prints 6e4885c5d9f191b2e4ee5f4cb40ae148b13b277e606181bf853e040a06a0ac80
func main() {
	fmt.Println(Sign("es_0123456789abcdefghijABCDEFGHIJ0123456789", "POST", "/api/tenant/v1/player/launch",
		"1790218800", "n0nce8H2kQ9xYz4LmP0aBcDe", []byte(`{"username":"alice","lang":"ENG","device":"mobile"}`)))
}

伺服器的檢查順序 ​

順序檢查失敗時
1X-Api-Key 的格式401 UNAUTHORIZED(invalid credentials)
2本文不超過 64 KB413 PAYLOAD_TOO_LARGE
3金鑰存在、未停用(輪替中的舊金鑰在 24 小時內仍有效)401 UNAUTHORIZED(invalid credentials)
4X-Timestamp 格式正確、與伺服器時間差 ≤ 300 秒401 UNAUTHORIZED(timestamp outside the ±300 s window)
5X-Nonce 格式(16–64 英數字)401 UNAUTHORIZED(invalid credentials)
6簽章以固定時間比對401 UNAUTHORIZED(invalid credentials)
7租戶未停用、未停權403 TENANT_SUSPENDED
8來源 IP 在白名單內(有設定時)403 IP_NOT_ALLOWED
9租戶限流429 RATE_LIMITED
10nonce 在 10 分鐘內沒有用過401 UNAUTHORIZED(nonce already used)
  • 金鑰不存在與簽章錯誤回同一個訊息,避免被用來探測金鑰。
  • nonce 只有在前面全部通過時才會被記錄,所以因簽章錯誤、限流等被拒絕的請求不會占用 nonce。即使如此,每次重試都請產生新的 nonce 與時間戳。
  • 收到 nonce already used 表示這個請求沒有被處理(例如網路層自動重送了同一個請求)。

IP 白名單 ​

在 Console 上線與串接 → IP 白名單與 Webhook 設定,最多 100 筆 IPv4、IPv6 位址或 CIDR(例如 203.0.113.10、203.0.113.0/24、2001:db8::/32)。空白代表不限制。

  • 判斷依據是 Cloudflare 看到的來源 IP(CF-Connecting-IP),也就是你的伺服器的對外 IP。
  • 伺服器同時有 IPv4 與 IPv6 時,請兩種都加入,或讓 HTTP 用戶端固定使用其中一種。
  • 白名單設定在正式租戶上;沙箱金鑰不受限制。

常見錯誤 ​

狀況原因與解法
一直收到 invalid credentials用簽章除錯器輸入同樣的值,逐行比對簽章字串
路徑少了前綴PATH_AND_QUERY 必須從 /api/tenant/v1 開始,不是相對於 Base URL 的 /player/launch
GET 簽章錯誤忘了把查詢字串簽進去,或簽章用的查詢字串與實際送出的編碼、順序不同
POST 簽章錯誤簽章用的本文和送出的本文不是同一個字串:只序列化一次,簽章與送出都用那個字串;不要讓 HTTP 函式庫重新序列化
非英文字元以 UTF-8 位元組計算本文雜湊(例如中文暱稱)
timestamp outside the ±300 s window伺服器時鐘偏移(請開啟 NTP),或送了毫秒而不是秒
nonce already used重試時沿用了舊的 nonce;每個請求都要產生新的 nonce
Secret 不對Secret 要包含 es_ 前綴;金鑰 ID 與 Secret 必須是同一把、同一個環境
IP_NOT_ALLOWED對外 IP 不在白名單,或伺服器改走 IPv6

elite 租戶整合 API v1