Bet Data Push Callback
VCT Platform batch-pushes bet, settlement, and correction transactions to the Agent (Operator).
Note
This API is called by the VCT Platform to the Agent (Operator) to push or batch-push wager logs and report data.
NOTE (Important for Transfer Wallet mode): In Seamless (real-time wallet) mode, this API is the only channel for the Agent to process game balance changes. Bet placements (BET), settlements (SETTLED), bonuses (BONUS), and cancellations/rollbacks (CANCEL/ROLLBACK/REFUND/VOID) are pushed here. The Agent must apply idempotent real-time debits, credits, or reversals to the player's master wallet according to status.
Environment URLs
| Environment | Base URL |
|---|---|
| Production | https://hub-panel-api.v7stech.com |
| Staging | https://hub-panel-api-dev.v7stech.com |
Endpoint
| Property | Value |
|---|---|
| Path | POST /api/v1/callback/bet-push |
| Caller | VCT Platform → Agent (Operator) |
Authentication and Signature
See Authentication for the common Header Token rule. Because this endpoint contains batched wagers, its Body sign is HMAC-SHA256(agent_api_key + "." + timestamp, agent_api_secret); the Header and Body timestamps must match exactly.
Request Parameters (from VCT)
| Parameter | Type | Required | Description |
|---|---|---|---|
operator_code | string | Yes | Operator code |
wagers | array | Yes | Wager list |
wagers[].username | string | Yes | Raw player username (without VCT platform agent prefix) |
wagers[].currency | string | Yes | Currency code (e.g., MYR, VND, USD, used to locate the player's currency-specific wallet) |
wagers[].session_id | string | Yes | VCT session ID; use it to associate every wager with the matching game launch and offline reclaim |
wagers[].bet_id | string | Yes | Bet ID |
wagers[].brand_guid | string | Yes | Stable Brand ID, matching /papi/v1/game/list and /papi/v1/game/providers; use for Brand relationships |
wagers[].brand_code | string | Yes | Current t_brand.brand_code; not a globally unique key |
wagers[].brand_name | string | Yes | Current t_brand.brand_name, for display |
wagers[].game_code | string | Yes | Game code |
wagers[].game_type | string | Yes | Provider-native type retained for compatibility; not the commission genre |
wagers[].game_genre | integer | Yes | Numeric catalogue genre; 0 when the game cannot be identified uniquely |
wagers[].game_genre_name | string | Yes | 2 Slot, 3 Fish, 4 Sport, 5 Card, 6 Live, 7 Other; empty for 0 |
wagers[].game_name | string | Yes | Game name |
wagers[].bet_amount | decimal | Yes | Bet debit included in this transaction |
wagers[].valid_bet_amount | decimal | Yes | Valid bet amount |
wagers[].prize_amount | decimal | Yes | Payout, bonus, or refund credit included in this transaction |
wagers[].win_loss | decimal | Yes | Net amount for this transaction, normally prize_amount - bet_amount; it may not be the final round P/L |
wagers[].status | string | Yes | Wager status/action: BET / SETTLED / CANCEL / VOID / ROLLBACK / REFUND / BONUS |
wagers[].bet_time | string | Yes | Bet time |
wagers[].settle_time | string | Yes | Settlement time |
wagers[].round_id | string | No | Unified round/game ID. VCT maps provider-specific identifiers to this field so operators can associate BET, SETTLED, and corrections for the same player, currency, and session. Do not use round_id alone as the correlation key. |
wagers[].original_transaction_id | string | No | Original settlement/bet transaction ID; recommended for status=BONUS or correction records (e.g., CANCEL) |
sign | string | Yes | Signature |
timestamp | long | Yes | Timestamp |
Brand Identity
Each wager includes brand_guid, brand_code, and brand_name from the same valid t_brand record. Provider and product data locate the linked Brand GUID; product codes and names are not global identity keys. Distinct GUIDs must remain separate even when codes or names match. Changing a Brand code or name preserves its GUID; deleting and recreating a Brand changes the GUID.
A product may identify a Brand even without a specific game/table, so game_code and game_name may be empty. Missing, deleted, ambiguous, or wrongly owned Brand mappings are not sent as guessed identities: Hub records a delivery failure for correction. Existing delivered wagers are not backfilled or resent automatically. This addition requires the updated Hub backend to be deployed.
game_genre comes from catalogue games matching Platform, Product, and Game Code; a product-only match is classified only if every matching game has the same genre. Mixed or unmatched products and unclassified games return 0 and an empty name. Never infer it from provider-native game_type; hold genre-0 wagers for reconciliation until the settlement policy is agreed.
/game/rates accepts exact brand_guid filters. Query with the wager GUID and original business date in as_of_date; the legacy brand request field searches current master Code or Name and is not unique.
Response Fields
| Field | Type | Description |
|---|---|---|
received | int | Number of records received |
status | string | Processing status |
Amounts and Player Win/Loss
bet_amount is the bet debit included in the current record. prize_amount is the payout, bonus, or refund credit included in the current record.
For normal betting and settlement records:
Net amount for this record = prize_amount - bet_amountA positive value increases the player's balance, a negative value decreases it, and zero means there is no net balance change in this record. To calculate the final P/L for a round, aggregate normal records for the same player, currency, session_id, and round_id:
Round player P/L = sum(prize_amount) - sum(bet_amount)BET and SETTLED as separate records
Some providers send the debit and payout separately:
| status | bet_amount | prize_amount | Net amount |
|---|---|---|---|
BET | 100 | 0 | -100 |
SETTLED | 0 | 200 | +200 |
For the same round, the player's P/L is 200 - 100 = +100. Do not treat the SETTLED row by itself as the final round P/L.
Bet and payout combined in one SETTLED record
Some providers send one SETTLED record containing both amounts:
| status | bet_amount | prize_amount | Round player P/L |
|---|---|---|---|
SETTLED | 100 | 200 | +100 |
In this format, calculate prize_amount - bet_amount directly. Providers may split rows differently, but the meanings of the two amount fields remain the same.
Status and Balance-Processing Rules
| status | Meaning | Common amount shape | Agent processing |
|---|---|---|---|
BET | Bet debit | bet_amount > 0, prize_amount = 0 | Debit bet_amount |
SETTLED | Normal settlement | May contain only prize_amount, or both amounts | Debit the included bet_amount and credit prize_amount; if both are present, process them atomically |
BONUS | Extra bonus or independent payout | Usually bet_amount = 0, only prize_amount | Credit prize_amount and classify it separately |
REFUND | Refund | Usually only prize_amount | Credit the refund and mark the original transaction as refunded |
CANCEL / ROLLBACK / VOID | Cancellation, rollback, or void | Provider-dependent; both amounts may be zero | Idempotently reverse the original transaction using original_transaction_id |
action is the internal Hub-System name; in this callback, the Agent processes the same meaning through wagers[].status.
Correction records
CANCEL, ROLLBACK, VOID, and REFUND are not new normal wins or losses. The Agent must locate the original transaction with original_transaction_id, check whether it was already reversed, and apply the correction exactly once.
Some providers include the original bet in bet_amount as a reference on a correction record. This does not mean the Agent should debit it again. Money returned to the player is represented by prize_amount. If both amounts are zero, the correction changes reporting state but not the balance. Do not use prize_amount - bet_amount alone to determine the final P/L of a correction.
Example Request
Settled Wager SETTLED
{
"operator_code": "AG",
"wagers": [
{
"username": "player001",
"currency": "MYR",
"session_id": "vct-session-uuid-001",
"bet_id": "settle-20260706-0001",
"brand_guid": "30ff77f5-0616-4ecc-a8a3-80e35fcaaf3e",
"brand_code": "1007",
"brand_name": "PG Soft",
"game_code": "PG-SLOT-001",
"game_type": "SLOT",
"game_genre": 2,
"game_genre_name": "Slot",
"game_name": "Fortune Tiger",
"bet_amount": 100.0,
"valid_bet_amount": 100.0,
"prize_amount": 200.0,
"win_loss": 100.0,
"status": "SETTLED",
"bet_time": "2026-07-06T10:00:00+08:00",
"settle_time": "2026-07-06T10:01:00+08:00",
"round_id": "round-10001"
}
],
"sign": "demo-sign",
"timestamp": 1783325100
}Extra Bonus Record BONUS
{
"operator_code": "AG",
"wagers": [
{
"username": "player001",
"currency": "MYR",
"session_id": "vct-session-uuid-001",
"bet_id": "bonus-20260706-0001",
"brand_guid": "30ff77f5-0616-4ecc-a8a3-80e35fcaaf3e",
"brand_code": "1007",
"brand_name": "PG Soft",
"game_code": "PG-SLOT-001",
"game_type": "SLOT",
"game_genre": 2,
"game_genre_name": "Slot",
"game_name": "Fortune Tiger",
"bet_amount": 0.0,
"valid_bet_amount": 0.0,
"prize_amount": 30.0,
"win_loss": 30.0,
"status": "BONUS",
"bet_time": "2026-07-06T10:01:00+08:00",
"settle_time": "2026-07-06T10:01:10+08:00",
"round_id": "round-10001",
"original_transaction_id": "settle-20260706-0001"
}
],
"sign": "demo-sign",
"timestamp": 1783325160
}Example Response
{
"received": 1,
"status": "success"
}
```## Push Logic & Processing Recommendations
### 1. Real-Time Wallet Processing (CRITICAL)
* **Apply Balance Changes**: Process `BET` as a debit of `bet_amount`. For `SETTLED`, debit any included `bet_amount` and credit `prize_amount` atomically; when `bet_amount` is zero, only credit `prize_amount`. Credit `BONUS` separately. `CANCEL` / `ROLLBACK` / `REFUND` / `VOID` must idempotently correct the original transaction identified by `original_transaction_id`.
* `/offline` closes the session and reconciles the final balance. It must not apply a second debit or credit for transactions already processed through `/bet-push`.
### 2. Idempotency Check
* Use a unique transaction key (at minimum `wagers[].bet_id`, combined with `status` and `original_transaction_id` where applicable) to prevent duplicate debits, credits, or reversals caused by retries.
### 3. Wager Status and Correction Flow
* **Normal Statuses**: `BET` (raw bet placement), `SETTLED` (normal settled wager), `BONUS` (extra payout or independent win).
* **Correction/Rollback Statuses**: `CANCEL`, `ROLLBACK`, `REFUND`, `VOID`.
* **Correction Flow Handling**: When the wager status is a rollback/correction status, VCT provides `original_transaction_id`. The Agent must locate the original wallet transaction, reverse or refund it exactly once, and update its reporting state. A `bet_amount` included for reference must not cause a second debit.
### 4. Automatic Retry Mechanism and Response Codes
* **Successful Processing**: Return a `HTTP 200` status code with `{"received": 1, "status": "success"}`. VCT's queue worker will mark this task as successfully delivered.
* **Retry Guarantee**: If the Agent's database is busy or has transient errors, return `HTTP 5xx` or let the request timeout. VCT's delivery queue will **automatically retry pushing the log** over subsequent intervals to guarantee zero log loss.
## Provider Source Mapping
| Provider | Callback Path | Request Format |
|----------|--------------|----------------|
| BG | — | Push not supported |
| GSC | `seamless/pushbetdata` | JSON, wagers array |
| SA | — | Push not supported (use query API to pull) |
| WM | `webhook` (cmd=SendMemberReport) | form-data, result array |
## AI Implementation Prompt (Copy Ready)
```text
Implement the Agent endpoint POST /api/v1/callback/bet-push. This is the Seamless-wallet source of truth for debits, payouts, bonuses, and corrections.
Requirements:
1. Verify the three-part Authorization Token, require Header timestamp == Body timestamp, and verify Body sign as lowercase hex HMAC-SHA256(agent_api_key + "." + timestamp, agent_api_secret) using constant-time comparison.
2. Verify operator_code belongs to this Agent and validate every wager's username, currency, session_id, bet_id, status, and amounts.
3. Use decimal/fixed-point money types, never float. Process each batch in a database transaction and return success only after commit.
4. Enforce a stable idempotency key including Agent, bet_id, status, and original_transaction_id as appropriate.
5. BET debits bet_amount. SETTLED debits its bet_amount and credits prize_amount. BONUS credits prize_amount. BET/SETTLED may be split or combined and may arrive out of order.
6. CANCEL/ROLLBACK/REFUND/VOID must locate the original transaction via original_transaction_id and reverse it exactly once; a reference bet_amount must not cause another debit.
7. Store session_id, round_id, brand_guid, brand_code, brand_name, game fields, timestamps, and raw payload. Relate wagers to the catalogue using brand_guid, never brand_code/name alone. Keep equal names/codes with different GUIDs separate. Preserve historical records lacking GUID and mark Brand attribution unresolved; never invent a GUID. Preserve wallet handling and query rates using brand_guid plus the original business date in as_of_date. Do not use round_id alone as a unique key.
7a. Use numeric game_genre (2 Slot, 3 Fish, 4 Sport, 5 Card, 6 Live, 7 Other) for commission and game_genre_name only for display. The old game_type is provider-native. Hold genre-0 wagers for reconciliation; do not guess a type or silently charge commission.
8. Return HTTP 200 with {"received": committed_count, "status":"success"} only when all required work commits. Return 5xx for transient failures so VCT retries.
9. Offline reclaimed_amount must not change wallet funds already processed by bet-push.
10. Handle duplicates, out-of-order, and late records idempotently. Redact credentials/signatures from logs.
11. Test BET, split/combined SETTLED, BONUS, all corrections, batch atomicity, duplicate/out-of-order/late delivery, precision, invalid signatures, database failures, and distinct Brand GUIDs sharing the same code/name.
Reuse the existing wallet ledger, transactions, unique constraints, and callback authentication; do not create a second balance source of truth.