# TapFun 接続仕様書 Web App

> ブラウザで動作するウェブアプリケーションを TapFun に提供するパブリッシャー向けの接続仕様書


## はじめに {#introduction}

このページは、ブラウザで動作する **ウェブアプリケーション** （Web App）を TapFun に提供するパブリッシャー・開発者向けの接続仕様書です。ウェブアプリケーションは TapFun のページ内に iframe として埋め込まれて動作します。

Windows 向け exe をクラウドゲーミングで提供する場合は [クラウドゲーミング向けの仕様書](../cloudgaming/) を参照してください。

### コミュニケーション手段の確立 {#intro-communication}

- 基本はメールで行います。
- 必要に応じて、両社が参加する Slack コネクトチャネルを TapFun 側で発行します。
- Slack コネクトチャネルへの参加ができない場合は、御社のコミュニケーションツールへ参加させていただきます。

### Publisher Console へのログイン {#intro-publisher-console-login}

exe のアップデートや各種設定は、テスト環境の Publisher Console（`https://<TEST_HOST>/publisher`）で行えます。担当者がログイン権限を付与しますので、ログインされる方のメールアドレスをお伝えください。

### リリース内容の調整 {#intro-release}

動作上の問題の解決の目処が立ちましたら、リリース時期や提供価格を調整します。

### 開発中の問題発生時の対応 {#intro-issue-report}

「操作が反応しない」「意図しない動作がある」といった問題が発生した際は、以下を添えてコミュニケーションチャネルでお知らせください。

- 問題が再現したアプリ名（テスト環境上に同一ゲームが複数ある場合は、アプリ ID など識別できる情報）
- 問題が発生したテスト環境の TapFun ユーザーアカウント（ゲスト状態でのみ発生する場合を除く）
- 発生状況が把握できるスクリーンショット
- スクリーンショットで不足する場合は、再現手順全体の録画
- 問題の再現手順

問題が正常な仕様に起因する場合、双方協議のうえで機能追加や変更を行う場合があります。

### デバッグ {#intro-debugging}

ゲーム実行ページの URL に次のパラメータを付けると、[クライアント API](#client-api) のやり取りを確認できます。

| パラメータ | 内容 |
| --- | --- |
| `?debugLog=1` | ゲーム画面の上にログ表示領域を出します。TapFun が受け取ったクライアント API のリクエスト（[consoleLog](#api-consolelog) を含む）が表示されます。 |
| `?verbose=1` | ブラウザのデベロッパーツールのコンソールに詳細なログを出します。consoleLog の `data` もここに出力されます。 |

## 環境 {#environments}

### ホスト名 {#hosts}

| TapFun 環境 | ホスト名 | Web UI の Basic 認証 |
| --- | --- | --- |
| テスト | 担当者にお問い合わせください（本ドキュメントでは `<TEST_HOST>` と表記） | あり（ID / パスワードは担当者にお問い合わせください） |
| 本番 | `https://tapfun.co.jp` | なし |

テスト環境のホスト名と Basic 認証の ID / パスワードは秘匿情報です。このドキュメントには記載していませんので、TapFun の担当者にお問い合わせください。コード例などの `<TEST_HOST>` はテスト環境のホスト名に置き換えてください。社外や公開の場（公開リポジトリ、公開チャンネルなど）には記載しないでください。

### テスト環境での決済方法 {#test-payment}

テスト環境でアイテムを購入する際は、次のいずれかを使用してください。

- **AmazonPay**: 選択すると「マルチペイメントサービス テスト環境」に遷移します。「決済する」を選択すると決済できます。
- **テスト用クレジットカード**: 番号 `4111111111111111`、有効期限は未来の任意の年月、セキュリティコードは任意の 3 桁の数字。

### 本番でのテストプレイ {#production-test-play}

リリース前のアプリにアクセスするには、Publisher Console の基本情報でメンテナンスモードを有効にし、「メンテナンス例外メールアドレス」にテストプレイするユーザーのメールアドレスを入力してください。

本番で購入を伴うテストを行う場合は、TapFun 側でアプリごとにテストプレイ用アカウントを登録します。このアカウントには対象アプリでのみ使える TapFun クレジットが付与されます。テストプレイ用アカウントの購入は即座に取り消され、売上から相殺されます。

## Publisher Console {#publisher-console}

### 基本情報 {#pc-basic}

| 項目 | 説明 |
| --- | --- |
| アプリ ID | アプリの内部 ID です。 |
| name | 内部名です。 |
| apiKey | [Web API](#web-api) を呼ぶ際の API キーです。 |
| Bearer token | Web API を呼ぶ際の `Authorization: Bearer` の値です（`アプリID_apiKey` の形式）。 |
| メンテナンスモード | 有効にするとメンテナンスモードになります。 |
| メンテナンス例外メールアドレス | メンテナンス中でもプレイできるユーザーのメールアドレスです。リリース前の非公開アプリでも、メンテナンスモードを有効にするとこのアドレスのアカウントからアクセスできます。 |
| 起動 URL | ウェブアプリケーションの起動 URL です。TapFun のページ内に iframe として読み込みます。プレースホルダ `{token}`（[create_session](#api-create-session) 用トークン）、`{host}`（TapFun 側のホスト名。例: `tapfun.co.jp`）、`{appId}`（アプリ ID）が使えます。 |
| sigKey | HMAC-SHA256 の署名用キーです。[purchaseItem](#api-purchaseitem) の `sig` と [Callback API](#callback-api) の署名に使います。 |
| purchaseItem 時の sig 必須 | 有効にすると、`sig` を送らない purchaseItem は失敗します（`invalidSignature`）。 |
| フレームレス表示 | 有効にすると、プレイ画面に TapFun のヘッダーを表示しません。 |
| callback HTTP POST URL | アイテム購入時の通知先（[Callback API](#callback-api)）の URL です。 |
| callback Notice Webhook URL | Callback の送信に問題があったとき・復旧したときに、Webhook で通知する先の URL です（Slack の Incoming Webhook など）。 |
| callback Notice Email | Callback の送信に問題があったとき・復旧したときに通知するメールアドレスです。 |
| purchases API 互換モード | [purchases API](#api-purchases) のレスポンスの `userId` を旧形式で返す後方互換モードです。通常は無効にしてください。 |


### 詳細情報（紹介ページ） {#pc-detail}

「詳細情報」タブでは、TapFun 上のゲーム紹介ページに表示する内容を編集します。

| 項目 | 説明 |
| --- | --- |
| アイコン画像 | ゲームのアイコンです。推奨解像度は 512x512（正方形）です。 |
| 2:1 画像 | 一覧や紹介ページで使う横長の画像です。推奨解像度は 1920x960 です。 |
| ローディング画像 PC / SP | ゲームの起動中に表示する画像です。推奨解像度は PC が 600x520、スマートフォンが 300x260 です。 |
| 操作説明画像 | プレイ中に TapFun の画面から開ける操作説明の画像です（任意）。 |
| スクリーンショット画像 | 紹介ページに表示するスクリーンショットです。 |
| ゲームタイトル | 紹介ページなどに表示するゲームの名前です。 |
| 開発元 / パブリッシャー | 紹介ページに表示する開発元とパブリッシャーの名前です。 |
| ゲーム詳細紹介文 | 紹介ページの本文です。[Markdown](https://marked.js.org/#specifications) 形式で書けます。 |
| サードパーティー EULA | ゲームに含まれるサードパーティーの使用許諾など、紹介ページに表示する規約の文面です。 |

操作説明画像とスクリーンショット画像は、ドラッグで並べ替えられます。削除するときは、画像をクリックして削除マークを付けたあと、「追加アップロード & 順序を保存」を押すと反映されます。

### ゲームの起動 {#pc-launch}

アプリ画面上部の「アプリページ」から、次のオプションを付けてゲームを起動できます（[デバッグ](#intro-debugging)）。リリース前のアプリには「非公開状態」と表示されます。

| オプション | 説明 |
| --- | --- |
| ログ表示 | 画面上に consoleLog API などのログを表示します（URL の `?debugLog=1` と同じ）。 |
| verbose | ブラウザのコンソールに出すログの量を増やします（URL の `?verbose=1` と同じ）。 |

### そのほかのタブ {#pc-other-tabs}

| タブ | 内容 |
| --- | --- |
| 月別売上 | 日ごとの購入回数・売上・取消と、売上区分（Platform / Publisher）ごとの内訳です。 |
| 全ての Purchases / 未完了の Purchases | 購入の一覧です。「未完了」は Callback が完了していない購入だけを表示します。各購入の画面から Callback を再送できます。テスト環境では購入の取り消しと Callback 再送の停止もできます。 |
| Callbacks | [Callback API](#callback-api) の送信履歴です。 |
| Leaderboards | リーダーボードとエントリーの一覧です。 |
| 紹介ページ | TapFun 上のゲーム紹介ページを別タブで開きます。 |

## 起動と認証 {#webapp}

### 動作概要 {#webapp-overview}

- ウェブアプリケーションは TapFun のページが親フレームとなり、Publisher Console の [起動 URL](#pc-basic) で iframe として埋め込まれて開かれます。
- 起動 URL のプレースホルダ（`{token}` / `{host}` / `{appId}`）は起動時に値へ置き換えられます。
- TapFun 親フレームとの通信は [クライアント API](#client-api)（postMessage）で行います。

起動 URL の例:

```text
https://game.example.com/play?host={host}&token={token}&appId={appId}
```

### ユーザー認証 / TapFun ユーザー ID の取得 {#webapp-auth}

1. 起動 URL の `{token}` に渡された値を、ブラウザからパブリッシャー様のゲームサーバーへ送ります。
2. ゲームサーバーから [`POST /api/v1/create_session`](#api-create-session) を呼ぶと、TapFun ユーザー ID が返ります。
3. `{token}` の有効期限はテスト環境で 24 時間、本番環境で 180 秒です。有効期限内は繰り返し使用できます。

```bash
curl https://<TEST_HOST>/api/v1/create_session -X POST \
  -H "Authorization: Bearer BEARER_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{"token":"TOKEN_HERE"}'
# {"result":"success","userId":"PLYRABCDEFGHIJKLMNOP123456789APPGRP"}
```

Bearer token はサーバー側でのみ扱い、ブラウザに渡さないでください。

## クライアント API {#client-api}

ウェブアプリケーションと、親フレームである TapFun との通信用 API です。送信データはユーザーのブラウザ上でやり取りされるため、**秘匿情報は含めないでください。**

| API | 概要 |
| --- | --- |
| [purchaseItem](#api-purchaseitem) | アイテム購入 |
| [getPlayerEnvironment](#api-getplayerenvironment) | プレイヤー環境の取得 |
| [getOneTimeData](#api-getonetimedata) | URL の `onetime` 値を一度だけ取得 |
| [proposeAuthentication](#api-proposeauthentication) | ログインを促す |
| [openAccount](#api-openaccount) | アカウントページを開く |
| [reboot](#api-reboot) | 再起動 |
| [consoleLog](#api-consolelog) | ログ表示 |

### 通信仕様 {#client-api-transport}

| 項目 | 内容 |
| --- | --- |
| 送信 | `window.parent.postMessage(JSON 文字列, "https://" + {host})` |
| 受信 | `window` の `message` イベント。`event.data` は JSON 文字列です。 |
| 文字コード | UTF-8 |
| リクエスト / レスポンス形式 | JSON |

TapFun はメッセージの送信元が埋め込んだ iframe であること、およびそのオリジンが起動 URL のオリジンであることを確認します。受信側でも `event.origin` が `https://{host}` であることを確認してください。

```js
const params = new URLSearchParams(location.search);
const tapfunOrigin = 'https://' + params.get('host'); // 起動 URL に {host} を含めておく

function send(data) {
  window.parent.postMessage(JSON.stringify(data), tapfunOrigin);
}

window.addEventListener('message', (event) => {
  if (event.origin !== tapfunOrigin) return;
  const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
  console.log('from TapFun', data);
});

send({ req: 'getPlayerEnvironment' });
```

### purchaseItem {#api-purchaseitem}

アイテム購入の確認ダイアログを表示し、決済を行います。決済が成功すると、TapFun からパブリッシャー様のゲームサーバーへ [Callback API](#callback-api) で通知します。アイテムの付与は Callback を受けたゲームサーバー側で行ってください。

| パラメータ | 必須 | 説明 |
| --- | :-: | --- |
| `req` | ✓ | `purchaseItem`（固定） |
| `tran` | ✓ | トランザクション ID。アプリ側で生成する、アプリごとに一意な ID。過去に同じ `tran` の購入（成功・失敗を問わない）がある場合は購入できません |
| `code` | ✓ | 商品コード（アプリ側で自由に設定。TapFun 上の集計などに使用） |
| `item` | | 複合商品コード（アプリ側で自由に設定） |
| `amount` | ✓ | 金額（300〜10000） |
| `name` | ✓ | 商品名（アプリ側で自由に設定） |
| `img` | | 商品サムネイル画像の URL |
| `note` | | 自由入力フィールド |
| `ret` | | 戻り先。TapFun ウォレットへのチャージでページ遷移した場合に、アプリを再起動するときの起動 URL のパス以降を指定します。起動 URL として扱われるため、`{host}` などのプレースホルダが改めて必要です。`/` から始めてください。例: `/purchaseReturn?host={host}&token={token}&appId={appId}#anchor` |
| `sig` | ※ | 署名（[署名の作り方](#purchaseitem-sig)）。Publisher Console で「purchaseItem 時の sig 必須」を有効にしている場合は必須です。送った場合は常に検証されます |

リクエスト例:

```json
{
  "req": "purchaseItem",
  "tran": "UNIQUE_TRANSACTION_ID_HERE",
  "code": "code_type01",
  "item": "item_type01",
  "img": "https://example.com/img/item.png",
  "amount": 500,
  "name": "300ジェム",
  "ret": "/purchaseReturn?host={host}&token={token}&appId={appId}#anchor",
  "sig": "SIGNATURE_HASH_HERE"
}
```

#### 署名の作り方 {#purchaseitem-sig}

次の順に値を `&` で結合した文字列を、Publisher Console の sigKey をキーとして HMAC-SHA256 でハッシュ化し、16 進数（小文字）の文字列にした値です。

```text
tapfunUserId&tran&code&item&amount&name&img&note&ret
```

- `tapfunUserId` はリクエストパラメータには不要ですが、署名用の文字列には必要です。
- `item` / `img` / `note` / `ret` などが空の場合も省略せず、`&` を連続させます。例: `PLYR123456DBG&ABCD&gem300&&500&ジェム300&&&`
- 署名はパブリッシャー様のゲームサーバーで計算し、sigKey をブラウザに渡さないでください。

```js
// Node.js（ゲームサーバー側）
import { createHmac } from 'node:crypto';

function purchaseSig(sigKey, p) {
  const s = [p.tapfunUserId, p.tran, p.code, p.item ?? '', p.amount, p.name, p.img ?? '', p.note ?? '', p.ret ?? ''].join('&');
  return createHmac('sha256', sigKey).update(s).digest('hex');
}
```

#### レスポンス通知 {#purchaseitem-response}

すべての通知に `req`（`purchaseItem`）、`code`（リクエストした `code`）、`tran`（リクエストした `tran`）が付きます。

| `result` | タイミング | 追加キー |
| --- | --- | --- |
| `received` | 購入イベントを受信し、確認ダイアログを開いた | — |
| `purchaseCancel` | 確認ダイアログが閉じられた（購入キャンセル） | — |
| `purchase` | ダイアログ上で購入ボタンが押された | — |
| `purchaseSuccess` | 決済に成功した | — |
| `incompleteCallback` | 購入に失敗した（失敗のモーダルを表示した時点で送信） | `reason`（下表） |
| `parameterError` | パラメータ不足、`sig` の不一致など | `reason`: エラー内容（例: `required param not found "code"`） |

| `reason` | 説明 |
| --- | --- |
| `duplicateTran` | 過去の購入と `tran` が重複している |
| `unfinishedClallbackPrucahseRemaining` | 完了していない Callback がある（通信エラーなど）。綴りはこのとおりです |
| `rejected` | Callback のレスポンスの `result` が `reject` だった |
| `unknown` | 原因不明で Callback の結果が得られなかった |
| `invalidSignature` | 署名が一致しなかった |

- 過去の購入と同じ `tran` の場合は、購入前のチェックでエラーダイアログを表示します。タイミングによって購入前のチェックを通過した場合も、購入ボタンを押した後に同じ `tran` を検出するとエラーダイアログを表示します。
- 同一 `tran` のエラーを再現するには、TapFun のゲーム実行ページの URL に `?purchasableCheck=0` を付けて開き、ゲームから同じ `tran` で purchaseItem を呼び出してください。購入前のチェックをスキップして購入ボタンを押せるようになります。
- ブラウザが受け取ったリクエストの内容は、ブラウザのデベロッパーツールのコンソールで確認できます。

### getPlayerEnvironment {#api-getplayerenvironment}

プレイヤーの環境を取得します。

| パラメータ | 必須 | 説明 |
| --- | :-: | --- |
| `req` | ✓ | `getPlayerEnvironment`（固定） |

レスポンス通知:

| キー | 説明 |
| --- | --- |
| `from` | `getPlayerEnvironment` |
| `userAgent` | ブラウザの `navigator.userAgent` |
| `isSP` | `1`: スマートデバイス（iPhone / iPad / Android） / `0`: それ以外 |
| `authenticated` | `1`: ログイン中 / `0`: ゲスト状態 |


### getOneTimeData {#api-getonetimedata}

TapFun のページ URL のクエリストリング `onetime` の値を一度だけ取得します。ディープリンクなどに使えます。リクエストを受けると URL 上の `onetime` は消えます。

| パラメータ | 必須 | 説明 |
| --- | :-: | --- |
| `req` | ✓ | `getOneTimeData`（固定） |

レスポンス通知:

| キー | 説明 |
| --- | --- |
| `from` | `getOneTimeData` |
| `onetime` | `onetime` の値（デコード済み）。取得済みまたは無い場合は空文字 |

例: URL が `https://tapfun.co.jp/p/dummy/play?onetime=abcdefg` の場合

```json
// 1 回目
{ "from": "getOneTimeData", "onetime": "abcdefg" }
// 2 回目（URL から onetime が消えているため）
{ "from": "getOneTimeData", "onetime": "" }
```

値をエンコードした URL の場合:

```json
// ?onetime=page%3Dqrcode%26code%3Daaaa
{ "from": "getOneTimeData", "onetime": "page=qrcode&code=aaaa" }
// ?onetime=abc%0D%0Adef%0D%0Aghi
{ "from": "getOneTimeData", "onetime": "abc\r\ndef\r\nghi" }
```

### proposeAuthentication {#api-proposeauthentication}

ユーザーにログインを促します。ゲスト状態の場合は新規登録 / ログインモーダルを表示します。

| パラメータ | 必須 | 説明 |
| --- | :-: | --- |
| `req` | ✓ | `proposeAuthentication`（固定） |

レスポンス通知:

| キー | 説明 |
| --- | --- |
| `from` | `proposeAuthentication` |
| `authenticated` | `1`: ログイン中だったので何もしなかった / `0`: ゲスト状態だったのでモーダルを表示した |

### openAccount {#api-openaccount}

アカウントページを開きます。ゲスト状態の場合は新規登録 / ログインモーダルを表示します。

| パラメータ | 必須 | 説明 |
| --- | :-: | --- |
| `req` | ✓ | `openAccount`（固定） |

レスポンス通知: なし

### reboot {#api-reboot}

ゲームを再起動します。プレイヤーが TapFun UI 上の再起動アイコンをタップしたときと同じ動作です。

| パラメータ | 必須 | 説明 |
| --- | :-: | --- |
| `req` | ✓ | `reboot`（固定） |

レスポンス通知: なし

### consoleLog {#api-consolelog}

TapFun のログ表示領域にログを追加します。ブラウザのデベロッパーツールのコンソールにも出力されます。

| パラメータ | 必須 | 説明 |
| --- | :-: | --- |
| `req` | ✓ | `consoleLog`（固定） |
| `data` | ✓ | 文字列または JSON 文字列。`JSON.parse()` できた場合は JSON オブジェクトとしてコンソールに表示します。 |

レスポンス通知: なし

## Callback API（アイテム購入完了通知） {#callback-api}

[purchaseItem](#api-purchaseitem) の決済が成功すると、TapFun サーバーからパブリッシャー様のゲームサーバーへ購入内容を通知します。

### 仕様 {#callback-spec}

| 項目 | 内容 |
| --- | --- |
| 通知先 URL | Publisher Console の「callback HTTP POST URL」 |
| 接続元 IP アドレス | テスト環境: 担当者にお問い合わせください / 本番: `54.178.93.41` |
| メソッド | HTTP POST |
| Content-Type | `application/json` |
| 文字コード | UTF-8 |
| 応答 | JSON（[レスポンス](#callback-response)） |
| タイムアウト | 5 秒 |

リクエストヘッダー:

| ヘッダー | 説明 |
| --- | --- |
| `X-TapFun-Callback-Key` | Callback ごとのユニークキー |
| `X-TapFun-Retry` | リトライ回数（リトライの場合のみ） |
| `X-TapFun-Signature` | リクエストボディを sigKey で HMAC-SHA256 署名した値（16 進数）。受信したボディの文字列そのものから計算して照合してください |

```js
// Node.js（Express）での署名の検証例
import { createHmac, timingSafeEqual } from 'node:crypto';

app.post('/tapfun/callback', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = createHmac('sha256', SIG_KEY).update(req.body).digest('hex');
  const actual = req.get('X-TapFun-Signature') || '';
  if (expected.length !== actual.length || !timingSafeEqual(Buffer.from(expected), Buffer.from(actual))) {
    return res.status(400).json({ result: 'error' }); // success / reject 以外はリトライされる
  }
  const purchase = JSON.parse(req.body);
  // purchase.tran で冪等に処理し、アイテムを付与する
  res.json({ result: 'success' });
});
```

### 通知とリトライ {#callback-retry}

- 初回の失敗と復旧を、Publisher Console の「callback Notice Email」と「callback Notice Webhook URL」に通知します。
- ユーザーの購入時は、失敗またはタイムアウトの後 0.5 秒待って 3 回リトライします。3 回とも失敗すると、ユーザーに失敗のモーダルを表示します。
- その後は次の間隔でリトライします。
  - 10 分後まで: 1 分ごと
  - 60 分後まで: 10 分ごと
  - 24 時間後まで: 1 時間ごと
  - 14 日後まで: 1 日ごと
  - それ以降は Publisher Console から手動で再実行する必要があります。
- Callback が完了していない購入がある間は、同じ `code` の商品を続けて購入できません。

### リクエスト {#callback-request}

| キー | 説明 |
| --- | --- |
| `req` | `purchaseItem` |
| `event` | `purchaseItem` |
| `result` | `success` |
| `key` | 購入ごとのユニークキー |
| `appId` | アプリ ID |
| `userId` | TapFun ユーザー ID |
| `publisherUserId` | パブリッシャー様側のユーザー ID。未設定の場合は `null` |
| `fromRegisterDevice` | 購入ユーザーが TapFun に新規登録したデバイス（`SP` / `PC`） |
| `purchaseDevice` | 購入時のデバイス（`SP` / `PC`） |
| `salesDistinction` | 売上区分（`Publisher` / `Platform`） |
| `tran` | トランザクション ID |
| `code` | 商品コード |
| `item` | 複合商品コード、または `null` |
| `amount` | 金額 |
| `name` | 商品名 |
| `img` | 画像 URL、または `null` |
| `note` | 補足、または `null` |
| `createdAt` | 購入日時（UTC） |

```json
{
  "req": "purchaseItem",
  "event": "purchaseItem",
  "result": "success",
  "key": "01HRC7QZV1GVT0WX9G6EKSYCAS",
  "appId": 1,
  "userId": "PLYR01HE2R3JV6Y2NEGRMS4KZQB861DBG",
  "publisherUserId": null,
  "fromRegisterDevice": "PC",
  "purchaseDevice": "PC",
  "salesDistinction": "Publisher",
  "tran": "abcdef123456",
  "code": "gem300",
  "item": "itemGem300",
  "amount": 500,
  "name": "300ジェム",
  "img": "https://example.com/img/item.png",
  "note": null,
  "createdAt": "2024-03-06T06:37:22.000Z"
}
```

### レスポンス {#callback-response}

ゲームサーバーは、JSON の `result` に `success` か `reject` を返してください。それ以外の場合、TapFun はリトライします。HTTP ステータスは判定に使いませんが、200 を推奨します。リダイレクトには対応していません。

```json
// 購入成功
{ "result": "success" }
// 購入拒否
{ "result": "reject" }
```

- `success` の場合、正式に売上が計上されます。
- `reject` の場合、購入は取り消され、ユーザーのウォレット残高は減りません（ユーザーの利用履歴には購入と返金が記載されます）。
- ゲームサーバー側で同じ `tran` を検出した場合は、既存の購入の結果を返してください（冪等性の確保）。例えば、同じ `tran` の既存の購入が成功していれば `success` を返します。

## Web API {#web-api}

パブリッシャー様のサーバーから呼び出す HTTP API です。Bearer token はサーバー側でのみ扱い、ブラウザに渡さないでください。

### 共通仕様 {#web-api-common}

| 項目 | 内容 |
| --- | --- |
| ベース URL | `https://<環境のホスト名>/api/v1`（[ホスト名](#hosts)） |
| メソッド | HTTP GET / POST |
| 文字コード | UTF-8 |
| リクエスト形式 | URL パス、クエリストリング、POST データ（`application/x-www-form-urlencoded`。個別に記載があるものを除く） |
| レスポンス形式 | JSON。配列の結果が 0 件の場合は空配列。HTTP ステータスは記載が無い限り 200 |
| 日時 | UTC、`yyyy-mm-ddThh:mm:ss.sssZ` |

### 認証 {#web-api-auth}

HTTP ヘッダー `Authorization: Bearer <アプリID>_<APIキー>`（Publisher Console の Bearer token）で認証します。

```text
Authorization: Bearer 123_NWU0MjlkY2ViYTVkOTdiYjIwNThmZTkxMDMzYWM4Y2JmNGUyMTQ0Ng
```

認証失敗時は HTTP 403 で次を返します。

```json
{"error":"no authorization"}
```

### POST /api/v1/create_session {#api-create-session}

起動 URL の `{token}` から TapFun ユーザー ID を取得します（[ユーザー認証](#webapp-auth)）。認証: 必要。

| POST データ | 必須 | 説明 |
| --- | :-: | --- |
| `token` | ✓ | 起動 URL の `{token}` に渡された値 |

```json
// 成功
{ "result": "success", "userId": "TapFunユーザーID" }
// 失敗（400）: トークンが無い
{ "result": "failure", "message": "token is missing" }
// 失敗（400）: トークンが不正
{ "result": "failure", "message": "token is invalid" }
```

### GET /api/v1/purchases {#api-purchases}

購入履歴を取得します。結果は新しい順に並びます。Callback を取りこぼした購入の確認などに使えます。認証: 必要。

| クエリストリング | 必須 | 説明 |
| --- | :-: | --- |
| `userId` | | TapFun ユーザー ID。省略するとアプリ全体の購入を返します |
| `key` | | 購入キー |
| `tran` | | トランザクション ID |
| `limit` | | 件数（1〜100、デフォルト 10）。範囲外は 10 |
| `nextKey` | | 続きの結果を取得するためのキー（前回のレスポンスの `nextKey`） |

```json
// 成功
{
  "nextKey": "Purchase01HE2R3JV6Y2NEGRMS4KZQB858",   // 続きが無い場合は null
  "result": [
    {
      "key": "Purchase01HE2R3JV6Y2NEGRMS4KZQB861",
      "appId": 1,
      "userId": "PLYR01HE2R3JV6Y2NEGRMS4KZQB861DBG",
      "tran": "abcdef123456",
      "code": "abcdef123456",
      "item": "abcdef123456",
      "amount": 500,
      "name": "300ジェム",
      "img": null,
      "callbackSuccess": false,
      "createdAt": "2024-03-06T06:37:22.000Z"
    }
  ]
}
```

- `callbackSuccess` は Callback が成功したかどうかです。
- Publisher Console の「purchases API 互換モード」が有効な場合、`userId` の末尾（suffix）が数文字短い形式になります。通常は無効にしてください。
- 失敗のレスポンスはありません（該当が無い場合は空の配列）。

### POST /api/v1/photos {#api-photos}

TapFun フォトストレージに画像を保存します。ユーザーは TapFun 上で、ゲームから保存された画像を閲覧・ダウンロードできます。認証: 必要。

| 項目 | 内容 |
| --- | --- |
| Content-Type | `multipart/form-data` |
| クエリストリング `userId` | TapFun ユーザー ID（必須） |
| POST データ `file` | PNG / JPEG / WebP の画像バイナリ（必須）。part の `Content-Disposition` の `filename` は任意の値で必須（例: `form-data; name="file"; filename="sample.jpg"`） |

制約:

- 最大保存枚数: 1,000 枚（超えた場合は古いものから自動削除）
- 最大容量: 1 枚あたり 10 MB（全体の容量制限はありません）
- 最大の高さ・幅: 12,000 ピクセル
- 最大画像サイズ: 100M ピクセル（例: 10,000 × 10,000）
- 連続呼び出しの待機時間・呼び出し回数の制限: なし

```bash
curl -F "file=@sample.jpg;type=image/jpeg" \
  -H "Authorization: Bearer 1_8an4dAfsIyEEvBbaKxd55P2Wgf" \
  "https://<TEST_HOST>/api/v1/photos?userId=PLYR01JY39A09EE40YQZRHWTT9EM1VDBG"
```

```json
// 成功
{ "result": "success" }
// 失敗（404）: ユーザー ID が無い
{ "result": "failure", "message": "userId is not found" }
// 失敗（400）: その他（file が無い、大きすぎる、形式が不正など）
{ "result": "failure", "message": "other problems." }
```

### GET /api/v1/leaderboard/entries {#api-leaderboard-get}

リーダーボードのエントリー一覧を取得します。認証: 必要。

| クエリストリング | 必須 | 説明 |
| --- | :-: | --- |
| `name` | ✓ | リーダーボード名 |
| `sort` | | `asc`: 昇順 / `desc`: 降順（デフォルト） |
| `userId` | | ユーザー ID |
| `arround` | | 指定ユーザーの周辺のエントリーを取得します（1〜100）。`userId` 指定時に機能します |
| `offset` | | 表示開始位置（0〜9999）。範囲外は 0 |
| `limit` | | 件数（1〜1000、デフォルト 100）。範囲外は 100 |

```json
// 成功
{
  "result": "success",
  "entries": [
    {
      "position": 1,
      "userId": "PLYR01HE2R3JV6Y2NEGRMS4KZQB861DBG",
      "userName": "ユーザー名",          // UTF-8、絵文字不可、1〜12 文字
      "score": 12345,
      "note": "追加情報",                // 最大 8192 文字
      "createdAt": "2025-09-24T00:00:00.000Z",
      "updatedAt": "2025-09-24T00:00:00.000Z"
    }
  ]
}
// 失敗（404）: ユーザー ID が無い
{ "result": "failure", "message": "userId is not found" }
// 失敗（400）: その他
{ "result": "failure", "message": "other problems." }
```

### POST /api/v1/leaderboard/entries {#api-leaderboard-post}

リーダーボードのエントリーを作成・更新します。既存のスコア値以下（同値を含む）の場合は更新しません。リーダーボードは `name` ごとに自動で作られます。認証: 必要。

| POST データ | 必須 | 説明 |
| --- | :-: | --- |
| `name` | ✓ | リーダーボード名。英数字・`_`・`-` の 1〜64 文字（`/^[0-9a-zA-Z_-]{1,64}$/`） |
| `userId` | ✓ | ユーザー ID |
| `score` | ✓ | スコア値（-9,223,372,036,854,775,808〜9,223,372,036,854,775,807） |
| `note` | | 追加情報（UTF-8、0〜8192 文字） |
| `force` | | `1` を渡すと、既存のスコア値を下回る場合でも登録します |

```json
// 成功
{ "result": "success" }
// 失敗（404）: ユーザー ID が無い
{ "result": "failure", "message": "userId is not found" }
// 失敗（400）: その他
{ "result": "failure", "message": "other problems." }
```

### POST /api/v1/leaderboard/entries/delete {#api-leaderboard-delete}

リーダーボードのエントリーを削除します。エントリーが無い場合も成功を返します。認証: 必要。

| POST データ | 必須 | 説明 |
| --- | :-: | --- |
| `name` | ✓ | リーダーボード名（`/^[0-9a-zA-Z_-]{1,64}$/`） |
| `userId` | ✓ | ユーザー ID |

```json
// 成功
{ "result": "success" }
// 失敗（404）: ユーザー ID が無い
{ "result": "failure", "message": "userId is not found" }
// 失敗（400）: その他
{ "result": "failure", "message": "other problems." }
```

## 用語 {#glossary}

### TapFun ユーザー ID {#glossary-user-id}

- ユーザーを特定する最大 64 桁のユニーク ID です。アプリごとに発行されます。
- 文字種は `0-9`, `a-z`, `A-Z` です。
- 形式は `PLYR` + ULID + アプリごとの suffix（3〜7 文字。例: `DBG`, `XENO`, `MOCODVLP`）です。

### アプリ ID {#glossary-app-id}

アプリを特定する unsigned int 型の ID です。

### API キー {#glossary-api-key}

アプリごとに発行される 26 桁の文字列です。文字種は `0-9`, `A-Z` です。


### Publisher Console {#glossary-publisher-console}

パブリッシャー様向けの管理画面です。アプリの設定、紹介ページ、売上の確認などを行います。テスト環境と本番環境で別々にあり、ログインできる方は TapFun が登録します。

### 紹介ページ {#glossary-introduction-page}

TapFun 上のゲームのページです（`https://tapfun.co.jp/p/<アプリ>`）。ゲームのプレイ画面は `https://tapfun.co.jp/p/<アプリ>/play` です。内容は Publisher Console の[詳細情報](#pc-detail)で編集します。

### ゲスト {#glossary-guest}

TapFun にログインしていない状態のプレイヤーです。

### TapFun ウォレット {#glossary-wallet}

プレイヤーが TapFun でチャージする残高です。TapFun 上での購入に使います。

### TapFun クレジット {#glossary-credit}

本番環境のテストプレイ用アカウントに付与される、対象のアプリでのみ使えるウォレット残高です（[本番でのテストプレイ](#production-test-play)）。

### 商品コード / 複合商品コード {#glossary-item-code}

- **商品コード（`code`）**: 商品の種類を表すコードです。TapFun 上での集計に使います。Callback が完了していない購入があるときは、同じ商品コードの商品を続けて購入できません。
- **複合商品コード（`item`）**: アプリ側で自由に使える補助のコードです。TapFun は内容を解釈せず、Callback API と purchases API でそのまま返します。

## 変更履歴 {#changelog}

| 日付 | 内容 |
| --- | --- |
| 2026-10-02 | このドキュメントを公開しました。 |
