Skip to content

Authentication & signing ​

Every tenant API request is signed with HMAC-SHA256. The server checks the key, timestamp, nonce, signature, tenant state, IP allowlist and rate limit, in that order, and only then handles the request.

Request headers ​

HeaderContent
X-Api-KeyKey ID: sandbox ek_s_…, live ek_l_…, for example ek_s_1a_XXXXXXXXXXXXXXXX
X-TimestampCurrent Unix time in seconds (9–11 digits); must be within ±300 seconds of server time
X-NonceA random string that differs on every request, 16–64 letters and digits; must not repeat within 10 minutes
X-SignatureThe signature, 64 hexadecimal characters (lower case recommended; either case is accepted)
Content-Typeapplication/json when there is a body

Keys ​

  • The key ID (X-Api-Key) has the form ek_<s|l>_<tenant>_<16 chars>: s for sandbox, l for live. The key ID identifies your tenant, so requests never carry a separate company code.
  • The secret is es_ followed by 40 letters and digits. The whole string (including es_), as UTF-8 bytes, is the HMAC key.
  • Keys are managed in the Console under Go-live & integration → API keys:
    • At most 10 active keys per environment (sandbox, live).
    • "Show" and "Copy" require two-factor verification (TOTP), and every reveal is audited.
    • Rotate creates a new key and marks the old one as rotating; the old key stops working 24 hours later. Deploy the new secret within that window.
    • Disable revokes the key immediately and cannot be undone.
  • A sandbox key belongs to your sandbox tenant (test coins, simulator tables); a live key belongs to your live tenant. Both use the same host and the same API.

Algorithm ​

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))
PartRule
METHODThe HTTP method in upper case: GET or POST
PATH_AND_QUERYThe full path starting with /api/tenant/v1/…, plus ? and the query string when there is one. No scheme, host or # fragment. The query string must be exactly what you send (same parameter order, same percent-encoding)
X-Timestamp, X-NonceExactly the header strings
hex(SHA-256(body))SHA-256 of the body bytes you send (UTF-8), lower-case hex. With no body it is the hash of the empty string: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Line breaks\n (LF) only, and no newline after the last line

GET requests

The server ignores the body of GET requests, so their body hash is always the hash of the empty string. GET parameters all go in the query string, which is part of PATH_AND_QUERY.

Worked example ​

With this (example-only) key and input:

ItemValue
Key IDek_s_1a_XXXXXXXXXXXXXXXX
Secretes_0123456789abcdefghijABCDEFGHIJ0123456789
Method and pathPOST /api/tenant/v1/player/launch
X-Timestamp1790218800 (2026-09-24T03:00:00Z)
X-Noncen0nce8H2kQ9xYz4LmP0aBcDe
Body{"username":"alice","lang":"ENG","device":"mobile"}
  1. SHA-256 of the body:

    text
    a0c1cfba44962853266340b6610d542620041fae49e6bcb10e6446ca0f73ad6a
  2. String to sign (five lines):

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

    text
    6e4885c5d9f191b2e4ee5f4cb40ae148b13b277e606181bf853e040a06a0ac80

The complete request:

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"}

A GET example: GET /api/tenant/v1/wallet/balance?username=alice with the same timestamp, nonce n0nce8H2kQ9xYz4LmP0aBcDf and no body signs to 273b761bda9ca095a8c7ae22908844bc1ce728212fceda3d0b2129fc9bb03032.

Paste either example into the signature debugger to check your own work.

Code samples ​

Each sample has a sign function and a small request helper. Running the file prints the signature of the worked example above, 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"}`)))
}

What the server checks ​

OrderCheckOn failure
1Format of X-Api-Key401 UNAUTHORIZED (invalid credentials)
2Body at most 64 KB413 PAYLOAD_TOO_LARGE
3Key exists and is not revoked (a rotated key keeps working for 24 hours)401 UNAUTHORIZED (invalid credentials)
4X-Timestamp is well-formed and within 300 seconds of server time401 UNAUTHORIZED (timestamp outside the ±300 s window)
5X-Nonce format (16–64 letters and digits)401 UNAUTHORIZED (invalid credentials)
6Signature, compared in constant time401 UNAUTHORIZED (invalid credentials)
7Tenant is not disabled or suspended403 TENANT_SUSPENDED
8Source IP is on the allowlist (when you have one)403 IP_NOT_ALLOWED
9Tenant rate limit429 RATE_LIMITED
10Nonce not used in the last 10 minutes401 UNAUTHORIZED (nonce already used)
  • An unknown key and a bad signature return the same message, so keys cannot be probed.
  • A nonce is only recorded once every other check has passed, so requests rejected for a bad signature, rate limiting and so on do not use up their nonce. Even so, generate a new nonce and timestamp for every retry.
  • nonce already used means the request was not processed (for example a network layer retried the exact same request).

IP allowlist ​

Configure it in the Console under Go-live & integration → IP allowlist and Webhook: up to 100 IPv4 or IPv6 addresses or CIDR ranges (for example 203.0.113.10, 203.0.113.0/24, 2001:db8::/32). Empty means no restriction.

  • The check uses the source IP Cloudflare sees (CF-Connecting-IP), that is, your server's outbound IP.
  • If your server has both IPv4 and IPv6, add both, or pin your HTTP client to one of them.
  • The allowlist is set on your live tenant; sandbox keys are not restricted.

Common mistakes ​

SymptomCause and fix
Always invalid credentialsEnter the same values in the signature debugger and compare the string to sign line by line
Path prefix missingPATH_AND_QUERY starts with /api/tenant/v1, not the /player/launch relative to the base URL
GET signatures failThe query string was not signed, or the signed query differs from the sent one in encoding or order
POST signatures failThe signed body and the sent body are different strings: serialize once and use that string for both; do not let your HTTP library re-serialize
Non-English charactersHash the body as UTF-8 bytes (for example a Chinese nickname)
timestamp outside the ±300 s windowServer clock drift (enable NTP), or milliseconds sent instead of seconds
nonce already usedA retry reused the old nonce; every request needs a new one
Wrong secretThe secret includes the es_ prefix; the key ID and secret must be the same key, same environment
IP_NOT_ALLOWEDThe outbound IP is not on the allowlist, or the server switched to IPv6

elite Tenant Integration API v1