Seamless wallet
TALOS does not store the player balance. On every balance query and every transaction it sends a callback to your walletUrl, and your reply is the source of truth. The full specification is in Wallet callback.
Request#
POST to your walletUrl, with Content-Type: application/json and the signed headers (see Validating calls from TALOS).
| Field | Present in | Description |
|---|---|---|
action | all | balance, bet, win, refund or rollback. |
player_id | all | The player_id you sent at launch. |
currency | all | Merchant currency. |
provider | all | Game provider code (SPORTSBOOK for the sportsbook). |
game | all | Game code. |
tx_id | transactions | Unique transaction id. Idempotency key. |
round_id | transactions | Round (or bet, in the sportsbook) the transaction belongs to. |
amount | transactions | Amount, always positive, in the major currency unit (e.g. 2.5 = US$ 2.50; up to 2 decimals; CLP has none). |
ref_tx_id | refund, rollback | tx_id of the reverted transaction. |
Treat tx_id, round_id and ref_tx_id as opaque strings.
Actions#
| Action | Balance effect | What to do |
|---|---|---|
balance | none | Return the current balance. |
bet | debit of amount | Debit. Without funds, reject with INSUFFICIENT_FUNDS. |
win | credit of amount | Credit. A round without a win generates no win. |
refund | credit | Return the ref_tx_id bet (same amount). If you never applied that bet, reply TRANSACTION_NOT_FOUND. |
rollback | debit | Undo the ref_tx_id win (same amount). If you never applied that win, reply TRANSACTION_NOT_FOUND. |
The normal order of a round is bet → win. Some providers send the win before the bet; do not rely on the order, rely on the tx_id.
Response#
Always reply with JSON:
{ "ok": true, "balance": 97.5 }{ "ok": false, "error": "INSUFFICIENT_FUNDS", "balance": 1.2 }balanceis the balance after the transaction, in the major currency unit. It is required whenokistrue.- In rejections,
balanceis optional (it helps the provider show the right balance). - The
errorcode is read in uppercase. Use these when they apply:
error | When |
|---|---|
INSUFFICIENT_FUNDS | Not enough balance for the bet. |
PLAYER_NOT_FOUND | Unknown player_id. |
PLAYER_BLOCKED | Player blocked (self-exclusion, fraud, limit). |
TRANSACTION_NOT_FOUND | In refund/rollback: you never applied the ref_tx_id transaction. |
Any other code is also accepted and treated as a rejection.
Idempotency#
The same tx_id can arrive more than once — resends after a timeout, retries from the provider or the sportsbook. For each tx_id:
- The first time, apply it and store the result (success with the balance, or the rejection).
- The following times, do not apply it again: return the same result (with the current balance, if you prefer).
Do the check and the debit in one database transaction (e.g. tx_id with a unique index), so two simultaneous requests with the same tx_id never debit twice.
Never apply the same tx_id twice
TALOS reuses the same tx_id when resending the same transaction precisely so you can deduplicate. If your endpoint is not idempotent, a timeout followed by a resend debits the player twice.
Unknown result and resends#
TALOS considers the reply unavailable — the result on your side is unknown — when:
- there is no reply within 8 seconds (default timeout);
- the connection fails;
- the HTTP status is 5xx;
- the body is not JSON, has no boolean
ok, or hasok: truewithout a numericbalance.
In those cases the transaction stays pending. It is resolved by a resend with the same tx_id (when the provider or the sportsbook repeats the operation) or by a refund that references it. So, if you received and applied the bet but the reply did not arrive, the resend must return the same success; and if you never received the bet, its refund must reply TRANSACTION_NOT_FOUND.
Replies with an HTTP status below 500 are read from the body: a 401 or 400 with { "ok": false, "error": "..." } is a rejection, not an outage.
Final rejections#
- A rejected bet (
bet) or rollback is final: a resend of the sametx_idreturns the same rejection without calling you again. - A rejected credit (
win,refund) is resent when the provider repeats the operation. Avoid rejecting credits for business rules — the money belongs to the player. - The same
tx_idwith a different amount is rejected by TALOS (TRANSACTION_MISMATCH) without calling your wallet.
Sportsbook#
Sportsbook bets use the same wallet, with provider: "SPORTSBOOK" and game: "sportsbook". The round_id is the bet id.
bet— one per bet in the betslip, when the player places it. If one bet of the betslip is rejected, the bets of the same betslip that were already debited get arefund; if the reply to a bet never arrives (unknown result), it also gets arefund— replyTRANSACTION_NOT_FOUNDif you never applied it.win— on settlement with an amount to pay (won bet, partial return or voided bet). A lost bet generates nowin.refund— the reversal described above, withref_tx_id= the bettx_id.
The sportsbook does not send rollback today. refund and win without a final reply are resent with increasing backoff (1 s, 2 s, 4 s… up to 5 minutes between attempts), always with the same tx_id.
Endpoint example#
// app/api/talos/wallet/route.ts (Next.js) — sketch; adapt it to your database
import { verifyTalos } from './verify' // see Authentication
export async function POST(request: Request) {
const raw = await request.text()
if (!verifyTalos(request, raw)) {
return Response.json({ ok: false, error: 'INVALID_SIGNATURE' }, { status: 401 })
}
const cb = JSON.parse(raw)
if (cb.action === 'balance') {
const balance = await wallet.balanceOf(cb.player_id, cb.currency)
return Response.json({ ok: true, balance })
}
// idempotency: the same tx_id returns the stored result
const previous = await wallet.findMove(cb.tx_id)
if (previous) return Response.json(previous.reply)
const reply = await wallet.apply(cb) // debit/credit + store tx_id and reply in the same transaction
return Response.json(reply)
}<?php
// wallet.php — sketch; adapt it to your database
$raw = file_get_contents('php://input');
if (!verify_talos($raw, $_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'])) {
http_response_code(401);
exit(json_encode(['ok' => false, 'error' => 'INVALID_SIGNATURE']));
}
$cb = json_decode($raw, true);
header('Content-Type: application/json');
if ($cb['action'] === 'balance') {
exit(json_encode(['ok' => true, 'balance' => wallet_balance($cb['player_id'], $cb['currency'])]));
}
$previous = wallet_find_move($cb['tx_id']); // idempotency by tx_id
if ($previous) {
exit($previous['reply']);
}
echo json_encode(wallet_apply($cb)); // apply and store tx_id + reply in the same transactionReconciliation#
Every transaction sent to your wallet, with its status (success, pending, failed) and the returned error, is in GET /v1/transactions. Use it to reconcile with your ledger.