Skip to main content

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_url is registered in your Ruby brand configuration (no trailing slash)
  • Endpoints return application/json responses

Response Correctness

  • /balance returns { "balance": "<decimal string>" }
  • /debit returns { "balance": "<decimal string>", "balance_before": "<decimal string>" }
  • /credit returns { "balance": "<decimal string>", "balance_before": "<decimal string>" }
  • /rollback returns { "balance": "<decimal string>" } (no balance_before)
  • All balance values 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-Key is validated against your configured api_key
  • X-Aggregator-Timestamp is 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_id twice to /debit produces the same response without re-processing
  • Sending the same transaction_id twice to /credit produces the same response without re-crediting
  • Sending the same transaction_id twice to /rollback produces the same response without re-crediting
  • /balance returns 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 /debit returns a non-2xx status code
  • Unknown player_id returns a non-2xx status code
  • Internal errors return 5xx (not 200 with 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 balance and balance_before values

3. Test edge cases

  • Empty optional fields (send only required fields)
  • amount as "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:

  1. Call /balance — verify you return the correct test player balance
  2. Call /debit with a known amount — verify balance_before matches the /balance result
  3. Call /credit with a smaller amount — verify balance increases correctly
  4. Call /rollback — verify balance is restored
  5. Repeat /debit with the same transaction_id — verify idempotent response and no double-debit
  6. Call /balance again — 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, not team_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-Signature header 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_id stored in a table with a unique constraint?
  • Do you check for existing transaction_id before applying the transaction?
  • Under concurrent load, can two requests with the same transaction_id pass 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 balance values returned as strings (e.g., "1250.00"), not JSON numbers (1250.00)? Ruby parses them via Decimal(str(...)), so a JSON number will still work, but string is the expected format.
  • Does /rollback return only { "balance": "..." } — without balance_before? Extra fields are ignored but confirm your implementation matches the spec.
  • Does /debit and /credit return both balance and balance_before? A missing balance_before will 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.