Skip to main content

HMAC-SHA256 Signing

All requests to the Ruby Team API must be authenticated using HMAC-SHA256 signatures. This page explains how to construct and sign each request.

note

This signing algorithm applies to Team API requests only. If you are implementing Seamless Wallet Callbacks, the callback signing algorithm is different — see Callback Authentication for details.


Required Headers

Every request must include these three headers:

HeaderValue
X-Team-KeyYour team_api_key issued by Ruby
X-Team-TimestampCurrent Unix timestamp in seconds (e.g., 1711500000)
X-Team-SignatureHMAC-SHA256 signature (hex-encoded)

Signature Construction

Step 1 — Build the path string

Use the request path. If the request has a query string, append it to the path:

path = "/api/brand/123"                        # no query string
path = "/api/bet/list?page=1&size=20" # with query string

Step 2 — Assemble the signature string

Concatenate four fields in this exact order, with no separators:

signature_string = {timestamp}{HTTP_METHOD}{path}{raw_body}
FieldDetails
timestampThe same Unix timestamp value sent in X-Team-Timestamp
HTTP_METHODUppercase HTTP method: GET, POST, PUT, etc.
pathPath including query string if present (from Step 1)
raw_bodyThe raw request body exactly as sent over the wire. For GET requests or requests with no body, use an empty string ""

Step 3 — Compute the signature

signature = HMAC-SHA256(key=team_api_secret, message=signature_string)
  • Key: team_api_secret (UTF-8 encoded bytes)
  • Message: the signature string (UTF-8 encoded bytes)
  • Output: lowercase hex digest

Worked Examples

Example 1: PUT request with a JSON body

Request:   PUT /api/brand/123
Body: {"status": 0}
Timestamp: 1711500000

Signature string:

1711500000PUT/api/brand/123{"status": 0}
Body must be byte-for-byte identical

The body included in the signature string must be character-for-character identical to the raw HTTP request body you send. In this example, {"status": 0} has a space after the colon. If your HTTP client serializes JSON without the space (i.e., {"status":0}), use that exact form in the signature string too. Never re-format or re-serialize the body just for signing.

Example 2: GET request with a query string

Request:   GET /api/bet/list?page=1&size=20
Timestamp: 1711500000

Signature string (no body — use empty string):

1711500000GET/api/bet/list?page=1&size=20

Timestamp Tolerance

The server accepts requests where the timestamp is within ±300 seconds of server time. Requests outside this window are rejected with a 401 Unauthorized error. Ensure your system clock is synchronized (see Security Best Practices).


Code Examples

Python

import hashlib
import hmac
import time
import requests

def build_signature(
api_secret: str,
timestamp: int,
method: str,
path: str,
body: str = "",
) -> str:
"""Compute the HMAC-SHA256 signature for a Ruby Team API request."""
message = f"{timestamp}{method.upper()}{path}{body}"
return hmac.new(
api_secret.encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256,
).hexdigest()


def make_request(api_key: str, api_secret: str, method: str, url: str, body: str = ""):
"""Send an authenticated request to the Ruby Team API."""
timestamp = int(time.time())

# Parse path (with query string) from full URL
from urllib.parse import urlparse
parsed = urlparse(url)
path = parsed.path
if parsed.query:
path = f"{path}?{parsed.query}"

signature = build_signature(api_secret, timestamp, method, path, body)

headers = {
"X-Team-Key": api_key,
"X-Team-Timestamp": str(timestamp),
"X-Team-Signature": signature,
"Content-Type": "application/json",
}
return requests.request(method, url, headers=headers, data=body.encode("utf-8") if body else None)


# Example: update brand status
body = '{"status": 0}'
response = make_request(
api_key="your_team_api_key",
api_secret="your_team_api_secret",
method="PUT",
url="https://api.ruby.example.com/api/brand/123",
body=body,
)
print(response.json())

Node.js

const crypto = require("crypto");
const https = require("https");
const { URL } = require("url");

function buildSignature(apiSecret, timestamp, method, path, body = "") {
const message = `${timestamp}${method.toUpperCase()}${path}${body}`;
return crypto
.createHmac("sha256", apiSecret)
.update(message, "utf8")
.digest("hex");
}

function makeRequest(apiKey, apiSecret, method, urlString, body = "") {
const timestamp = Math.floor(Date.now() / 1000);
const url = new URL(urlString);
const path = url.search ? `${url.pathname}${url.search}` : url.pathname;

const signature = buildSignature(apiSecret, timestamp, method, path, body);

const options = {
hostname: url.hostname,
path: `${url.pathname}${url.search}`,
method: method.toUpperCase(),
headers: {
"X-Team-Key": apiKey,
"X-Team-Timestamp": String(timestamp),
"X-Team-Signature": signature,
"Content-Type": "application/json",
},
};

return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => resolve(JSON.parse(data)));
});
req.on("error", reject);
if (body) req.write(body);
req.end();
});
}

// Example: list bets
makeRequest(
"your_team_api_key",
"your_team_api_secret",
"GET",
"https://api.ruby.example.com/api/bet/list?page=1&size=20"
).then(console.log);

Java

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
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.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.HexFormat;

public class RubyApiClient {

private final String apiKey;
private final String apiSecret;
private final HttpClient httpClient = HttpClient.newHttpClient();

public RubyApiClient(String apiKey, String apiSecret) {
this.apiKey = apiKey;
this.apiSecret = apiSecret;
}

public String buildSignature(long timestamp, String method, String path, String body)
throws NoSuchAlgorithmException, InvalidKeyException {
String message = timestamp + method.toUpperCase() + path + (body != null ? body : "");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] raw = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(raw);
}

public HttpResponse<String> makeRequest(String method, String urlString, String body)
throws Exception {
long timestamp = Instant.now().getEpochSecond();
URI uri = URI.create(urlString);
String path = uri.getRawQuery() != null
? uri.getRawPath() + "?" + uri.getRawQuery()
: uri.getRawPath();

String signature = buildSignature(timestamp, method, path, body);

HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(uri)
.header("X-Team-Key", apiKey)
.header("X-Team-Timestamp", String.valueOf(timestamp))
.header("X-Team-Signature", signature)
.header("Content-Type", "application/json");

if (body != null && !body.isEmpty()) {
builder.method(method.toUpperCase(),
HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8));
} else {
builder.method(method.toUpperCase(), HttpRequest.BodyPublishers.noBody());
}

return httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
}

// Example usage
public static void main(String[] args) throws Exception {
RubyApiClient client = new RubyApiClient("your_team_api_key", "your_team_api_secret");

// PUT with body
String body = "{\"status\": 0}";
HttpResponse<String> response = client.makeRequest(
"PUT",
"https://api.ruby.example.com/api/brand/123",
body
);
System.out.println(response.body());
}
}

Common Mistakes

MistakeResultFix
Missing query string in pathSignature mismatch (401)Always append ?query_string when present
Re-serializing JSON body before signingSignature mismatch (401)Use the exact raw body bytes you send in the request
Stale timestamp (>300s drift)Timestamp out of range (401)Sync system clock with NTP
Signing with wrong method caseSignature mismatch (401)Always use uppercase method (GET, PUT, etc.)
Sending body for GET requestsUnexpected behaviorUse empty string "" for no-body requests