Callback Methods
This page documents every S2S callback method. Each method includes the request and response JSON, field descriptions, and behavioral notes. Your callback endpoint must handle all eight methods.
$1.00 = 100. All requests are POST to your callback_url with a JWT in the Authorization header.PING
Health check callback. Predictu sends this during operator onboarding, periodic monitoring, and when an admin clicks "Test Connection" in the God Mode dashboard. No player context - just verify connectivity and JWT validation.
Request
{
"method": "PING",
"timestamp": "2026-03-19T14:30:00.000Z",
"request_id": "d4e5f6a7-b8c9-0123-def0-456789abcdef",
"operator_id": "op_abc123",
"params": {}
}Response
{
"status": "OK"
}The params object is intentionally empty. Your handler should verify the JWT signature, confirm the operator_id is recognized, and return OK. No balance or transaction ID is needed.
BALANCE
Fetch a player's current wallet balance. Called before every trade to validate sufficient funds, and whenever the UI needs to display an up-to-date balance. This callback has no side effects - it is a read-only query.
Request
{
"method": "BALANCE",
"timestamp": "2026-03-19T14:30:00.000Z",
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"operator_id": "op_abc123",
"params": {
"player_id": "player_456",
"currency": "USD"
}
}Params
| Field | Type | Description |
|---|---|---|
player_id | string | The operator's external player identifier. This is the external_user_id from the embed initialization. |
currency | string | Currency code. Currently always "USD". |
Response
{
"status": "OK",
"balance": 1000000
}Return the player's current balance in subunits. If the player_id is not found, you may either return PLAYER_NOT_FOUND or auto-create the player with a default balance (the reference implementation auto-creates with 10,000.00 = 1,000,000 subunits).
BET_MAKE
Debit a player's wallet for a new prediction market bet. This is the most critical callback - it must be atomic and idempotent. If the debit succeeds, Predictu records the trade and updates the player's position. If the debit fails, no trade is recorded.
Request
{
"method": "BET_MAKE",
"timestamp": "2026-03-19T14:30:00.000Z",
"request_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
"operator_id": "op_abc123",
"params": {
"player_id": "player_456",
"transaction_id": "tx_pred_001",
"amount": 5200,
"currency": "USD",
"bet": {
"market_id": "mkt_btc_100k",
"market_title": "Will Bitcoin exceed $100k by June 2026?",
"outcome": "yes",
"odds": 1.92,
"shares": 100,
"execution_price": 52
}
}
}Params
| Field | Type | Description |
|---|---|---|
player_id | string | The operator's external player identifier. |
transaction_id | string | Predictu's unique identifier for this specific transaction. Different from request_id (the idempotency key). |
amount | number | Amount to debit in subunits. Example: 5200 = $52.00. |
currency | string | Currency code. Currently always "USD". |
Bet Object
The bet object provides full context about the prediction market bet. This is informational for the operator's records and reporting - the debit amount is what matters for the wallet operation.
| Field | Type | Description |
|---|---|---|
market_id | string | Unique market identifier in Predictu's system. |
market_title | string | Human-readable market question. Example: "Will Bitcoin exceed $100k by June 2026?" |
outcome | "yes" | "no" | Which outcome the player is betting on. |
odds | number | Decimal odds at time of execution. Example: 1.92 means a $52 bet pays $100 if correct. |
shares | number | Number of shares purchased. Each share pays $1.00 (100 subunits) if the outcome is correct. |
execution_price | number | Price per share in cents (0–100). This is the spread-adjusted price, not the mid price. Example: 52 means 52c per share. |
Response
// Success
{
"status": "OK",
"balance": 948000,
"transaction_id": "op-tx-bet-001"
}
// Insufficient funds
{
"status": "INSUFFICIENT_FUNDS",
"balance": 3200,
"error_message": "Player balance 3200 < debit amount 5200"
}
// Player not found
{
"status": "PLAYER_NOT_FOUND",
"error_message": "No player with ID player_456"
}BET_MAKE with a request_id you have already successfully processed, return DUPLICATE_TRANSACTION with the cached balance and transaction ID. Do not debit the player again.BET_WIN
Credit a player's wallet after their bet wins. Sent when a market resolves and the player's chosen outcome is correct. The credit amount equals shares × 100subunits ($1.00 per share).
Request
{
"method": "BET_WIN",
"timestamp": "2026-03-19T14:30:00.000Z",
"request_id": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"operator_id": "op_abc123",
"params": {
"player_id": "player_456",
"transaction_id": "tx_pred_002",
"parent_transaction_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
"amount": 10000,
"currency": "USD",
"settlement": {
"market_id": "mkt_btc_100k",
"market_title": "Will Bitcoin exceed $100k by June 2026?",
"winning_outcome": "yes",
"shares": 100,
"cost_basis": 5200,
"profit": 4800
}
}
}Params
| Field | Type | Description |
|---|---|---|
player_id | string | The operator's external player identifier. |
transaction_id | string | Predictu's unique transaction ID for this settlement credit. |
parent_transaction_id | string | The request_id of the original BET_MAKE callback. Links this settlement to the originating bet. |
amount | number | Amount to credit in subunits. Equal to shares × 100. Example: 100 shares = 10,000 subunits ($100.00). |
currency | string | Currency code. |
Settlement Object
| Field | Type | Description |
|---|---|---|
market_id | string | Market that was resolved. |
market_title | string | Human-readable market question. |
winning_outcome | "yes" | "no" | The outcome that won. |
shares | number | Number of shares the player held. |
cost_basis | number | What the player originally paid for these shares, in subunits. |
profit | number | Net profit in subunits. Equal to amount - cost_basis. Example: 10000 - 5200 = 4800 ($48.00 profit). |
Response
{
"status": "OK",
"balance": 1048000,
"transaction_id": "op-tx-win-001"
}BET_LOST
Notification that a player's bet has lost after market resolution. This callback has no money movement - the amount is always 0. It exists purely for bookkeeping so the operator can close the bet in their records and update reporting.
Request
{
"method": "BET_LOST",
"timestamp": "2026-03-19T14:30:00.000Z",
"request_id": "d4e5f6a7-b8c9-0123-def0-456789abcdef",
"operator_id": "op_abc123",
"params": {
"player_id": "player_456",
"transaction_id": "tx_pred_003",
"parent_transaction_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
"amount": 0,
"currency": "USD",
"settlement": {
"market_id": "mkt_btc_100k",
"market_title": "Will Bitcoin exceed $100k by June 2026?",
"winning_outcome": "no",
"player_outcome": "yes",
"shares": 100,
"cost_basis": 5200
}
}
}Settlement Object
| Field | Type | Description |
|---|---|---|
market_id | string | Market that was resolved. |
market_title | string | Human-readable market question. |
winning_outcome | "yes" | "no" | The outcome that won. |
player_outcome | "yes" | "no" | The outcome the player bet on (which lost). |
shares | number | Number of shares the player held (now worthless). |
cost_basis | number | What the player originally paid, in subunits. This is the player's total loss. |
Response
{
"status": "OK",
"balance": 948000,
"transaction_id": "op-tx-loss-001"
}BET_MAKE. Just record the loss in your transaction log and return the current balance.BET_REFUND
Fully refund a player's bet. Triggered when a market is voided, cancelled by an admin, or determined to be invalid. The refund amount equals the original BET_MAKEdebit - the player is made completely whole.
Request
{
"method": "BET_REFUND",
"timestamp": "2026-03-19T14:30:00.000Z",
"request_id": "e5f6a7b8-c9d0-1234-ef01-567890abcdef",
"operator_id": "op_abc123",
"params": {
"player_id": "player_456",
"transaction_id": "tx_pred_004",
"parent_transaction_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
"amount": 5200,
"currency": "USD",
"reason": "Market voided: ambiguous resolution criteria"
}
}Params
| Field | Type | Description |
|---|---|---|
player_id | string | The operator's external player identifier. |
transaction_id | string | Predictu's unique transaction ID for this refund. |
parent_transaction_id | string | The request_id of the original BET_MAKE being refunded. |
amount | number | Full cost basis to credit back, in subunits. Equal to the original BET_MAKE amount. |
currency | string | Currency code. |
reason | string | Human-readable reason for the refund. Useful for audit trails and customer support. |
Response
{
"status": "OK",
"balance": 1000000,
"transaction_id": "op-tx-refund-001"
}BET_ROLLBACK
Reverse a BET_MAKE debit that succeeded at the operator but failed on Predictu's side (e.g., the trade recording or position update failed after the debit). This is the compensating transaction that ensures atomicity across the distributed system.
BET_ROLLBACK, it means the debit was successful but the trade could not be completed. You must credit the amount back. Failing to process a rollback leaves the player's balance permanently incorrect.Request
{
"method": "BET_ROLLBACK",
"timestamp": "2026-03-19T14:30:01.000Z",
"request_id": "f6a7b8c9-d0e1-2345-f012-678901abcdef",
"operator_id": "op_abc123",
"params": {
"player_id": "player_456",
"transaction_id": "tx_pred_005",
"parent_transaction_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
"amount": 5200,
"currency": "USD",
"reason": "Buy failed after debit - Failed to record trade: unique constraint violation"
}
}Params
| Field | Type | Description |
|---|---|---|
player_id | string | The operator's external player identifier. |
transaction_id | string | Predictu's unique transaction ID for this rollback. |
parent_transaction_id | string | The request_id of the BET_MAKE being reversed. |
amount | number | Amount to credit back, in subunits. |
currency | string | Currency code. |
reason | string | Technical reason for the rollback. Includes the downstream error message. |
Response
{
"status": "OK",
"balance": 1000000,
"transaction_id": "op-tx-rollback-001"
}BET_SELL
Credit a player for selling a position before market resolution. Unlike BET_WIN, the credit amount is based on the current market price (minus spread), not the full share value. The player may realize a profit or a loss depending on price movement.
Request
{
"method": "BET_SELL",
"timestamp": "2026-03-19T14:30:00.000Z",
"request_id": "a7b8c9d0-e1f2-3456-0123-789012abcdef",
"operator_id": "op_abc123",
"params": {
"player_id": "player_456",
"transaction_id": "tx_pred_006",
"parent_transaction_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
"amount": 6340,
"currency": "USD",
"sale": {
"market_id": "mkt_btc_100k",
"market_title": "Will Bitcoin exceed $100k by June 2026?",
"outcome": "yes",
"shares_sold": 100,
"execution_price": 65,
"realized_pnl": 1140
}
}
}Sale Object
| Field | Type | Description |
|---|---|---|
market_id | string | Market being sold from. |
market_title | string | Human-readable market question. |
outcome | "yes" | "no" | The outcome the player held and is selling. |
shares_sold | number | Number of shares being sold. |
execution_price | number | Sell price per share in cents (0–100). This is the bid price (mid minus spread). |
realized_pnl | number | Realized profit/loss in subunits. Positive = profit, negative = loss. Calculated as (execution_price - avg_buy_price) × shares_sold. |
Response
{
"status": "OK",
"balance": 1006340,
"transaction_id": "op-tx-sell-001"
}P&L Examples
| Scenario | Buy Price | Sell Price | Shares | Cost | Proceeds | Realized P&L |
|---|---|---|---|---|---|---|
| Price went up | 52c | 65c | 100 | $52.00 | $63.40 * | +$11.40 |
| Price went down | 52c | 38c | 100 | $52.00 | $36.48 * | -$15.52 |
| Break even | 52c | 52c | 100 | $52.00 | $50.96 * | -$1.04 |
* Sell proceeds include spread. Even at the same mid price, the sell price (bid) is lower than the buy price (ask) due to spread, so the player realizes a small loss on an immediate round-trip.
Idempotency & Duplicate Handling
Every S2S request includes a unique request_id (UUID v4). This serves as the idempotency key. Your callback handler must implement duplicate detection.
Implementation Flow
request_id from the request body.request_id in your transaction table (e.g., SELECT * FROM transactions WHERE predictu_request_id = ?).DUPLICATE_TRANSACTION with the player's current balance and the cached transaction_id. Do not re-process the wallet operation.request_id, and return OK.// Idempotency check (reference implementation)
const existingTx = findTransactionByRequestId(request_id);
if (existingTx) {
console.log("Duplicate request_id, returning cached result");
const player = getPlayer(params.player_id);
return {
status: "DUPLICATE_TRANSACTION",
balance: player.balance,
transaction_id: existingTx.id,
};
}request_id. If the first attempt succeeded but Predictu didn't receive the response (e.g., network timeout), the retry will carry the same request_id. Without idempotency, the player would be double-debited.Error Handling Best Practices
Operator Side
- Always return valid JSON, even for errors. Predictu parses the response as JSON.
- Use the correct status codes:
INSUFFICIENT_FUNDSfor balance issues,PLAYER_NOT_FOUNDfor missing players,ERRORfor everything else. - Include
balancein every response when possible, even error responses. This helps Predictu keep the UI in sync. - Log every callback with the
request_idfor troubleshooting. - Implement a transaction table with
predictu_request_idas a unique index for idempotency.
Predictu Side
OKandDUPLICATE_TRANSACTIONare treated as success - the trade proceeds.INSUFFICIENT_FUNDSandPLAYER_NOT_FOUNDare non-retryable business errors.ERRORstatus and HTTP failures (timeouts, 5xx) trigger exponential backoff retries.- Non-JSON responses are treated as
ERROR. - After all retry attempts are exhausted, the transaction is marked as
failed.
Method Summary Reference
| Method | Wallet Op | Has Parent | Has Context Object | Retryable |
|---|---|---|---|---|
PING | None | No | No | Yes |
BALANCE | Read | No | No | Yes |
BET_MAKE | Debit | No | bet | Yes * |
BET_WIN | Credit | Yes | settlement | Yes |
BET_LOST | None | Yes | settlement | Yes |
BET_REFUND | Credit | Yes | No (reason string) | Yes |
BET_ROLLBACK | Credit | Yes | No (reason string) | Yes |
BET_SELL | Credit | Yes | sale | Yes |
* BET_MAKE retries use the same request_id, so idempotent duplicate detection prevents double-debits.
