Python SDK Examples
This page provides a complete Python integration example for the Ruby Team API, including the HMAC-SHA256 signing utility, an HTTP client wrapper, and callback verification for Seamless Wallet integrations.
Dependencies: requests (install with pip install requests)
Signing Utility and HTTP Client
import hashlib
import hmac
import json
import time
from urllib.parse import urlparse
import requests
class RubyApiClient:
"""
HTTP client for the Ruby Team API with automatic HMAC-SHA256 signing.
Usage:
client = RubyApiClient(
base_url="https://api-test.ruby-gaming.com",
api_key="your_team_api_key",
api_secret="your_team_api_secret",
)
"""
def __init__(self, base_url: str, api_key: str, api_secret: str) -> None:
self.base_url = base_url.rstrip("/")
self.api_key = api_key
self.api_secret = api_secret
def _sign(self, method: str, path: str, body: str = "") -> tuple[int, str]:
"""
Compute the HMAC-SHA256 signature for a Team API request.
Signature string: {timestamp}{METHOD}{path}{raw_body}
- timestamp: Unix seconds (integer, converted to string)
- METHOD: Uppercase HTTP method (GET, POST, PUT, ...)
- path: Request path including query string if present
- raw_body: Raw request body string; empty string for no-body requests
Returns (timestamp, hex_signature).
"""
timestamp = int(time.time())
message = f"{timestamp}{method.upper()}{path}{body}"
signature = hmac.new(
self.api_secret.encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return timestamp, signature
def _build_headers(self, method: str, path: str, body: str = "") -> dict[str, str]:
"""Build the three required authentication headers plus Content-Type."""
timestamp, signature = self._sign(method, path, body)
return {
"X-Team-Key": self.api_key,
"X-Team-Timestamp": str(timestamp),
"X-Team-Signature": signature,
"Content-Type": "application/json",
}
def _path_from_url(self, url: str) -> str:
"""Extract path (with query string) from a full URL or a bare path."""
parsed = urlparse(url)
if parsed.netloc:
# Full URL — strip the scheme + host
path = parsed.path
if parsed.query:
path = f"{path}?{parsed.query}"
return path
# Already a bare path
return url
def get(self, path: str, params: dict | None = None) -> requests.Response:
"""Send an authenticated GET request."""
url = self.base_url + path
if params:
from urllib.parse import urlencode
url = f"{url}?{urlencode(params)}"
signed_path = self._path_from_url(url)
headers = self._build_headers("GET", signed_path, "")
return requests.get(url, headers=headers)
def post(self, path: str, body: dict) -> requests.Response:
"""Send an authenticated POST request with a JSON body."""
raw_body = json.dumps(body, separators=(",", ":"))
headers = self._build_headers("POST", path, raw_body)
return requests.post(
self.base_url + path,
headers=headers,
data=raw_body.encode("utf-8"),
)
def put(self, path: str, body: dict) -> requests.Response:
"""Send an authenticated PUT request with a JSON body."""
raw_body = json.dumps(body, separators=(",", ":"))
headers = self._build_headers("PUT", path, raw_body)
return requests.put(
self.base_url + path,
headers=headers,
data=raw_body.encode("utf-8"),
)
The body included in the signature must be byte-for-byte identical to the body sent in the HTTP request. The examples above use json.dumps(..., separators=(",", ":")) (no spaces after : or ,). Do not re-serialize or reformat the body after signing.
Example: Create a Brand
client = RubyApiClient(
base_url="https://api-test.ruby-gaming.com",
api_key="your_team_api_key",
api_secret="your_team_api_secret",
)
response = client.post(
"/api/brand/create",
body={
"name": "My Brand",
"code": "mybrand01",
"wallet_mode": "seamless",
"callback_url": "https://mybrand.example.com/ruby/callback",
"currency": "KRW",
},
)
data = response.json()
print(data)
# {
# "id": 42,
# "name": "My Brand",
# "code": "mybrand01",
# "api_key": "aBcDeFgH...",
# "api_secret": "xYzSeCrEt...", <-- save this, returned only once
# "wallet_mode": "seamless",
# "status": 1,
# ...
# }
brand_api_key = data["api_key"]
brand_api_secret = data["api_secret"] # Store securely — never returned again
api_secret immediatelyThe api_secret field is returned only in the create response. Store it in your secret manager before proceeding.
Example: Query Bet List
response = client.get(
"/api/bet/list",
params={
"page": 1,
"size": 20,
"brand_id": 42,
},
)
data = response.json()
print(f"Total bets: {data['total']}")
for bet in data["items"]:
print(bet)
Callback Signature Verification
If your brand uses wallet_mode: "seamless", Ruby will call your server for balance, debit, credit, and rollback operations. Each request is signed with an HMAC-SHA256 signature. You must verify it before processing the request.
Callback verification uses a different signing algorithm from the Team API. The HMAC input is body_bytes + timestamp_bytes (body first, timestamp at the end). The key is your brand.api_secret, not your team_api_secret.
import hashlib
import hmac
import time
def verify_callback_signature(
api_secret: str,
body_bytes: bytes,
timestamp: str,
signature: str,
max_age_seconds: int = 300,
) -> bool:
"""
Verify the HMAC-SHA256 signature on an incoming Ruby callback request.
Args:
api_secret: Your brand's api_secret (from the create brand response).
body_bytes: Raw HTTP request body bytes — do NOT parse before calling this.
timestamp: Value of the X-Aggregator-Timestamp header (string).
signature: Value of the X-Aggregator-Signature header (hex string).
max_age_seconds: Reject requests older than this many seconds (default 300).
Returns:
True if the signature is valid and the request is fresh, False otherwise.
"""
# Validate timestamp freshness
try:
ts = int(timestamp)
except ValueError:
return False
if abs(int(time.time()) - ts) > max_age_seconds:
return False
# Callback signing: HMAC-SHA256(key=api_secret, data=body_bytes + timestamp_bytes)
hmac_input = body_bytes + timestamp.encode("utf-8")
expected = hmac.new(
api_secret.encode("utf-8"),
hmac_input,
hashlib.sha256,
).hexdigest()
# Constant-time comparison to prevent timing attacks
return hmac.compare_digest(expected, signature)
Flask Integration Example
from flask import Flask, abort, request
app = Flask(__name__)
BRAND_API_KEY = "key_brandabc" # From create brand response
BRAND_API_SECRET = "my_brand_secret" # From create brand response
def _verify_or_abort():
"""Verify the callback signature; abort with 401 if invalid."""
raw_body = request.get_data()
valid = verify_callback_signature(
api_secret=BRAND_API_SECRET,
body_bytes=raw_body,
timestamp=request.headers.get("X-Aggregator-Timestamp", ""),
signature=request.headers.get("X-Aggregator-Signature", ""),
)
if not valid:
abort(401)
@app.route("/callback/balance", methods=["POST"])
def handle_balance():
# Read raw bytes BEFORE any parsing
_verify_or_abort()
# Return current player balance
return {"balance": "1250.00"}
@app.route("/callback/debit", methods=["POST"])
def handle_debit():
_verify_or_abort()
payload = request.get_json()
# Deduct from player wallet (must be idempotent on transaction_id)
return {"balance": "1149.50", "balance_before": "1250.00"}
@app.route("/callback/credit", methods=["POST"])
def handle_credit():
_verify_or_abort()
payload = request.get_json()
# Add winnings to player wallet (must be idempotent on transaction_id)
return {"balance": "1350.00", "balance_before": "1250.00"}
@app.route("/callback/rollback", methods=["POST"])
def handle_rollback():
_verify_or_abort()
payload = request.get_json()
# Reverse a previous debit (must be idempotent on transaction_id)
return {"balance": "1250.00"}
Common Mistakes
| Mistake | Result | Fix |
|---|---|---|
| Query string missing from path during signing | Signature mismatch (401) | Always include ?key=value in the path when signing GET requests |
| Re-serializing JSON body after signing | Signature mismatch (401) | Sign the exact bytes you send — do not reformat |
| Stale timestamp (>300 s drift) | Request rejected (401) | Sync system clock with NTP |
Using team_api_secret for callback verification | Verification fails | Callback uses brand.api_secret, not team_api_secret |
Parsing body before calling verify_callback_signature | Signature mismatch | Read raw bytes first, parse JSON only after verification |