Testing Callbacks
This guide helps you verify your Seamless Wallet callback implementation before going live. Follow the checklist and testing flow below to ensure your endpoints behave correctly.
Pre-Launch Checklist
Endpoint Availability
- All four endpoints are implemented:
/balance,/debit,/credit,/rollback - Endpoints are reachable over HTTPS from Ruby's test environment
- Your
callback_urlis registered in your Ruby brand configuration (no trailing slash) - Endpoints return
application/jsonresponses
Response Correctness
-
/balancereturns{ "balance": "<decimal string>" } -
/debitreturns{ "balance": "<decimal string>", "balance_before": "<decimal string>" } -
/creditreturns{ "balance": "<decimal string>", "balance_before": "<decimal string>" } -
/rollbackreturns{ "balance": "<decimal string>" }(nobalance_before) - All
balancevalues are returned as decimal strings (e.g.,"1250.00"), not numbers
Authentication
- Signature verification is implemented for all four endpoints
- Raw body bytes are captured before JSON parsing
-
X-Aggregator-Keyis validated against your configuredapi_key -
X-Aggregator-Timestampis checked within a 300-second window - Signature comparison uses constant-time function (not
==) - Requests with invalid signatures return
401 - Requests with expired timestamps return
401
Idempotency
- Sending the same
transaction_idtwice to/debitproduces the same response without re-processing - Sending the same
transaction_idtwice to/creditproduces the same response without re-crediting - Sending the same
transaction_idtwice to/rollbackproduces the same response without re-crediting -
/balancereturns the current live balance on every call (no caching)
Performance
- All endpoints respond in under 3 seconds under expected load
- Endpoints respond within 5 seconds even under peak load (hard cutoff)
- Load tested with concurrent requests to verify no race conditions in debit logic
Error Handling
- Insufficient funds on
/debitreturns a non-2xx status code - Unknown
player_idreturns a non-2xx status code - Internal errors return
5xx(not200with an error body)
Suggested Testing Flow
Phase 1 — Unit Testing
Before connecting to Ruby's test environment, verify your implementation with locally crafted requests.
1. Test signature verification
Craft a request manually and verify your implementation accepts a valid signature and rejects an invalid one:
import hashlib
import hmac
import json
import time
api_secret = "your_test_secret"
body = json.dumps({"player_id": 1, "username": "test"}).encode()
timestamp = int(time.time())
timestamp_str = str(timestamp)
sig = hmac.new(
api_secret.encode(),
body + timestamp_str.encode(),
hashlib.sha256,
).hexdigest()
print(f"X-Aggregator-Key: your_test_key")
print(f"X-Aggregator-Timestamp: {timestamp_str}")
print(f"X-Aggregator-Signature: {sig}")
print(f"Body: {body.decode()}")
Send this request to your local server and confirm it returns 200. Then modify the body or signature and confirm it returns 401.
2. Test idempotency
Send the same /debit request twice with identical transaction_id values. Verify:
- The balance is only decremented once
- Both calls return the same
balanceandbalance_beforevalues
3. Test edge cases
- Empty optional fields (send only required fields)
amountas"0.01"(minimum value)- Large amounts close to your balance limits
- Concurrent identical requests (race condition test)
Phase 2 — Integration Testing
Connect to Ruby's sandbox/test environment using test credentials provided by your Ruby account manager.
Typical test sequence:
- Call
/balance— verify you return the correct test player balance - Call
/debitwith a known amount — verifybalance_beforematches the/balanceresult - Call
/creditwith a smaller amount — verify balance increases correctly - Call
/rollback— verify balance is restored - Repeat
/debitwith the sametransaction_id— verify idempotent response and no double-debit - Call
/balanceagain — verify final balance is consistent
Phase 3 — Load and Resilience Testing
Before going live, verify your implementation under realistic traffic:
- Run concurrent debit+credit requests for the same player to verify no race conditions
- Simulate slow database responses to confirm your server stays under the 5-second timeout
- Verify your server returns
5xx(not hangs) when your database is temporarily unavailable
Common Errors
Signature Mismatch (401)
Symptoms: Ruby logs show callback failures with status 401; all four endpoints reject requests.
Checklist:
- Are you reading the raw body bytes before JSON-parsing? Re-serializing the JSON body (even with identical content) can change field ordering or whitespace.
- Are you using the correct secret? Callbacks use
brand.api_secret, notteam_api_secret. - Is the concatenation order correct? It must be
body_bytes + timestamp_bytes. The timestamp goes at the end, unlike Team API signing where it goes first. - Is your hex encoding lowercase? Compare against the
X-Aggregator-Signatureheader value directly.
Debugging tip: Log the exact bytes you feed into the HMAC computation and compare with a reference implementation. The Python example in Callback Authentication is a reliable reference.
Timeout Failures
Symptoms: Ruby reports callback timeout errors; game transactions fail for players.
Checklist:
- Does your database query for balance/transaction lookup have appropriate indexes?
- Are you performing any synchronous I/O that could block the response thread?
- Is your callback server deployed in a region with low latency to Ruby's servers? Ask your account manager about the expected source IP ranges.
- Is your server under memory or CPU pressure during these timeouts?
Debugging tip: Add per-request timing logs to your callback endpoints. Log the time spent in database queries separately from network time. If your database queries are fast but the overall response is slow, check for connection pool exhaustion.
Non-Idempotent Responses
Symptoms: Players report duplicate charges after a game round; balance discrepancies in reconciliation reports.
Root cause: Your server processed the same transaction_id twice and applied the debit/credit/rollback a second time.
Checklist:
- Is
transaction_idstored in a table with a unique constraint? - Do you check for existing
transaction_idbefore applying the transaction? - Under concurrent load, can two requests with the same
transaction_idpass the duplicate check simultaneously? Use a database-level unique constraint (not application-level checks alone) to prevent this.
Fix pattern:
-- Example unique constraint
ALTER TABLE wallet_transactions
ADD CONSTRAINT uq_transaction_id UNIQUE (transaction_id, action);
Then in your handler:
- Try to insert the transaction record
- If you get a unique constraint violation, look up the existing record and return its saved response
- Never re-apply the wallet operation on a duplicate
Incorrect Response Shape
Symptoms: Ruby logs show a successful HTTP response but the game round behaves incorrectly (wrong balance displayed, round not completing).
Checklist:
- Are
balancevalues returned as strings (e.g.,"1250.00"), not JSON numbers (1250.00)? Ruby parses them viaDecimal(str(...)), so a JSON number will still work, but string is the expected format. - Does
/rollbackreturn only{ "balance": "..." }— withoutbalance_before? Extra fields are ignored but confirm your implementation matches the spec. - Does
/debitand/creditreturn bothbalanceandbalance_before? A missingbalance_beforewill be treated as"0".
Clock Skew (Timestamp Validation Failures)
Symptoms: All callbacks fail with 401 immediately after deployment; intermittent 401s under normal operation.
Cause: Your server clock is more than 300 seconds out of sync with Ruby's servers.
Fix: Ensure NTP is running and your server time is synchronized. The 300-second window is generous for any properly configured server.
Debugging Tips
Log raw headers and body
During development, log all three X-Aggregator-* headers and the raw body bytes for every incoming callback request. This makes it easy to reproduce issues locally.
Replay failed requests locally
If you receive a callback that fails signature verification, log the exact headers and raw body. You can then replay the request locally against your verification code to debug the mismatch.
Use a reference implementation to generate test requests
The Python code snippet in Phase 1 above generates correctly signed test requests. Use it to send requests directly to your local server to isolate whether the issue is in your signature verification or your business logic.
Check for middleware that modifies the body
Some web frameworks have body-parsing middleware that decodes and re-encodes JSON before your handler sees it. Make sure you capture the raw bytes at the outermost layer, before any middleware transforms the body. Refer to the framework-specific notes in Callback Authentication.
Verify decimal handling
Parse amount and return balance values using a decimal library, not floating-point. For example, "100.10" represented as a float may be 100.09999999999999 internally. Use Python's Decimal, Java's BigDecimal, or Node.js's decimal.js/big.js.