심리스 지갑 콜백
플레이어 잔액의 원본은 오퍼레이터 지갑입니다. 플랫폼은 돈이 움직일 때마다 오퍼레이터가 구현한 지갑 엔드포인트 4개를 서버 간 HTTP 로 호출합니다. 이 장은 오퍼레이터가 구현해야 하는 쪽이므로, 멱등 · 재시도 · 순서 규칙을 빠짐없이 지켜 주세요.
개요
- 플랫폼은 플레이어 머니를 따로 보관하지 않습니다. 베팅 · 착석 때 debit(출금), 정산 · 이석 때 credit(입금), 취소 때 rollback(출금 취소)을 부르고, 화면에 잔액을 보여 줄 때 balance 를 부릅니다.
- 호출은 플레이어 화면과 무관하게 플랫폼 서버에서 백그라운드로 일어납니다. 플레이어가 창을 닫았거나 오퍼레이터 사이트에서 로그아웃한 뒤에도 credit · rollback 은 옵니다.
- 모든 거래에는 플랫폼이 만든
tx_id가 붙습니다. 오퍼레이터는tx_id로 중복 처리(멱등) 를 해야 합니다. - 플랫폼은 호출하기 전에 자기 원장에 거래를 먼저 기록합니다. 그래서 응답이 끊겨도 거래가 사라지지 않고, 아래 규칙대로 마무리(재시도 또는 취소)됩니다.
콜백 4종
| 엔드포인트 | 언제 | 하는 일 | 재시도 |
|---|---|---|---|
POST {wallet_base_url}/balance | 게임 화면 진입, 잔액 새로고침 | 현재 잔액 조회 | 안 함 |
POST {wallet_base_url}/debit | 베팅(bet · ante · double · split), 착석(buy_in) · 칩 추가(top_up) | 잔액 차감 | 안 함 — 결과가 불확실하면 실패로 보고 rollback |
POST {wallet_base_url}/credit | 라운드 정산(settle, 패배해도 amount: 0), 이석(cash_out) | 잔액 증가 | 성공할 때까지 |
POST {wallet_base_url}/rollback | 베팅 취소(cancel_bet), 라운드 무효(round_void), debit 불확실(timeout), 미완료 debit 정리(system) | 원 debit 금액 환원 | 성공할 때까지 |
콜백 URL
연동 신청 때 오퍼레이터가 지갑 기본 URL(wallet_base_url) 하나를 등록합니다. 플랫폼은 그 뒤에 / 와 엔드포인트 이름을 붙여 호출합니다.
등록한 wallet_base_url | 실제 호출 | 서명에 쓰는 경로 |
|---|---|---|
https://api.example-casino.com/wallet | POST https://api.example-casino.com/wallet/debit | /wallet/debit |
https://wallet.example-casino.com | POST https://wallet.example-casino.com/credit | /credit |
- 메서드는 네 엔드포인트 모두
POST입니다. - 등록한 URL 끝의
/는 지워서 저장합니다. 쿼리 문자열은 붙이지 마세요. - 호출은 프록시를 거치지 않고 직접 나갑니다. 오퍼레이터 방화벽에서 플랫폼의 발신 IP 를 허용해야 할 수 있습니다.
협의 사항
플랫폼의 지갑 콜백 발신 IP 와 지갑 URL 의 HTTPS 요건은 연동 신청 때 협의합니다. 공개 인터넷을 거치는 지갑 URL 은 HTTPS 를 쓰세요.
요청 헤더 · 서명
지갑 콜백은 Operator API 와 같은 HMAC-SHA256 서명 규칙을 씁니다. 서명문 구성과 예시 코드는 인증 · 서명을 보세요.
| 헤더 | 값 |
|---|---|
Content-Type | application/json; charset=utf-8 |
X-Operator | 오퍼레이터 코드 (예: acme) |
X-Timestamp | 보낸 시각, Unix epoch 밀리초 (예: 1792547400123) |
X-Nonce | 요청마다 새로 만든 24자 랜덤 문자열 (영문 대소문자 · 숫자 · - · _) |
X-Signature | 소문자 hex HMAC-SHA256 서명 (키 = api_secret) |
- 플랫폼은 지갑 콜백에
X-Api-Key를 보내지 않습니다. - 서명 대상 경로는 호출 URL 의 경로 부분(위 표의 "서명에 쓰는 경로")이고, 본문은 받은 원문 바이트 그대로입니다. JSON 을 파싱했다가 다시 직렬화해 서명을 비교하면 틀립니다.
- 같은 거래를 재시도할 때 본문은 처음과 바이트 단위로 같지만,
X-Timestamp·X-Nonce·X-Signature는 매번 새로 만듭니다.
오퍼레이터는 다음을 검사하세요.
X-Operator가 자기 오퍼레이터 코드인지X-Timestamp가 현재 시각 ±5분 안인지- 서명이 맞는지 (상수 시간 비교)
- 같은
X-Nonce가 5분 안에 다시 오지 않았는지 — 서버가 여러 대면 공유 저장소(Redis 등)에 기록하세요
검증에 실패하면 HTTP 200 + {"status":"INVALID_SIGNATURE"} 로 응답합니다.
서명 실패가 계속되면 돈이 묶입니다
debit 은 INVALID_SIGNATURE 를 받으면 거절로 끝나지만, credit · rollback 은 OK 를 받을 때까지 계속 재시도합니다. 시크릿 교체 · 서버 시계 오차로 검증이 계속 실패하면 그동안 당첨금 · 환불이 플레이어에게 들어가지 않습니다. 서버 시계를 NTP 로 맞추고, 시크릿을 바꿀 때는 플랫폼과 시점을 맞추세요.
요청 본문
본문은 공백 없는 한 줄 JSON 입니다. 필드 순서는 정해져 있지 않으니 순서에 의존하지 마세요. debit · credit · rollback 은 같은 골격을 쓰고, 종류별로 필드가 하나씩 더 붙습니다.
| 필드 | 타입 | balance | debit | credit | rollback | 설명 |
|---|---|---|---|---|---|---|
tx_id | string | ● | ● | ● | 플랫폼 거래 ID(tx_ + 32자리 hex). 멱등 키 | |
external_id | string | ● | ● | ● | ● | 오퍼레이터 쪽 플레이어 ID (게임 실행 때 보낸 값) |
player_id | string | null | ● | ● | ● | ● | 플랫폼 플레이어 ID(pl_…), 참고용. balance 의 연결 시험에서는 null |
currency | string | ● | ● | ● | ● | 통화 코드 (예: PHP). 플레이어의 통화는 처음 실행 때 정해지고 바뀌지 않습니다 |
operator_session_id | string | null | ● | ● | ● | ● | 게임 실행 때 보낸 값. 없었으면 null, 플랫폼 복구 경로에서는 null 일 수 있음 |
amount | integer | ● | ● | ● | 통화 최소 단위 정수. debit · rollback 은 1 이상, credit 은 0 이상 | |
reason | string | ● | ● | ● | 거래 사유. 엔드포인트별 페이지의 표 참고 | |
game_code | string | ● | ● | ● | 게임 코드 (예: live_baccarat) | |
table_id | string | ● | ● | ● | 테이블 ID (tbl_…) | |
round_id | string | ● | ● | ● | 묶음 ID. 베팅형 게임은 라운드(rnd_…), 바이인형 게임은 테이블 세션(tss_…) | |
created_at | string | ● | ● | ● | 플랫폼이 거래를 만든 시각, UTC 밀리초 (2026-10-19T02:10:00.123Z). 재시도해도 바뀌지 않음 | |
round_closed | boolean | ● | 이 credit 으로 플레이어의 라운드(또는 테이블 세션)가 끝났는지 | |||
ref_tx_id | string | ● | 취소할 debit 의 tx_id |
round_id 의 접두어는 reason 으로 정해집니다. buy_in · top_up · cash_out 은 tss_…, bet · ante · double · split · settle · cancel_bet · round_void 는 rnd_… 입니다. timeout · system rollback 은 원 debit 과 같은 접두어를 씁니다.
응답
업무 결과는 항상 HTTP 200 으로 돌려주고, 본문의 status 로 구분합니다. 플랫폼은 아래 세 필드만 읽고 나머지는 무시합니다. 응답 본문은 64KB 까지만 읽습니다.
{ "status": "OK", "balance": 1250000, "operator_tx_id": "w-99812" }| 필드 | 타입 | 설명 |
|---|---|---|
status | string | 결과 코드. 아래 표의 값 중 하나 (필수) |
balance | integer | 처리 후 플레이어 잔액(최소 단위 정수). OK 일 때는 꼭 넣으세요 — 플랫폼이 이 값을 플레이어 화면에 바로 반영합니다 |
operator_tx_id | string | 오퍼레이터 쪽 거래 ID. 플랫폼 원장에 함께 남아 거래 조회와 대사에 쓰입니다 |
status 값
| status | 의미 | debit | credit · rollback | balance |
|---|---|---|---|---|
OK | 처리됨 (같은 tx_id 재요청이면 처음 결과) | 확정 | 확정 | 잔액 표시 |
INSUFFICIENT_FUNDS | 잔액 부족 | 거절 확정 | 재시도 | 실패 처리 |
PLAYER_NOT_FOUND | 플레이어 없음 | 거절 확정 | 재시도 | 실패 처리 |
PLAYER_BLOCKED | 차단 · 자기 배제 중인 플레이어 | 거절 확정 | 재시도 | 실패 처리 |
LIMIT_EXCEEDED | 오퍼레이터의 책임 도박 한도 초과 | 거절 확정 | 재시도 | 실패 처리 |
CURRENCY_MISMATCH | 플레이어 통화와 요청 통화가 다름 | 거절 확정 | 재시도 | 실패 처리 |
INVALID_SIGNATURE | 서명 검증 실패 | 거절 확정 | 재시도 | 실패 처리 |
TX_ALREADY_ROLLED_BACK | 이미 rollback 된 tx_id 의 debit 이 늦게 도착 | 거절 확정 | 재시도 | 실패 처리 |
INVALID_REQUEST | 필드 누락 · 형식 오류 | 거절 확정 | 재시도 | 실패 처리 |
UNKNOWN_ERROR | 그 밖의 오류 (처리 여부를 모름) | 결과 불확실 → 실패 처리 + 즉시 rollback | 재시도 | 실패 처리 |
- 거절 확정: 플랫폼은 이 debit 이 반영되지 않았다고 보고 베팅 · 착석을 거절합니다. rollback 은 보내지 않습니다. 따라서 거절 status 를 줄 때는 잔액을 건드리지 않았어야 합니다.
- 결과 불확실: HTTP 200 이 아닌 응답, 타임아웃, 연결 실패, JSON 이 아닌 본문,
status가 빈 응답도UNKNOWN_ERROR와 똑같이 처리합니다. - 재시도: credit · rollback 은
OK가 아니면 status 종류와 상관없이 다시 보냅니다. credit · rollback 에는OK말고 끝나는 응답이 없습니다. - 실패 처리(balance): 플랫폼은 마지막으로 알던 잔액(최근 10초 안의 값)을 쓰고, 그것도 없으면 잔액을 표시하지 못합니다.
목록에 없는 status 를 쓰지 마세요
debit 에 위 표에 없는 값(예: "ERROR")을 돌려주면 플랫폼은 거절 확정으로 처리하고 rollback 을 보내지 않습니다. 차감했는지 확실하지 않은 오류는 반드시 UNKNOWN_ERROR 또는 HTTP 5xx 로 응답하세요. 그래야 플랫폼이 rollback 으로 정리합니다.
멱등성
tx_id 는 플랫폼이 거래 내용에서 결정적으로 만들기 때문에, 같은 거래는 몇 번을 다시 보내도 같은 tx_id 로 옵니다.
- 같은
tx_id가 다시 오면 다시 반영하지 말고 처음 응답을 그대로 돌려주세요. 성공뿐 아니라 거절(INSUFFICIENT_FUNDS등)도 처음 결과를 그대로 줍니다. - 플랫폼은 하나의
tx_id를 다른 금액 · 다른 플레이어 · 다른 사유로 다시 쓰지 않습니다. 오퍼레이터 쪽에서 같은tx_id인데 내용이 다른 요청이 오면INVALID_REQUEST로 거절하고 플랫폼에 알려 주세요. tx_id고유성은 엔드포인트 종류를 가리지 않습니다. debit · credit · rollback 의tx_id는 서로 겹치지 않습니다.- 한 debit 에 대한 rollback 의
tx_id는 하나로 정해져 있습니다. 같은 debit 이 사유를 바꿔 두 번 취소되는 일은 없습니다.
타임아웃 · 재시도
플랫폼 기본 설정입니다.
| 호출 | 응답 대기 | 실패 · 불확실할 때 |
|---|---|---|
| balance | 3초 | 재시도하지 않음. 최근 10초 안에 알던 잔액을 표시 |
| debit | 3초 | 재시도하지 않음. 실패로 처리하고 곧바로 같은 금액의 rollback(reason: "timeout")을 보냄 |
| credit | 5초 | 성공할 때까지 재시도 |
| rollback | 5초 | 성공할 때까지 재시도 |
- 연결 수립은 2초, TLS 핸드셰이크는 3초 안에 끝나야 합니다. 연결은 keep-alive 로 재사용하고 HTTP/2 를 시도합니다.
- credit · rollback 재시도 간격: 1초 → 2초 → 5초 → 15초 → 1분 → 5분, 그 뒤로는 5분마다 기한 없이 다시 보냅니다. 24시간이 넘도록 끝나지 않은 거래는 플랫폼 운영 쪽에서 수동 확인 대상으로 잡힙니다.
- 플랫폼 서버가 호출 도중 멈추면, 남은 credit · rollback 은 약 1분 뒤 이어서 보내고, 결과를 받지 못한 debit 은 30초 뒤
reason: "system"rollback 으로 정리합니다. - 라이브 게임은 베팅 마감 직전에 debit 이 몰립니다. debit 은 3초를 넘기면 실패하므로 지갑은 부하가 몰릴 때도 충분히 빨리 응답해야 합니다.
협의 사항
지갑 응답 시간 목표(SLA)와 처리량은 계약 시 협의합니다.
순서와 동시성
- 플랫폼은 같은 플레이어의 새 호출을 한 줄로 세워 순서대로 보냅니다. 같은 플레이어의 debit 두 건이 동시에 나가지는 않습니다. 다른 플레이어끼리는 병렬입니다.
- 다만 재시도는 순서를 보장하지 않습니다. 재시도 중인 credit 이 그 뒤에 생긴 debit 보다 늦게 도착할 수 있습니다.
- debit 이 3초를 넘기면 플랫폼은 응답을 기다리지 않고 rollback 을 보냅니다. 오퍼레이터 쪽에서는 아직 처리 중인 debit 과 그 rollback 이 겹치거나, rollback 이 debit 보다 먼저 처리될 수 있습니다. → rollback
플레이어 단위 잠금(예: 플레이어 잔액 행 SELECT … FOR UPDATE) 안에서 debit · credit · rollback 을 처리하면 이 경우들을 안전하게 다룰 수 있습니다.
호출 순서 예
베팅형 게임 (바카라 · 블랙잭 · 스피드 홀덤)
라운드마다 debit 이 여러 번 오고, 정산 credit 은 베팅이 남아 있는 플레이어마다 한 번 옵니다. 지면 amount: 0 입니다.
라운드 rnd_A — 정상
debit tx_1 5,000 reason=bet
debit tx_2 10,000 reason=bet
credit tx_3 30,000 reason=settle round_closed=true (돌려받는 원금 포함 총 지급액)
라운드 rnd_B — 베팅 하나 취소
debit tx_4 5,000 reason=bet
debit tx_5 5,000 reason=bet
rollback tx_6 5,000 reason=cancel_bet ref_tx_id=tx_5
credit tx_7 0 reason=settle round_closed=true (패배)
라운드 rnd_C — 라운드 무효
debit tx_8 5,000 reason=bet
rollback tx_9 5,000 reason=round_void ref_tx_id=tx_8 (credit 은 오지 않음)
라운드 rnd_D — debit 응답 지연
debit tx_10 5,000 reason=bet ── 3초 안에 응답 없음 → 플랫폼은 베팅 거절
rollback tx_11 5,000 reason=timeout ref_tx_id=tx_10바이인형 게임 (홀덤 · 오마하 · 라이브 홀덤 · 맞고 · 고스톱 · 섯다 · 통잇 · 레드독)
핸드마다 지갑을 부르지 않습니다. 칩은 테이블 안에서만 움직이고, 앉을 때 debit, 일어날 때 credit 합니다. round_id 에는 테이블 세션 ID(tss_…)가 들어갑니다.
테이블 세션 tss_B — 착석부터 이석까지
debit tx_1 100,000 reason=buy_in
debit tx_2 50,000 reason=top_up
credit tx_3 182,000 reason=cash_out round_closed=true (남은 칩 전부, 다 잃었으면 0)- 핸드가 무효가 되면 칩은 좌석으로 돌아가므로 지갑 호출이 없습니다.
- 레이크(수수료)는 지갑 거래가 아닙니다. GGR 리포트에 따로 집계됩니다.
알(선불 크레딧)과 지갑 콜백
운영 방식이 prepaid(알방식)인 오퍼레이터는 지갑 콜백 앞뒤로 알이 함께 움직입니다. independent 방식에는 해당하지 않습니다.
| 시점 | 알 | 지갑 콜백 |
|---|---|---|
| debit 직전 | debit 금액만큼 먼저 차감 | 알이 모자라면 debit 을 보내지 않고 베팅 · 착석을 거절 (플레이어 화면 오류 OPERATOR_CREDIT_EXHAUSTED) |
| debit 거절 확정 | 차감분 복원 | — |
| debit 불확실 | rollback 이 OK 로 끝날 때 차감분 복원 | rollback 발송 |
rollback OK | 원 debit 차감분 복원 | — |
credit OK | credit 금액만큼 더함 (0 이면 변화 없음) | — |
알이 모자라 거절된 베팅은 지갑에 아무 호출도 남기지 않습니다. 알 잔액은 GET /credit으로 확인합니다.
오퍼레이터 구현 권장사항
- 거래 기록 테이블을 두고
tx_id를 기본 키로 씁니다. 처리 결과(응답 JSON)를 함께 저장해, 같은tx_id재요청에 그대로 돌려줍니다. - 롤백 표시(tombstone) 를 둡니다. rollback 이 왔는데 원 debit 이 없으면
ref_tx_id를 "롤백됨"으로 기록하고, 나중에 그 debit 이 도착하면TX_ALREADY_ROLLED_BACK으로 거절합니다. - 잔액 변경과 거래 기록은 한 DB 트랜잭션 안에서, 플레이어 단위 잠금을 잡은 뒤에 합니다. 롤백 표시 확인도 잠금을 잡은 뒤에 해야 debit 과 rollback 이 겹칠 때 이중 처리가 생기지 않습니다.
- credit · rollback 은 어떤 상태의 플레이어에게도
OK로 끝내야 합니다. 차단 · 로그아웃 · 세션 만료 · 자기 배제 중이어도 돈을 돌려주는 거래는 반영합니다. - 차감했는지 확실하지 않은 오류(DB 타임아웃 등)는
UNKNOWN_ERROR나 HTTP 5xx 로 응답합니다. 거절 status 는 잔액을 건드리지 않은 게 확실할 때만 씁니다. - 응답에는 항상 처리 후
balance를 넣습니다. - 요청 본문의
game_code·table_id·round_id·reason을 함께 저장해 두면 라운드 · 거래 API와 대사하기 쉽습니다.GET /transactions의tx_id·operator_tx_id로 양쪽 기록을 맞춰 볼 수 있습니다. - 알 수 없는 엔드포인트 경로는 HTTP 404, 깨진 JSON 은
INVALID_REQUEST로 응답합니다.
구현 예 (의사 코드):
on debit(req):
verify signature → 실패면 {status: INVALID_SIGNATURE}
validate fields (tx_id, external_id, amount > 0 정수)
→ 실패면 {status: INVALID_REQUEST}
BEGIN
if wallet_tx[req.tx_id] exists → return 저장된 응답
lock player row (SELECT … FOR UPDATE) → 없으면 PLAYER_NOT_FOUND (기록 후 반환)
if rolled_back[req.tx_id] exists → TX_ALREADY_ROLLED_BACK (기록 후 반환)
if currency 다름 → CURRENCY_MISMATCH
if 차단 · 자기 배제 → PLAYER_BLOCKED
if 책임 도박 한도 초과 → LIMIT_EXCEEDED
if balance < amount → INSUFFICIENT_FUNDS
balance -= amount
res = {status: OK, balance, operator_tx_id}
insert wallet_tx(req.tx_id, 'debit', amount, res)
COMMIT
return res연결 확인
연동 신청 뒤 플랫폼이 등록된 wallet_base_url 로 balance 를 한 번 호출해 연결과 서명을 확인합니다. 존재하지 않는 플레이어(external_id 가 __hq_ping_<숫자>, player_id 는 null)로 부르므로, 서명 검증을 통과한 뒤 PLAYER_NOT_FOUND 를 돌려주면 정상입니다. 시험 계정의 external_id 를 알려 주면 그 계정으로 OK 응답까지 확인합니다. 출시 전 확인 항목은 연동 체크리스트를 보세요.