API REFERENCE / V1
하나의 Core API.
브라우저 게임은 같은 origin의 절대 경로를 사용합니다. 가격, 잔액, 영수증 상태는 항상 KOISCORE 서버 응답을 권위값으로 취급하세요.BASE / AUTH
주소와 인증
KOISCORE Core API를 호출하려면 먼저 “어디서 실행 중인 코드인가”에 따라 인증 방식을 나눠야 합니다. koiscore.com 도메인 위에서 돌아가는 포털·게임 iframe은 HttpOnly same-site 쿠키 세션을 씁니다. 별도 origin에 호스팅된 승인 런타임은 발급된 Bearer 세션 또는 identity assertion을 Authorization 헤더에 실어 서버 API를 호출합니다. 브라우저 fetch에서는 credentials: 'same-origin'을 빼먹으면 로그인했는데도 게스트로 보이는 문제가 생깁니다.
아래 예시는 가장 흔한 “현재 프로필 조회” 패턴입니다. 응답 JSON의 잔액·가격·영수증 상태는 UI 표시용으로만 쓰고, 실제 지급·차감은 항상 서버가 다시 계산한 값을 권위값으로 삼으세요. MCP 토큰·DB URL·관리자 토큰을 게임 클라이언트 번들·쿼리스트링·console.log에 넣지 마세요.
const response = await fetch('/api/profile/me', {
credentials: 'same-origin',
headers: { accept: 'application/json' },
});DEVELOPER / RELEASE ASSETS
계정별 스토어 제출 데이터
Google 로그인 개발자 세션을 사용하며 모든 게임 요청에서 등록 소유권을 다시 확인합니다. 쓰기 요청은 동일 출처에서만 허용됩니다.
로그인 계정이 소유한 게임과 릴리즈 슬롯을 조회합니다.
패키지명과 스토어별 공개 URL을 저장합니다.
릴리즈 에셋을 검사하고 KOISCORE 이름 PWA를 Google Play·원스토어·앱인토스 출시 파이프라인에 넣습니다. 스토어 제출은 플랫폼이 이어 갑니다.
multipart/form-data의 file 이미지를 규격 검증 후 저장합니다.
소유권 확인 후 업로드 원본을 내려받습니다.
소유 게임에 등록된 상품 SKU 목록을 조회합니다.
소유 게임에 상품 SKU를 추가합니다. 젬 상품은 등록 즉시 receipt bridge로 판매·정산되고, 구독제는 현금결제로 판매됩니다.
스토어 URL 키는 googlePlay, appStore, oneStore, appsInToss, web입니다. 실제 제출 규격은 릴리즈 목록 응답의 specs를 기준으로 합니다.
개발사가 KOISCORE 이름으로 내려면 개발자센터에서 KOISCORE로 출시를 누르면 됩니다. 에셋이 채워져 있으면 PWA 패키지와 스토어 리스팅을 만들고 제출 큐에 올립니다. Google Play는 Developer API, 앱인토스는 .ait 번들을 만들어 npx ait deploy로 올립니다. 원스토어는 최초 등록 때 외부결제(포트원)를 켭니다. 패키지명은 com.koiscore.game.{gameId}입니다.
SUBSCRIPTIONS / CASH
아이템 종류와 결제
플랫폼이 하는 일은 지급 종류를 나누고, 그 종류에 결제 수단을 붙이는 것입니다. 게임은 등록한 아이템을 검사하기만 하면 됩니다.
- 지급 종류:
soft_currency게임 재화,item아이템,entitlement권한. - 젬 결제: 코이젬 지갑. 젬 상품은 개발자 콘솔 등록 즉시 서버 카탈로그·영수증·정산에 반영됩니다.
- 결제는 채널마다 다릅니다. 웹·원스토어·해외는 포트원, Google Play는 Play 빌링, 앱인토스는 앱인토스 IAP입니다. 게임이 영수증을 자체 검증해서 권한을 주지 마세요.
구독 SKU를 등록하면 웹은 포트원(한국은 토스 채널)으로 한 기간을 결제하고, 그 아이템이 계정에 붙습니다. 구매 페이지는 /subscribe/:gameId입니다. 게임 서버는 identity assertion으로 보유·티어·쿼터만 확인합니다.
게임이 발행한 구독 아이템 SKU와 현재 세션의 보유 아이템을 조회합니다. 앱은 ?salesChannel=googleplay|onestore|appintoss 로 스토어 상품만 봅니다.
웹에서 구독 아이템을 토스페이로 결제할 주문을 만듭니다. 연결된 계정과 약관 동의가 필요합니다.
웹 토스페이 승인을 확인하고 구독 아이템을 기간만큼 지급합니다.
앱에서 스토어 구독을 등록합니다. Google Play·앱인토스만 받으며, 웹·원스토어 결제창을 열지 않습니다.
앱에서 등록한 스토어 주문의 영수증을 받아 결제합니다. 그 게임 스토어 검증 전에는 권한을 지급하지 않습니다.
쿠키 세션의 현재 구독 아이템을 조회합니다.
identity assertion으로 구독 아이템(grantKey·최소 티어)을 확인하고 선택적으로 기간 쿼터를 차감합니다. 클라이언트 playerId는 받지 않습니다.
POST /api/app/baduk/subscriptions/entitlements
Content-Type: application/json
{
"assertion": identity.assertion.token,
"grantKey": "ai_coach",
"minTier": 2,
"consume": true
}POST /api/app/baduk/subscriptions/store/orders
Content-Type: application/json
{
"assertion": identity.assertion.token,
"sku": "baduk.coach.l2.monthly.googleplay",
"salesChannel": "googleplay",
"termsAccepted": true,
"withdrawalConsent": true
}POST /api/app/baduk/subscriptions/receipts
Content-Type: application/json
{
"assertion": identity.assertion.token,
"orderId": "sub_...",
"sku": "baduk.coach.l2.monthly.googleplay",
"salesChannel": "googleplay",
"transactionId": "GPA.1234-5678-9012-34567",
"receipt": "<store-receipt>"
}- 모바일은 앱에서만 등록하고 결제합니다. 앱이 스토어 주문을 만든 뒤, 스토어 영수증을 같은 주문에 붙입니다. 웹 토스 결제창을 앱 안에 넣지 마세요.
- 같은 권한은 채널마다 SKU를 따로 둡니다. 웹은 토스페이 단건, 모바일은 스토어 구독만 둡니다. 게임은 grantKey 하나만 검사하면 됩니다.
- SKU는
{gameId}.로 시작합니다. 스토어 구독 SKU는 카탈로그에 있어도 앱 주문을 등록하기 전에는 결제하지 않습니다. - 구독은 젬으로 팔 수 없습니다. 월/연 현금 가격, grantKey, 티어 1–9, 기간 쿼터(0=무제한)를 붙입니다.
- identity
items는 UI용입니다. 코칭처럼 서버에서 막을 기능은 아래처럼 assertion으로 다시 확인합니다. - 같은 티어를 다시 결제하면 남은 기간 뒤에 한 기간이 이어집니다. 이용 중인 더 낮은 티어로는 내릴 수 없습니다.
- 게임 서버는 클라이언트
playerId를 믿지 말고 identity assertion만 보냅니다.
COMMON MAILBOX / SERVER API
게임별 키와 보상 우편
개발자센터에서 소유 게임의 서버 키를 발급합니다. 키 원문은 생성 응답에서 한 번만 반환되며 게임 클라이언트가 아닌 게임 서버에서만 사용합니다.
현재 개발자 계정이 발급한 게임별 키의 접두사·만료·최근 사용 상태를 조회합니다.
소유 게임의 hash-only 서버 키를 생성합니다. 원문은 이 응답에서 한 번만 반환됩니다.
유출·교체 대상 키를 즉시 폐기합니다.
현재 KOISCORE 사용자에게 도착한 플랫폼·게임 우편을 조회합니다. sourceGameId가 있으면 포털 우편함에서 해당 게임을 바로 열 수 있습니다.
플랫폼 젬을 받거나 게임 전용 보상의 게임 이동 정보를 생성합니다.
게임 서버 키로 현재 게임의 전용 재화 우편을 멱등 발송합니다.
게임 서버가 인증 사용자의 우편 grant를 멱등 확정합니다.
- 포털 우편함(
/mailbox/{locale})은 게임별 우편에 게임 열기(/portal/{locale}?game={sourceGameId})를 제공합니다. - 게임 보상 받기는 runtime으로 이동하고, 게임 서버가
/redemptions/consume으로 지급을 확정합니다.
CORE / ENDPOINTS
공개 게임 연동 엔드포인트
인증 없는 공개 게임 카탈로그. CORS와 캐시를 지원합니다.
게임의 업데이트·점검 상태와 현재 실행 가능 여부를 조회합니다.
최근 7일 플레이 지표와 게임 귀속 매출 가산점으로 계산한 포털 노출 순위입니다.
게스트 프로필과 세션을 생성합니다.
현재 로그인 또는 게스트 프로필을 조회합니다.
표시 이름, 프로필 이미지와 언어 설정을 수정합니다.
Google OAuth 로그인을 시작하고 현재 게스트 기록에 연결합니다.
등록 gameId의 현재 player 컨텍스트. 게스트·연결 계정 공통.
등록 gameId 단위 JSON 진행 조회. query gameId 필수.
등록 gameId 단위 JSON 진행 저장. version 충돌 시 409.
현재 세션의 게임별 playerId, 단기 서명 assertion, 보유 중인 구독 아이템을 발급합니다.
게임 서버가 identity assertion을 검증할 Ed25519 공개키입니다.
활성화된 게임의 등록 환율로 젬을 게임 재화로 교환합니다.
게임이 개발자 콘솔에 등록한 활성 젬 상품 카탈로그를 조회합니다. issue/commit 영수증의 가격 기준입니다.
서버 상품 카탈로그를 기준으로 구매 영수증을 발급합니다.
발급된 영수증의 현재 상태를 조회합니다.
게임 지급 어댑터가 영수증을 멱등 커밋합니다.
DB에 등록된 열린 매치를 확인하고 친구 초대 URL을 발급합니다.
서명·만료·DB 매치 상태를 검증하고 게임 참가 launch 정보를 반환합니다.
젬 교환과 구매 API는 게임 ID, SKU, 지급 어댑터가 서버에서 활성화된 뒤 사용할 수 있습니다. 등록만 된 draft 게임은 결제를 호출할 수 없습니다.
CATALOG / EXAMPLE
공개 게임 목록
GET /api/public/games
{
"schema": "koiscore.public-games.v1",
"updatedAt": "2026-08-03",
"count": 1,
"games": [{
"id": "example-game",
"portalId": "example-game",
"title": { "ko": "예제 게임", "ja": "サンプル", "en": "Example Game" },
"genres": ["puzzle"],
"runtimeUrl": "https://example.com/game",
"locales": ["ko", "ja", "en"],
"localeAware": true,
"status": "live"
}]
}RUNTIME / AVAILABILITY
업데이트·점검 중에는 실행하지 않기
게임 상태의 권위값은 KOISCORE 게임 레지스트리입니다. live는 공개 실행 가능하며, 오픈베타가 활성화된 draft, review, testing 게임도 베타 URL과 플랫폼 API를 사용할 수 있습니다. updating, suspended, rejected는 베타 여부와 관계없이 차단됩니다.
GET /api/public/games/availability/example-game
{
"gameId": "example-game",
"portalId": "example-game",
"status": "updating",
"betaEnabled": false,
"available": false
}- 중앙 포털은 실행 불가 게임을 공개 카탈로그에서 숨기고 게임 HTML·API 요청에
503,Retry-After: 60을 반환합니다. - 별도 origin 또는 앱 WebView 게임은 부팅 전에 이 API를 호출하고
available === false면 게임 로직과 결제·보상 API 호출을 시작하지 않습니다. 503응답의error는game_unavailable이며, 업데이트 안내 화면과 포털 복귀 동작을 제공해야 합니다.- 상태 API 자체가 일시적으로 실패한 경우 중앙 게이트는 서비스 전체 장애를 피하기 위해 fail-open합니다. 게임 서버의 자체 점검 차단도 별도로 유지하세요.
const availability = await fetch(
'https://koiscore.com/api/public/games/availability/example-game',
{ cache: 'no-store' }
).then(response => response.json());
if (availability.available === false) {
showMaintenanceScreen(availability.status);
return;
}
startGame();PLATFORM / PLAYER
세션·진행 저장 (등록 gameId 공통)
신규·외부 게임은 게임 전용 progress API를 만들지 않고 아래 공통 엔드포인트를 사용합니다. gameId는 개발자 콘솔에 등록한 값과 동일해야 하며, draft·베타·live 모두 identity audience가 켜진 게임만 호출할 수 있습니다. 게스트가 Google로 연결(회원가입)하면 같은 player_id로 진행이 유지되므로, 연결 직후 게임은 progress를 다시 읽어야 합니다.
<script src="https://koiscore.com/portal/koiscore-platform-player-client.js"></script>
<script>
KoiscorePlatformPlayer.configure({ gameId: 'your-registered-game-id' });
KoiscorePlatformPlayer.startSessionSync({
onChange: async ({ session, progress, reason }) => {
if (progress?.ok) applySave(progress.progress, progress.version);
},
});
await KoiscorePlatformPlayer.saveProgress(3, { level: 4, coins: 120 });
</script>- 브라우저 iframe:
credentials: 'include'+*.koiscore.comCORS - 게임 서버:
Authorization: Bearer <identity JWT>(aud= gameId) - 로그인·연결 후: 포털
portal:player-update→ progress GET 필수 - 샌드박스:
GET|PUT /api/sandbox/platform/player/...(에피머럴)
PROFILE / GOOGLE
로그인과 프로필은 Core가 관리
게임은 Google SDK나 OAuth 비밀값을 직접 포함하지 않습니다. KOISCORE가 로그인을 관리하고 게임에는 게임별 playerId와 단기 서명 assertion만 전달합니다. 세이브·점수·인벤토리는 각 게임 저장소가 playerId 기준으로 관리합니다.
// 로그인 시작
location.href = '/api/profile/connect/google?returnTo=/profile/connect';
// 현재 사용자 조회
const { profile } = await fetch('/api/profile/me', {
credentials: 'same-origin'
}).then(response => response.json());
// 프로필 설정 저장
await fetch('/api/profile/me', {
method: 'PATCH',
credentials: 'same-origin',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ displayName: 'Koi Player', locale: 'ko' })
});- 첫 Google 연결은 현재 게스트의 게임 기록과 지갑을 유지한 채 계정을 연결합니다.
- 이후 같은 Google 계정으로 로그인하면 기존 KOISCORE 프로필 세션으로 전환합니다.
- 게임에는 OAuth access token, Google client secret 또는 Google 이메일을 전달하지 않습니다.
- 전역 profileId는 게임 데이터 키로 사용하지 않으며, assertion은 JWKS의 Ed25519 공개키로 검증합니다.
- 프로필 사진은 KOISCORE 직접 업로드, 현재 주 로그인 플랫폼 사진, 공통 더미 순서로 선택되며 게임에는 표시 가능한
avatarUrl만 전달합니다.
WALLET / RECEIPTS
젬 구매는 영수증으로
게임 내 상점에서 KOISCORE 공용 젬으로 아이템을 팔려면, 클라이언트가 gemPrice를 직접 보내 “500젬 주세요”라고 호출하는 방식은 허용되지 않습니다. 반드시 서버 상품 SKU 카탈로그를 기준으로 /api/rewards/receipts/issue에서 영수증을 발급받고, 게임 서버의 purchase/commit 어댑터가 같은 receiptId를 멱등 처리해야 합니다. 이렇게 해야 환불·중복 클릭·네트워크 재시도·악의적 requestKey 재사용 모두 같은 결론으로 수렴합니다.
아래 HTTP 예시에서 requestKey는 “사용자가 구매 버튼을 한 번 누른 행위”마다 새로 만들고, 타임아웃 재시도에는 같은 key를 재사용합니다. items[].sku는 개발자 콘솔에 등록된 SKU id와 정확히 일치해야 하며, quantity·grantAmount를 클라이언트가 임의로 바꿔도 서버 카탈로그 값이 이깁니다.
POST /api/rewards/receipts/issue
Content-Type: application/json
{
"gameId": "example-game",
"requestKey": "issue_01K2ABCDEF12",
"source": "in_game_shop",
"items": [{ "sku": "example-game.coin-pack-1", "quantity": 1 }]
}requestKey는 사용자 작업마다 새 값을 만들고 재시도에는 같은 값을 사용합니다.- 가격과 지급량은 서버 SKU 카탈로그가 계산하며 클라이언트 값을 신뢰하지 않습니다.
- 게임 서버는
purchase/commit을 멱등 처리해 같은 영수증을 두 번 지급하지 않습니다.
PLAYER / POSTMESSAGE
공통 플레이어 브리지
웹 포털과 앱인토스용 페이지는 같은 KOISCORE 플레이어 셸을 사용합니다. 두 채널 모두 상단에 현재 게임 타이틀과 공용 젬 잔액을 표시하고 같은 메시지 계약으로 게임 프레임을 제어합니다.
| 방향 | TYPE | 역할 |
|---|---|---|
| 게임 → 포털 | game:ready | 리스너 준비 완료와 초기 상태 요청 |
| 포털 → 게임 | portal:init | locale, shell, 초기 음소거·라운지 상태 동기화 |
| 포털 → 게임 | portal:player-update | 로그인·연결·로그아웃 후 player 컨텍스트 갱신 |
| 포털 → 게임 | portal:audio | 실행 중 마스터 음소거 변경 |
| 게임 → 포털 | game:community-state-request | 현재 라운지 상태 조회 |
| 포털 → 게임 | portal:community-state | 라운지·풀스크린 상태 변경 |
| 게임 → 포털 | game:shell | 제목과 설명 등 공통 셸 갱신 |
| 게임 → 포털 | game:wallet | 교환 후 포털 젬 잔액 갱신 |
| 게임 → 포털 | game:match-invite | DB에 등록된 현재 매치의 친구 초대 링크 요청 |
| 포털 → 게임 | portal:match-invite | 공유·복사·취소 결과 반환 |
| 게임 → 포털 | game:rewarded-ad-request | 현재 플랫폼의 보상형 광고 요청 |
| 포털 → 게임 | portal:rewarded-ad-result | earned·dismissed·failed 결과 반환 |
window.parent.postMessage({
protocol: 'koiscore.portal.v1',
type: 'game:ready',
gameId: 'example-game'
}, 'https://koiscore.com');수신 측은 event.origin, event.source, protocol, gameId를 모두 검증해야 합니다. postMessage('*')는 사용하지 않습니다.
게임 안에서 보상형 광고 요청
<script src="https://koiscore.com/portal/koiscore-rewarded-ad-client.js"></script>
<script>
KoiscorePortalAds.configure({ gameId: 'example-game' });
async function continueAfterAd() {
const result = await KoiscorePortalAds.requestRewarded({
placement: 'extra_life'
});
if (result.status === 'earned' && result.earned === true) {
grantOneExtraLife();
}
}
</script>- 웹은 H5, 네이티브 앱은 AdMob, 앱인토스는 해당 플랫폼 광고 공급자를 포털이 선택합니다.
- 결과는 요청 때 사용한
requestId와placement로 돌아옵니다. - 동일 requestId는 광고를 중복 실행하지 않으며 동시 요청은
failed/busy로 거절됩니다. - 젬·유료 재화는 클라이언트
earned만 신뢰해 지급하지 말고 서버 claim 검증을 추가해야 합니다.
appintoss이면 웹 광고를 로드하지 않고 Apps in Toss 광고 SDK를 사용합니다. 타이틀, 젬 지갑, 게임 프레임 계약은 웹과 동일합니다.MULTIPLAYER / FRIEND INVITE
매칭은 게임이, 검증과 전달은 포털이 담당
게임 서버가 매치를 만든 뒤 KOISCORE의 ks_messenger_matches 레지스트리에 gameId + matchId를 등록합니다. 포털은 열린 상태이고 만료되지 않은 매치에만 짧은 /m/{trackingCode} URL을 발급합니다.
// 게임 iframe: 사용자가 "친구 초대"를 눌렀을 때
window.parent.postMessage({
protocol: 'koiscore.portal.v1',
type: 'game:match-invite',
gameId: 'example-game',
requestId: 'invite_01JABCDEF',
matchId: 'match_42'
}, 'https://koiscore.com');
// 초대받은 친구의 게임 iframe
window.addEventListener('message', (event) => {
if (event.origin !== 'https://koiscore.com') return;
const launch = event.data?.type === 'portal:init'
? event.data.launch
: null;
if (launch?.type === 'match_invite') {
joinMatch(launch.params.matchId);
}
});- 게임 종료 시 매치 레지스트리를
closed로 바꾸며, 만료된 매치는 초대할 수 없습니다. - URL에는 게임 서버 비밀번호, 세션 토큰 또는 개인정보를 직접 넣지 않습니다.
- 정원·차단·재접속·실제 참가 성공 여부는 게임의 매칭 서버가 최종 판단합니다.
- 게임은
portal:init을 여러 번 받아도 같은 매치 참가를 중복 실행하지 않도록 처리합니다.