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.
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:
| Header | Value |
|---|---|
X-Team-Key | Your team_api_key issued by Ruby |
X-Team-Timestamp | Current Unix timestamp in seconds (e.g., 1711500000) |
X-Team-Signature | HMAC-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}
| Field | Details |
|---|---|
timestamp | The same Unix timestamp value sent in X-Team-Timestamp |
HTTP_METHOD | Uppercase HTTP method: GET, POST, PUT, etc. |
path | Path including query string if present (from Step 1) |
raw_body | The 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}
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
| Mistake | Result | Fix |
|---|---|---|
| Missing query string in path | Signature mismatch (401) | Always append ?query_string when present |
| Re-serializing JSON body before signing | Signature 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 case | Signature mismatch (401) | Always use uppercase method (GET, PUT, etc.) |
| Sending body for GET requests | Unexpected behavior | Use empty string "" for no-body requests |