APIチュートリアル

このチュートリアルでは、XGを利用してユーザーのゲーム内通貨の発行、消費までの一連の流れについて説明します。

なお、この章では環境準備及び認証処理についてはすでに完了しているものとします。

APIへの通信の共通処理の実装

ここではPHP言語とGo言語でリクエストの送信とレスポンスの受け取りを行う例を示します。

APIへの通信の共通処理の実装は サンプル を参考にしてください。

XG用のユーザーIDの発行

XGを利用するためには、「ゲーム側のユーザーID」に対応する「XG側のユーザーID」の発行が必要です。

以下のコードは、リクエストの送信とレスポンスの受け取りを行う例です。

<?php

// リクエストの送信
$method = 'POST';                                     // HTTPメソッドの種類
$path = '/user/v1/users';                             // APIのパス
$body = json_encode(["gameUserId" => "1234567890"]);  // リクエストボディー

try {
    $response = $xgApi->request($method, $path, $body);
} catch (BadResponseException $e) {
    errorResponse($e->getResponse()->getBody());
}

// レスポンスの解析
$userId = "";
if ($response->getStatusCode() == 201) {
    $res = json_decode($response->getBody());
    if (is_null($res)) {
        $err = json_last_error_msg();
        throw new \Exception(sprintf("JSON Decode error: %s", $err));
    }
    printf("(XG)UserID: %s\n", $res->id);

    // このユーザーIDとゲームサーバー側のユーザーIDを紐付けしてください
    $userId = $res->id;
}else{
    errorResponse($response->getBody());
}
// リクエスト用の構造体
type Body struct {
	GameUserId string `json:"gameUserId"`
}

// レスポンス用の構造体
type PostUserResponse struct {
	ID         string `json:"id"`
	GameUserId string `json:"gameUserId"`
	CreatedAt  string `json:"createdAt"`
}

// リクエストの送信
method := http.MethodPost // HTTPメソッドの種類
path := "/user/v1/users"  // APIのパス
requestBodyBytes, err := json.Marshal(Body{
	GameUserId: "1234567890",
})
if err != nil {
	panic(err)
}
body := string(requestBodyBytes)

response, err := xgApi.SendRequest(method, path, body)
if err != nil {
	panic(err)
}
defer response.Body.Close()

// レスポンスの解析
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
	panic(err)
}

if response.StatusCode == http.StatusCreated {
	var res PostUserResponse
	if err := json.Unmarshal(bodyBytes, &res); err != nil {
		log.Fatalf("JSON Unmarshal error: %v", err)
	} else {
		fmt.Printf("(XG)UserID: %s\n", res.ID)

		// このユーザーIDとゲームサーバー側のユーザーIDを紐付けしてください
		userId = res.ID
	}
	return userId, err
} else {
	// エラー処理
}

実行時の出力結果のイメージは以下のようになります。

(XG)UserID: 8ef80b0a-ef80-4105-a978-3943f51bd14c

この手順で取得したユーザーIDと、「ゲーム側のユーザーID」を紐付けして保存してください。

これ以降、ゲーム内通貨の操作をする際にはこのユーザーIDを利用してリクエストしてください。

無償通貨の発行

次に「ログインボーナス」として無償通貨「gem」を1000個発行します。

ゲーム内通貨を操作するリクエストをする際には、ゲームサーバー側で取引ID(transactionID)の発行が必要です。

<?php

// リクエストの準備
$storeId = "googleplay";

$transactionId = Uuid::uuid4()->toString();    // 取引IDを作成します。(UUIDを利用)

$method = 'PUT';                                                                    // HTTPメソッドの種類
$path = sprintf("/currency/v1/users/%s/stores/%s/issue/free", $userId, $storeId);   // APIのパス
$body = json_encode([
    'transactions' => [
        [
            'transactionId' => $transactionId,
            'description' => 'ログインボーナス',
            'currency' => [
                'gem' => [
                    'quantity' => 1000,
                ],
            ],
        ],
    ],
]);

try {
    $response = $xgApi->request($method, $path, $body);
} catch (BadResponseException $e) {
    errorResponse($e->getResponse()->getBody());
}

// レスポンスの解析
if ($response->getStatusCode() == 200) {
    // responseBody(JSON)を構造体に変換
    $res = json_decode($response->getBody());
    if (is_null($res)) {
        // エラー
        $err = json_last_error_msg();
        throw new \Exception(sprintf("JSON Decode error: %s, response: %s", $err, $response->getBody()));
    }
    // 現在のゲーム内通貨の残高を表示
    if (isset($res->balance)) {
        foreach ($res->balance as $currencyId => $balance) {
            printf("%s 無償: %d, 有償: %d\n", $currencyId, $balance->free, $balance->paid);
        }
    }
} else {
    // エラー処理
}
// リクエスト用の構造体
type XgCurrencyDetail struct {
	Quantity int64      `json:"quantity"`
	ExpiryAt *time.Time `json:"expiryAt,omitempty"`
}
type XgTransactionRequestDetail struct {
	TransactionId string                      `json:"transactionId"`
	Description   string                      `json:"description"`
	Currency      map[string]XgCurrencyDetail `json:"currency"`
}
type Body struct {
	Transactions []XgTransactionRequestDetail `json:"transactions"`
}

// レスポンス用の構造体
type XgTransactionResponseDetail struct {
	TransactionId string                      `json:"transactionId"`
	TransactionAt time.Time                   `json:"transactionAt"`
	Status        string                      `json:"status"`
	Description   string                      `json:"description"`
	Currency      map[string]XgCurrencyDetail `json:"currency"`
}
type XgBalanceDetail struct {
	Free int64 `json:"free"`
	Paid int64 `json:"paid"`
}
type XgIsueeFreeResponse struct {
	Status       string                        `json:"status"`
	Transactions []XgTransactionResponseDetail `json:"transactions"`
	Balance      map[string]XgBalanceDetail    `json:"balance"`
}

// リクエストの準備
storeId := "googleplay"

transactionId, err := uuid.NewV4()    // 取引IDを作成します。(UUIDを利用)

if err != nil {
	return err
}

method := http.MethodPut                                                           // HTTPメソッドの種類
path := fmt.Sprintf("/currency/v1/users/%s/stores/%s/issue/free", userId, storeId) // APIのパス
requestBodyBytes, err := json.Marshal(Body{
requestBodyBytes, err := json.Marshal(Body{
	Transactions: []XgTransactionRequestDetail{
		{
			TransactionId: transactionId.String(),
			Description:   "ログインボーナス",
			Currency: map[string]XgCurrencyDetail{
				"gem": {
					Quantity: 1000,
				},
			},
		},
	},
})
if err != nil {
	panic(err)
}
body := string(requestBodyBytes)

response, err := xgApi.SendRequest(method, path, body)
if err != nil {
	panic(err)
}
defer response.Body.Close()

// レスポンスの解析
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
	panic(err)
}

if response.StatusCode == http.StatusOK {
	// responseBody(JSON)を構造体に変換
	var res XgIsueeFreeResponse
	if err := json.Unmarshal(bodyBytes, &res); err != nil {
		// エラー
		return err
	}
	// 現在のゲーム内通貨の残高を表示
	for currencyId, balance := range res.Balance {
		fmt.Printf("%s 無償: %d, 有償: %d\n", currencyId, balance.Free, balance.Paid)
	}
} else {
	// エラー処理
}

ゲーム内通貨(無償通貨のみ)の消費

次にゲーム内で無償通貨「gem」を500消費して、ガチャチケットを1枚獲得する例を説明します。

  1. ゲーム内通貨を操作するリクエストをする際には、ゲームサーバー側で取引ID(transactionID)の発行が必要です。

  2. ゲーム内通貨の消費状態と、ゲーム内でのアイテム発行状態を管理するレコードを作成してください。(ここでは「取引状態管理テーブル」と表現します。)

    このテーブルでは、XG側のレスポンスと、ゲーム側のアイテム付与状態に応じて状態を管理するようにします。

    これは処理が中断した際の再開処理をする際に必要となります。

  3. REQUEST BODYのパラメーター「currencyType」に「free」を指定して消費リクエストを呼び出します。(なお、「currencyType」を指定しない場合は、無償通貨もしくは有償通貨は予め登録してあるゲーム内通貨の消費順に従って消費します。)

  4. XGから成功レスポンスが戻ってきましたので「取引状態管理テーブル」の状態を「ゲーム内通貨消費済み」に更新します。

  5. ゲーム内アイテム「ガチャチケット」を1枚発行します。

  6. アイテムの付与が完了しましたら、「取引状態管理テーブル」の状態を「アイテム発行済み」に更新します。 これで一連のゲーム内通貨の消費とアイテムの発行処理が完了となります。

<?php

//-----------------------------------------------------------
// ゲーム内通貨の消費状態とアイテム付与の状態管理のための実装
//-----------------------------------------------------------
class TransactionConsumeStatus{
    public function setConsumeCurrencyStatus($transactionId, $userId, $status) {
        // データベース更新処理などを記入
    }
}

//-----------------------------------------------------------
// 無償通貨500gem消費し、ガチャチケット1枚獲得する実装イメージ
//-----------------------------------------------------------

// ゲーム内通貨の消費とアイテム付与の状態管理するクラス
$transactionStatus = new TransactionConsumeStatus();

// リクエストの準備
$storeId = "googleplay";

$transactionId = Uuid::uuid4()->toString();

// リカバリー対策として、取引状態及びアイテム等の付与状態を保存する
$transactionStatus->setConsumeCurrencyStatus($transactionId, $userId, "処理前");

$method = 'PUT';                                                                // HTTPメソッドの種類
$path = sprintf("/currency/v1/users/%s/stores/%s/consume", $userId, $storeId);  // APIのパス

// 無償通貨500gem消費して、ガチャチケット1枚獲得
$body = json_encode([
    'transactionId' => $transactionId,
    'description' => 'ガチャチケット',
    'quantity' =>      1,
    'currencyType' =>  "free",
    'transaction' => [
        "gem" => 500,
    ],
]);

try {
    $response = $xgApi->request($method, $path, $body);
} catch (BadResponseException $e) {
    errorResponse($e->getResponse()->getBody());
}

// レスポンスの解析
if ($response->getStatusCode() == 200) {
    $res = json_decode($response->getBody());
    if (is_null($res)) {
        //エラー
        $err = json_last_error_msg();
        throw new \Exception(sprintf("JSON Decode error: %s, response: %s", $err, $response->getBody()));
    }

    // ゲーム内通貨の残高
    if (isset($res->balance)) {
        foreach ($res->balance as $currencyId => $balance) {
            printf("%s 無償: %d, 有償: %d\n", $currencyId, $balance->free, $balance->paid);
        }
    }

    // XG側でゲーム内通貨を消費済み状態に変更
    $transactionStatus->setConsumeCurrencyStatus($transactionId, $userId, "ゲーム内通貨消費済み");

    /*
        ガチャチケットを1枚獲得
    */

    // アイテム付与完了状態にする
    $transactionStatus->setConsumeCurrencyStatus($transactionId, $userId, "アイテム付与済み");
} else {
	// エラー処理
}
//-----------------------------------------------------------
// ゲーム内通貨の消費状態とアイテム付与の状態管理のための実装
//-----------------------------------------------------------
type TransactionConsumeStatus struct{}

func (t *TransactionConsumeStatus) setConsumeCurrencyStatus(transactionId any, userId any, status any) error {
	// データベース更新処理などを記入
	return nil
}


//-----------------------------------------------------------
// 無償通貨500gem消費し、ガチャチケット1枚獲得する実装イメージ
//-----------------------------------------------------------

// リクエスト用の構造体
type Body struct {
	TransactionId string           `json:"transactionId"`
	Description   string           `json:"description"`
	Quantity      int64            `json:"quantity"`
	CurrencyType  string           `json:"currencyType"`
	Transaction   map[string]int64 `json:"transaction"`
}

// ゲーム内通貨の消費とアイテム付与の状態管理する構造体
transactionStatus := TransactionConsumeStatus{}

// レスポンス用の構造体
type XgBalanceDetail struct {
	Free int64 `json:"free"`
	Paid int64 `json:"paid"`
}

type XgConsumeResponse struct {
	// TransactionId 取引ID
	TransactionId string `json:"transactionId"`

	// TransactionAt ゲーム内通貨消費日時(UTC)
	TransactionAt time.Time `json:"transactionAt"`

	// Status 結果ステータス
	Status string `json:"status"`

	// StoreId ストアID
	StoreId string `json:"storeId"`

	// Balance ゲーム内通貨の残高
	Balance map[string]XgBalanceDetail `json:"balance"`
}

// リクエストの準備
storeId := "googleplay"

transactionId, err := uuid.NewV4()
if err != nil {
	panic(err)
}

// リカバリー対策として、取引状態及びアイテム等の付与状態を保存する
err = transactionStatus.setConsumeCurrencyStatus(transactionId, userId, "処理前")
if err != nil {
	// エラー処理
	panic(err)
}

method := http.MethodPut                                                        // HTTPメソッドの種類
path := fmt.Sprintf("/currency/v1/users/%v/stores/%v/consume", userId, storeId) // APIのパス

// 無償通貨500gem消費して、ガチャチケット1枚獲得
requestBodyBytes, err := json.Marshal(Body{
	TransactionId: transactionId.String(),
	Description:   "ガチャチケット",
	Quantity:      1,
	CurrencyType:  "free",
	Transaction: map[string]int64{
		"gem": 500,
	},
})
if err != nil {
	panic(err)
}
body := string(requestBodyBytes)

response, err := xgApi.SendRequest(method, path, body)
if err != nil {
	panic(err)
}
defer response.Body.Close()

// レスポンスの解析
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
	panic(err)
}

if response.StatusCode == http.StatusOk {
	var res XgConsumeResponse
	if err := json.Unmarshal(bodyBytes, &res); err != nil {
		// エラー
		panic(err)
	}

	// ゲーム内通貨の残高
	for currencyId, balance := range res.Balance {
		fmt.Printf("%s 無償: %d, 有償: %d\n", currencyId, balance.Free, balance.Paid)
	}

	// XG側でゲーム内通貨を消費済み状態に変更
	err = transactionStatus.setConsumeCurrencyStatus(transactionId, userId, "ゲーム内通貨消費済み")
	if err != nil {
		// エラー処理
		panic(err)
	}

	/*
		ガチャチケットを1枚獲得
	*/

	// アイテム付与完了状態にする
	err = transactionStatus.setConsumeCurrencyStatus(transactionId, userId, "アイテム付与済み")
	if err != nil {
		// エラー処理
		panic(err)
	}

} else {
	// エラー処理
}

課金アイテム購入に伴うゲーム内通貨の発行

App Store

ストア「App Store」で"someProductId"という品番の商品を購入したことをXGに登録します。

App Storeで購入処理をした際にストアから発行されたトランザクションID(TransactionID)をXGへリクエストしてください。
XGでは、登録されている品番"someProductId"の内容に基づいてゲーム内通貨を発行します。
<?php

// リクエストの準備
$storeId = "appstore";

$method = 'PUT';                                                        // HTTPメソッドの種類
$path = sprintf("/purchase/v1/users/%s/stores/%s", $userId, $storeId);  // APIのパス
$body = json_encode([
    'transactionId' => 'someTransactionId',     // ストアから発行されたトランザクションID
    'productId' => 'someProductId',             // 商品ID
]);

try {
    $response = $xgApi->request($method, $path, $body);
} catch (BadResponseException $e) {
    errorResponse($e->getResponse()->getBody());
}

// レスポンスの解析
if ($response->getStatusCode() == 200) {
    $res = json_decode($response->getBody());
    if (is_null($res)) {
        // エラー
        $err = json_last_error_msg();
        throw new \Exception(sprintf("JSON Decode error: %s, response: %s", $err, $response->getBody()));
    }
    if (isset($res->balance)) {
        foreach ($res->balance as $currencyId => $balance) {
            printf("%s 無償: %d, 有償: %d\n", $currencyId, $balance->free, $balance->paid);
        }
    }
} else {
	// エラー処理
}
// リクエスト用の構造体
type Body struct {
	TransactionId string `json:"transactionId"`
	ProductId     string `json:"productId"`
}

// レスポンス用の構造体
type XgBalanceDetail struct {
	Free int64 `json:"free"`
	Paid int64 `json:"paid"`
}

type XgPurchaseStoreResponse struct {
	TransactionId string                     `json:"transactionId"`
	TransactionAt time.Time                  `json:"transactionAt"`
	Quantity      int64                      `json:"quantity"`
	Balance       map[string]XgBalanceDetail `json:"balance"`
	Added         map[string]XgBalanceDetail `json:"added"`
}

// リクエストの準備
storeId := "appstore"

method := http.MethodPut                                                // HTTPメソッドの種類
path := fmt.Sprintf("/purchase/v1/users/%v/stores/%v", userId, storeId) // APIのパス
requestBodyBytes, err := json.Marshal(Body{
	TransactionId: "someTransactionId", // ストアから発行されたトランザクションID
	ProductId:     "someProductId",     // 商品ID
})
if err != nil {
	panic(err)
}
body := string(requestBodyBytes)

response, err := xgApi.SendRequest(method, path, body)
if err != nil {
	panic(err)
}
defer response.Body.Close()

// レスポンスの解析
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
	panic(err)
}

if response.StatusCode == http.StatusOK {
	var res XgPurchaseStoreResponse
	if err := json.Unmarshal(bodyBytes, &res); err != nil {
		// エラー
		panic(err)
	}
	for currencyId, balance := range res.Balance {
		fmt.Printf("%s 無償: %d, 有償: %d\n", currencyId, balance.Free, balance.Paid)
	}
} else {
	// エラー処理
}

DMM GAMES

ストア「DMM GAMES」で"someProductId"という品番の商品を購入したことをXGに登録します。

DMM GAMESで購入処理をした際にストアから発行された購入トークン(PurchaseToken)をTransactionIDとしてXGへリクエストしてください。
XGでは、登録されている品番"someProductId"の内容に基づいてゲーム内通貨を発行します。
<?php

// リクエストの準備
$storeId = "dmmgames";

$method = 'PUT';                                                        // HTTPメソッドの種類
$path = sprintf("/purchase/v1/users/%s/stores/%s", $userId, $storeId);  // APIのパス
$body = json_encode([
    'transactionId' => $transactionId,  // 購入トークン
    'productId' => $productId,          // 商品ID
    'games' => [
        "appId"  => $gamesAppId,        // GAMESアプリID
        "device" => $device,            // デバイス(android, ios, win, mac, pc_web, sp_web)
        "userType" => $userType,        // ユーザータイプ(空文字, staff, developer)
    ],
]);

try {
    $response = $xgApi->request($method, $path, $body);
} catch (BadResponseException $e) {
    errorResponse($e->getResponse()->getBody());
}

// レスポンスの解析
if ($response->getStatusCode() == 200) {
    $res = json_decode($response->getBody());
    if (is_null($res)) {
        // エラー
        $err = json_last_error_msg();
        throw new \Exception(sprintf("JSON Decode error: %s, response: %s", $err, $response->getBody()));
    }
    if (isset($res->balance)) {
        foreach ($res->balance as $currencyId => $balance) {
            printf("%s 無償: %d, 有償: %d\n", $currencyId, $balance->free, $balance->paid);
        }
    }
} else {
	// エラー処理
}
// リクエスト用の構造体
type Games struct {
	AppId    string `json:"appId"`
	Device   string `json:"device"`
	UserType string `json:"userType"`
}

type Body struct {
	TransactionId string `json:"transactionId"`
	ProductId     string `json:"productId"`
	Games         Games  `json:"games"`
}

// レスポンス用の構造体
type XgBalanceDetail struct {
	Free int64 `json:"free"`
	Paid int64 `json:"paid"`
}

type XgPurchaseStoreResponse struct {
	TransactionId string                     `json:"transactionId"`
	TransactionAt time.Time                  `json:"transactionAt"`
	Quantity      int64                      `json:"quantity"`
	Balance       map[string]XgBalanceDetail `json:"balance"`
	Added         map[string]XgBalanceDetail `json:"added"`
}

// リクエストの準備
storeId := "dmmgames"

method := http.MethodPut                                                // HTTPメソッドの種類
path := fmt.Sprintf("/purchase/v1/users/%v/stores/%v", userId, storeId) // APIのパス
requestBodyBytes, err := json.Marshal(Body{
	TransactionId: "someToken",       // 購入トークン
	ProductId:     "someProductId",   // 商品ID
	Games: Games{
		AppId:    gamesAppId, // ゲームID
		Device:   device,     // デバイス(android, ios, win, mac, pc_web, sp_web)
		UserType: userType,   // ユーザータイプ(空文字, staff, developer)
	},
})
if err != nil {
	panic(err)
}
body := string(requestBodyBytes)

response, err := xgApi.SendRequest(method, path, body)
if err != nil {
	panic(err)
}
defer response.Body.Close()

// レスポンスの解析
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
	panic(err)
}

if response.StatusCode == http.StatusOK {
	var res XgPurchaseStoreResponse
	if err := json.Unmarshal(bodyBytes, &res); err != nil {
		// エラー
		panic(err)
	}
	for currencyId, balance := range res.Balance {
		fmt.Printf("%s 無償: %d, 有償: %d\n", currencyId, balance.Free, balance.Paid)
	}
} else {
	// エラー処理
}

Google Play

ストア「Google Play」で"someProductId"という品番の商品を購入したことをXGに登録します。

Google Playで購入処理をした際にストアから発行された購入トークン(PurchaseToken)をTransactionIDとしてXGへリクエストしてください。
XGでは、登録されている品番"someProductId"の内容に基づいてゲーム内通貨を発行します。
<?php

// リクエストの準備
$storeId = "googleplay";

$method = 'PUT';                                                        // HTTPメソッドの種類
$path = sprintf("/purchase/v1/users/%s/stores/%s", $userId, $storeId);  // APIのパス
$body = json_encode([
    'transactionId' => 'someToken',     // 購入トークン
    'productId' => 'someProductId',     // 商品ID
    'productType' => 'someProductType', // 商品タイプ
]);

try {
    $response = $xgApi->request($method, $path, $body);
} catch (BadResponseException $e) {
    errorResponse($e->getResponse()->getBody());
}

// レスポンスの解析
if ($response->getStatusCode() == 200) {
    $res = json_decode($response->getBody());
    if (is_null($res)) {
        // エラー
        $err = json_last_error_msg();
        throw new \Exception(sprintf("JSON Decode error: %s, response: %s", $err, $response->getBody()));
    }
    if (isset($res->balance)) {
        foreach ($res->balance as $currencyId => $balance) {
            printf("%s 無償: %d, 有償: %d\n", $currencyId, $balance->free, $balance->paid);
        }
    }
} else {
	// エラー処理
}
// リクエスト用の構造体
type Body struct {
	TransactionId string `json:"transactionId"`
	ProductId     string `json:"productId"`
	ProductType   string `json:"productType"`
}

// レスポンス用の構造体
type XgBalanceDetail struct {
	Free int64 `json:"free"`
	Paid int64 `json:"paid"`
}

type XgPurchaseStoreResponse struct {
	TransactionId string                     `json:"transactionId"`
	TransactionAt time.Time                  `json:"transactionAt"`
	Quantity      int64                      `json:"quantity"`
	Balance       map[string]XgBalanceDetail `json:"balance"`
	Added         map[string]XgBalanceDetail `json:"added"`
}

// リクエストの準備
storeId := "googleplay"

method := http.MethodPut                                                // HTTPメソッドの種類
path := fmt.Sprintf("/purchase/v1/users/%v/stores/%v", userId, storeId) // APIのパス
requestBodyBytes, err := json.Marshal(Body{
	TransactionId: "someToken",       // 購入トークン
	ProductId:     "someProductId",   // 商品ID
	ProductType:   "someProductType", // 商品タイプ
})
if err != nil {
	panic(err)
}
body := string(requestBodyBytes)

response, err := xgApi.SendRequest(method, path, body)
if err != nil {
	panic(err)
}
defer response.Body.Close()

// レスポンスの解析
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
	panic(err)
}

if response.StatusCode == http.StatusOK {
	var res XgPurchaseStoreResponse
	if err := json.Unmarshal(bodyBytes, &res); err != nil {
		// エラー
		panic(err)
	}
	for currencyId, balance := range res.Balance {
		fmt.Printf("%s 無償: %d, 有償: %d\n", currencyId, balance.Free, balance.Paid)
	}
} else {
	// エラー処理
}

DMMチャージセンター

DMMチャージセンターで"someProductId"という品番の商品を購入したことをXGに登録します。

DMMチャージセンターで購入処理をした際にゲームサーバーはチャージセンターから購入結果連携のコールバックを受け取ります。
受け取った通知に含まれるアプリ内ゲームユーザーID・購入ID・注文日時をXGへリクエストしてください。

注釈

DMMチャージセンターを利用する際は前提条件があります。DMMチャージセンターについて を参照してください。

<?php

// リクエストの準備
$method = 'PUT';                                                        // HTTPメソッドの種類
$path = sprintf("/purchase/v1/users/%s/stores/chargecenter/dmmgames", $userId);  // APIのパス
$body = json_encode([
    'purchaseId' => 'somePurchaseId',                       // 購入ID
    'productId' => 'someProductId',                         // 商品ID
    'storeId' => $storeId,                                  // ゲーム内通貨を発行するストアID(googleplay, appstore)
    'gameUserId' => $gameUserId,                            // ゲームアプリ内のユーザーID
    'purchaseAt' => $purcahseAt->format(DateTime::RFC3339), // 購入日時
]);

try {
    $response = $xgApi->request($method, $path, $body);
} catch (BadResponseException $e) {
    errorResponse($e->getResponse()->getBody());
}

// レスポンスの解析
printf("%s : HTTP status: %d\n", $path, $response->getStatusCode());

if ($response->getStatusCode() == 200) {
    $res = json_decode($response->getBody());
    if (is_null($res)) {
        // エラー
        $err = json_last_error_msg();
        throw new \Exception(sprintf("JSON Decode error: %s, response: %s", $err, $response->getBody()));
    }
    if (isset($res->balance)) {
        foreach ($res->balance as $currencyId => $balance) {
            printf("%s 無償: %d, 有償: %d\n", $currencyId, $balance->free, $balance->paid);
        }
    }
} else {
	// エラー処理
}
// リクエスト用の構造体
type Body struct {
	ProductId  string    `json:"productId"`
	PurchaseId string    `json:"purchaseId"`
	StoreId    string    `json:"storeId"`
	GameUserId string    `json:"gameUserId"`
	PurchaseAt time.Time `json:"purchaseAt"`
}

// レスポンス用の構造体
type XgBalanceDetail struct {
	Free int64 `json:"free"`
	Paid int64 `json:"paid"`
}

type XgPurchaseStoreChargecenterResponse struct {
	PurchaseId string                     `json:"purchaseId"`
	PurchaseAt time.Time                  `json:"purchaseAt"`
	Quantity   int64                      `json:"quantity"`
	Balance    map[string]XgBalanceDetail `json:"balance"`
	Added      map[string]XgBalanceDetail `json:"added"`
}

// リクエストの準備
method := http.MethodPut                                                          // HTTPメソッドの種類
path := fmt.Sprintf("/purchase/v1/users/%v/stores/chargecenter/dmmgames", userId) // APIのパス
requestBodyBytes, err := json.Marshal(Body{
	PurchaseId: "somePurchaseId", 						// 購入ID
	ProductId:  "someProductId",  						// 商品ID
	StoreId:    storeId,    							// ゲーム内通貨を発行するストアID(googleplay, appstore)
	GameUserId: gameUserId, 							// ゲームアプリ内のユーザーID
	PurchaseAt: purcahseAt.UTC().Truncate(time.Second), // 購入日時(UTC)
})
if err != nil {
	panic(err)
}
body := string(requestBodyBytes)

response, err := xgApi.SendRequest(method, path, body)
if err != nil {
	panic(err)
}
defer response.Body.Close()

// レスポンスの解析
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
	panic(err)
}

if response.StatusCode == http.StatusOK {
	var res XgPurchaseStoreChargecenterResponse
	if err := json.Unmarshal(bodyBytes, &res); err != nil {
		// エラー
		panic(err)
	}
	for currencyId, balance := range res.Balance {
		fmt.Printf("%s 無償: %d, 有償: %d\n", currencyId, balance.Free, balance.Paid)
	}
} else {
	// エラー処理
}

課金アイテム購入の検証

App Store

ストア「App Store」で"someProductId"という品番の商品購入を検証します。

App Storeで購入処理をした際にストアから発行されたトランザクションID(TransactionID)をXGへリクエストしてください。
<?php
// リクエストの準備
$storeId = "appstore";

$productId = 'someProductId';     // 商品ID
$transactionId = 'someToken';     // ストアから発行されたトランザクションID
    
$method = 'GET'; // HTTPメソッドの種類
$path = sprintf("/verify/v1/users/%s/stores/%s/products/%s/transactions/%s", $userId, $storeId, $productId, $transactionId); // APIのパス

try {
    $response = $xgApi->request($method, $path, "");
} catch (BadResponseException $e) {
    errorResponse($e->getResponse()->getBody());
}

// レスポンスの解析
if ($response->getStatusCode() == 200) {
    $res = json_decode($response->getBody());
    if (is_null($res)) {
        // エラー
        $err = json_last_error_msg();
        throw new \Exception(sprintf("JSON Decode error: %s, response: %s", $err, $response->getBody()));
    }

    printf("ステータス: %s\n", $res->status);

    if (isset($res->balance)) {
        foreach ($res->balance as $currencyId => $balance) {
            printf("%s 無償: %d, 有償: %d\n", $currencyId, $balance->free, $balance->paid);
        }
    }
} else {
	// エラー処理
}
// レスポンス用の構造体
type XgBalanceDetail struct {
	Free int64 `json:"free"`
	Paid int64 `json:"paid"`
}

type XgVerifyStoreResponse struct {
	TransactionId string                     `json:"transactionId"`
	TransactionAt time.Time                  `json:"transactionAt"`
	Quantity      int64                      `json:"quantity"`
	Status        string                     `json:"status"`
	Balance       map[string]XgBalanceDetail `json:"balance"`
}

// リクエストの準備
storeId := "appstore"

transactionId := "someToken" // 購入トークン
productId := "someProductId" // 商品ID

method := http.MethodGet                                                                                                    // HTTPメソッドの種類
path := fmt.Sprintf("/verify/v1/users/%s/stores/%s/products/%s/transactions/%s", userId, storeId, productId, transactionId) // APIのパス

response, err := xgApi.SendRequest(method, path, "")
if err != nil {
	panic(err)
}
defer response.Body.Close()

// レスポンスの解析
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
	panic(err)
}

if response.StatusCode == http.StatusOK {
	var res XgVerifyStoreResponse
	if err := json.Unmarshal(bodyBytes, &res); err != nil {
		// エラー
		panic(err)
	}

	fmt.Printf("ステータス: %s\n", res.Status)

	for currencyId, balance := range res.Balance {
		fmt.Printf("%s 無償: %d, 有償: %d\n", currencyId, balance.Free, balance.Paid)
	}
} else {
	// エラー処理
}

DMM GAMES

ストア「DMM GAMES」で"someProductId"という品番の商品購入を検証します。

DMM GAMESで購入処理をした際にストアから発行された購入トークン(PurchaseToken)をTransactionIDとしてXGへリクエストしてください。
<?php
// リクエストの準備
$storeId = "dmmgames";

$productId = 'someProductId';     // 商品ID
$transactionId = 'someToken';     // 購入トークン

$param = [];
$param["appId"] = $gamesAppId; // GAMESアプリID
$param["device"] = $device; // デバイス(android, ios, win, mac, pc_web, sp_web)
$param["userType"] = $userType; // ユーザータイプ(空文字, staff, developer)

$method = 'GET'; // HTTPメソッドの種類
$path = sprintf("/verify/v1/users/%s/stores/%s/products/%s/transactions/%s?%s", $userId, $storeId, $productId, $transactionId, http_build_query($param)); // APIのパス

try {
    $response = $xgApi->request($method, $path, "");
} catch (BadResponseException $e) {
    errorResponse($e->getResponse()->getBody());
}

// レスポンスの解析
if ($response->getStatusCode() == 200) {
    $res = json_decode($response->getBody());
    if (is_null($res)) {
        // エラー
        $err = json_last_error_msg();
        throw new \Exception(sprintf("JSON Decode error: %s, response: %s", $err, $response->getBody()));
    }

    printf("ステータス: %s\n", $res->status);

    if (isset($res->balance)) {
        foreach ($res->balance as $currencyId => $balance) {
            printf("%s 無償: %d, 有償: %d\n", $currencyId, $balance->free, $balance->paid);
        }
    }
} else {
    // エラー処理
    errorResponse($response->getBody());
}
// レスポンス用の構造体
type XgBalanceDetail struct {
	Free int64 `json:"free"`
	Paid int64 `json:"paid"`
}

type XgVerifyStoreResponse struct {
	TransactionId string                     `json:"transactionId"`
	TransactionAt time.Time                  `json:"transactionAt"`
	Quantity      int64                      `json:"quantity"`
	Status        string                     `json:"status"`
	Balance       map[string]XgBalanceDetail `json:"balance"`
}

// リクエストの準備
storeId := "dmmgames"

transactionId := "someToken" // 購入トークン
productId := "someProductId" // 商品ID

method := http.MethodGet                                                                                                        // HTTPメソッドの種類
basePath := fmt.Sprintf("/verify/v1/users/%s/stores/%s/products/%s/transactions/%s", userId, storeId, productId, transactionId) // APIのパス
pathUrl, err := url.Parse(basePath)
if err != nil {
	panic(err)
}

// クエリパラメータの設定
param := url.Values{}
param.Add("appId", gamesAppId) // GAMESアプリID
param.Add("device", device)     // デバイス(android, ios, win, mac, pc_web, sp_web)
param.Add("userType", userType) // ユーザータイプ(空文字, staff, developer)

pathUrl.RawQuery = param.Encode()
path := pathUrl.String()

response, err := xgApi.SendRequest(method, path, "")
if err != nil {
	panic(err)
}
defer response.Body.Close()

// レスポンスの解析
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
	panic(err)
}

if response.StatusCode == http.StatusOK {
	var res XgVerifyStoreResponse
	if err := json.Unmarshal(bodyBytes, &res); err != nil {
		// エラー
		panic(err)
	}

	fmt.Printf("ステータス: %s\n", res.Status)

	for currencyId, balance := range res.Balance {
		fmt.Printf("%s 無償: %d, 有償: %d\n", currencyId, balance.Free, balance.Paid)
	}
} else {
	// エラー処理
}

Google Play

ストア「Google Play」で"someProductId"という品番の商品購入を検証します。
Google Playで購入処理をした際にストアから発行された購入トークン(PurchaseToken)をTransactionIDとしてXGへリクエストしてください。
<?php
// リクエストの準備
$storeId = "googleplay";

$productId = 'someProductId';     // 商品ID
$transactionId = 'someToken';     // 購入トークン

$param = [];
$param["productType"] = $productType; // 商品タイプ (consumable, non-consumable)

$method = 'GET'; // HTTPメソッドの種類
$path = sprintf("/verify/v1/users/%s/stores/%s/products/%s/transactions/%s?%s", $userId, $storeId, $productId, $transactionId, http_build_query($param)); // APIのパス

try {
    $response = $xgApi->request($method, $path, "");
} catch (BadResponseException $e) {
    errorResponse($e->getResponse()->getBody());
}

// レスポンスの解析
if ($response->getStatusCode() == 200) {
    $res = json_decode($response->getBody());
    if (is_null($res)) {
        // エラー
        $err = json_last_error_msg();
        throw new \Exception(sprintf("JSON Decode error: %s, response: %s", $err, $response->getBody()));
    }

    printf("ステータス: %s\n", $res->status);

    if (isset($res->balance)) {
        foreach ($res->balance as $currencyId => $balance) {
            printf("%s 無償: %d, 有償: %d\n", $currencyId, $balance->free, $balance->paid);
        }
    }
} else {
    // エラー処理
}
// レスポンス用の構造体
type XgBalanceDetail struct {
	Free int64 `json:"free"`
	Paid int64 `json:"paid"`
}

type XgVerifyStoreResponse struct {
	TransactionId string                     `json:"transactionId"`
	TransactionAt time.Time                  `json:"transactionAt"`
	Quantity      int64                      `json:"quantity"`
	Status        string                     `json:"status"`
	Balance       map[string]XgBalanceDetail `json:"balance"`
}

// リクエストの準備
storeId := "googleplay"

transactionId := "someToken" // 購入トークン
productId := "someProductId" // 商品ID

method := http.MethodGet                                                                                                        // HTTPメソッドの種類
basePath := fmt.Sprintf("/verify/v1/users/%s/stores/%s/products/%s/transactions/%s", userId, storeId, productId, transactionId) // APIのパス
pathUrl, err := url.Parse(basePath)
if err != nil {
	panic(err)
}

// クエリパラメータの設定
param := url.Values{}
param.Add("productType", productType) // 商品タイプ (consumable, non-consumable)
pathUrl.RawQuery = param.Encode()
path := pathUrl.String()

response, err := xgApi.SendRequest(method, path, "")
if err != nil {
	panic(err)
}
defer response.Body.Close()

// レスポンスの解析
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
	panic(err)
}

if response.StatusCode == http.StatusOK {
	var res XgVerifyStoreResponse
	if err := json.Unmarshal(bodyBytes, &res); err != nil {
		// エラー
		panic(err)
	}

	fmt.Printf("ステータス: %s\n", res.Status)

	for currencyId, balance := range res.Balance {
		fmt.Printf("%s 無償: %d, 有償: %d\n", currencyId, balance.Free, balance.Paid)
	}
} else {
	// エラー処理
}

ゲーム内通貨(有償通貨のみ)の消費

最後にゲーム内で有償通貨「gem」を100消費して、プレミアガチャチケットを1枚獲得する例を説明します。

  1. ゲーム内通貨を操作するリクエストをする際には、ゲームサーバー側で取引ID(transactionID)の発行が必要です。

  2. ゲーム内通貨の消費状態と、ゲーム内でのアイテム発行状態を管理するレコードを作成してください。(ここでは「取引状態管理テーブル」と表現します。)

    このテーブルでは、XG側のレスポンスと、ゲーム側のアイテム付与状態に応じて状態を管理するようにします。

    これは処理が中断した際の再開処理をする際に必要となります。

  3. REQUEST BODYのパラメーター「currencyType」に「paid」を指定して消費リクエストを呼び出します。(なお、「currencyType」を指定しない場合は、無償通貨もしくは有償通貨は予め登録してあるゲーム内通貨の消費順に従って消費します。)

  4. XGから成功レスポンスが戻ってきましたので「取引状態管理テーブル」の状態を「ゲーム内通貨消費済み」に更新します。

  5. ゲーム内アイテム「プレミアガチャチケット」を1枚発行します。

  6. アイテムの付与が完了しましたら、「取引状態管理テーブル」の状態を「アイテム付与済み」に更新します。 これで一連のゲーム内通貨の消費とアイテムの発行処理が完了となります。

<?php

//-----------------------------------------------------------
// ゲーム内通貨の消費状態とアイテム付与の状態管理のための実装
//-----------------------------------------------------------
class TransactionConsumeStatus{
    public function setConsumeCurrencyStatus($transactionId, $userId, $status) {
        // データベース更新処理などを記入
    }
}

//-----------------------------------------------------------
// 有償通貨100gem消費し、プレミアガチャチケット1枚獲得する実装イメージ
//-----------------------------------------------------------

// ゲーム内通貨の消費とアイテム付与の状態管理するクラス
$transactionStatus = new TransactionConsumeStatus();

// リクエストの準備
$storeId = "googleplay";

$transactionId = Uuid::uuid4()->toString();

// リカバリー対策として、取引状態及びアイテム等の付与状態を保存する
$transactionStatus->setConsumeCurrencyStatus($transactionId, $userId, "処理前");

$method = 'PUT';                                                                // HTTPメソッドの種類
$path = sprintf("/currency/v1/users/%s/stores/%s/consume", $userId, $storeId);  // APIのパス

// 有償通貨100gem消費して、プレミアガチャチケット1枚獲得
$body = json_encode([
    'transactionId' => $transactionId,
    'description' => 'プレミアガチャチケット',
    'quantity' =>      1,
    'currencyType' =>  "paid",
    'transaction' => [
        "gem" => 100,
    ],
]);

try {
    $response = $xgApi->request($method, $path, $body);
} catch (BadResponseException $e) {
    errorResponse($e->getResponse()->getBody());
}

// レスポンスの解析
if ($response->getStatusCode() == 200) {
    $res = json_decode($response->getBody());
    if (is_null($res)) {
        //エラー
        $err = json_last_error_msg();
        throw new \Exception(sprintf("JSON Decode error: %s, response: %s", $err, $response->getBody()));
    }

    // ゲーム内通貨の残高
    if (isset($res->balance)) {
        foreach ($res->balance as $currencyId => $balance) {
            printf("%s 無償: %d, 有償: %d\n", $currencyId, $balance->free, $balance->paid);
        }
    }

    // XG側でゲーム内通貨を消費済み状態に変更
    $transactionStatus->setConsumeCurrencyStatus($transactionId, $userId, "ゲーム内通貨消費済み");

    /*
        プレミアガチャチケットを1枚獲得
    */

    // アイテム付与完了状態にする
    $transactionStatus->setConsumeCurrencyStatus($transactionId, $userId, "アイテム付与済み");
} else {
    // エラー処理
}
//-----------------------------------------------------------
// ゲーム内通貨の消費状態とアイテム付与の状態管理のための実装
//-----------------------------------------------------------
type TransactionConsumeStatus struct{}

func (t *TransactionConsumeStatus) setConsumeCurrencyStatus(transactionId any, userId any, status any) error {
	// データベース更新処理などを記入
	return nil
}

//-----------------------------------------------------------
// 有償通貨100gem消費し、プレミアガチャチケット1枚獲得する実装イメージ
//-----------------------------------------------------------

// リクエスト用の構造体
type Body struct {
	TransactionId string           `json:"transactionId"`
	Description   string           `json:"description"`
	Quantity      int64            `json:"quantity"`
	CurrencyType  string           `json:"currencyType"`
	Transaction   map[string]int64 `json:"transaction"`
}

// ゲーム内通貨の消費とアイテム付与の状態管理する構造体
transactionStatus := TransactionConsumeStatus{}

// レスポンス用の構造体
type XgBalanceDetail struct {
	Free int64 `json:"free"`
	Paid int64 `json:"paid"`
}

type XgConsumeResponse struct {
	// TransactionId 取引ID
	TransactionId string `json:"transactionId"`

	// TransactionAt ゲーム内通貨消費日時(UTC)
	TransactionAt time.Time `json:"transactionAt"`

	// Status 結果ステータス
	Status string `json:"status"`

	// StoreId ストアID
	StoreId string `json:"storeId"`

	// Balance ゲーム内通貨の残高
	Balance map[string]XgBalanceDetail `json:"balance"`
}

// リクエストの準備
storeId := "googleplay"

transactionId, err := uuid.NewV4()
if err != nil {
	panic(err)
}

// リカバリー対策として、取引状態及びアイテム等の付与状態を保存する
err = transactionStatus.setConsumeCurrencyStatus(transactionId, userId, "処理前")
if err != nil {
	// エラー処理
	panic(err)
}

method := http.MethodPut                                                        // HTTPメソッドの種類
path := fmt.Sprintf("/currency/v1/users/%v/stores/%v/consume", userId, storeId) // APIのパス

// 有償通貨100gem消費して、プレミアガチャチケット1枚獲得
requestBodyBytes, err := json.Marshal(Body{
	TransactionId: transactionId.String(),
	Description:   "プレミアガチャチケット",
	Quantity:      1,
	CurrencyType:  "paid",
	Transaction: map[string]int64{
		"gem": 100,
	},
})
if err != nil {
	panic(err)
}
body := string(requestBodyBytes)

response, err := xgApi.SendRequest(method, path, body)
if err != nil {
	panic(err)
}
defer response.Body.Close()

// レスポンスの解析
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
	panic(err)
}

if response.StatusCode == http.StatusOk {
	var res XgConsumeResponse
	if err := json.Unmarshal(bodyBytes, &res); err != nil {
		// エラー
		panic(err)
	}

	// ゲーム内通貨の残高
	for currencyId, balance := range res.Balance {
		fmt.Printf("%s 無償: %d, 有償: %d\n", currencyId, balance.Free, balance.Paid)
	}

	// XG側でゲーム内通貨を消費済み状態に変更
	err = transactionStatus.setConsumeCurrencyStatus(transactionId, userId, "ゲーム内通貨消費済み")
	if err != nil {
		// エラー処理
		panic(err)
	}

	/*
		プレミアガチャチケットを1枚獲得
	*/

	// アイテム付与完了状態にする
	err = transactionStatus.setConsumeCurrencyStatus(transactionId, userId, "アイテム付与済み")
	if err != nil {
		// エラー処理
		panic(err)
	}

} else {
	// エラー処理
}

ゲーム内通貨消費の取消し

リカバリーでゲーム内アイテムの付与が適さないアイテムの場合、ゲーム内通貨の消費のリカバリーにてゲーム内通貨消費の取消しを行います。

注釈

リカバリーでゲーム内アイテムの付与が適さないアイテムは以下を想定しています。

  • コンティニューやスタミナ回復等、ユーザーがリクエストした時に効果が発揮されないと価値提供ができないアイテム

  • 期間限定品のように定められた期間を超えて付与されても使用できない(ガチャチケット)、あるいは都合の悪いアイテム

<?php
// ゲーム内通貨の消費とアイテム付与の状態管理するクラス
$transactionStatus = new TransactionConsumeStatus();

// リクエストの準備
$storeId = "googleplay";

$method = 'PUT';                                                                // HTTPメソッドの種類
$path = sprintf("/currency/v1/users/%s/stores/%s/consume/cancel", $userId, $storeId);  // APIのパス

// ゲーム内通貨消費の取消し
$body = json_encode([
    'transactionId' => $transactionId, // ゲーム内通貨の消費で指定した取引ID
    'description' => 'アイテムの使用期限切れによりアイテム付与が出来ないため', // 取消し理由
]);

try {
    $response = $xgApi->request($method, $path, $body);
} catch (BadResponseException $e) {
    errorResponse($e->getResponse()->getBody());
}

// レスポンスの解析
if ($response->getStatusCode() == 200) {
    $res = json_decode($response->getBody());
    if (is_null($res)) {
        //エラー
        $err = json_last_error_msg();
        throw new \Exception(sprintf("JSON Decode error: %s, response: %s", $err, $response->getBody()));
    }

    // ゲーム内通貨の残高
    if (isset($res->balance)) {
        print("ゲーム内通貨の残高\n");
        foreach ($res->balance as $currencyId => $balance) {
            printf("%s 無償: %d, 有償: %d\n", $currencyId, $balance->free, $balance->paid);
        }
    }
    // 発行されたゲーム内通貨の数量
    if(isset($res->added)){
        print("発行されたゲーム内通貨の数量\n");
        foreach ($res->added as $currencyId => $added) {
            printf("%s 無償: %d, 有償: %d\n", $currencyId, $added->free, $added->paid);
        }
    }

    // ゲーム内通貨の消費とアイテム付与の状態レコードを削除
    $transactionStatus->deleteConsumeCurrencyStatus($transactionId, $userId);
} else {
	// エラー処理
}
// リクエスト用の構造体
type Body struct {
	TransactionId string `json:"transactionId"`
	Description   string `json:"description"`
}

// レスポンス用の構造体
type XgCurrencyDetail struct {
	Free int64 `json:"free"`
	Paid int64 `json:"paid"`
}

type XgConsumeResponse struct {
	// TransactionId 取引ID
	TransactionId string `json:"transactionId"`

	// TransactionAt ゲーム内通貨消費の取消日時(UTC)
	TransactionAt time.Time `json:"transactionAt"`

	// Status 結果ステータス
	Status string `json:"status"`

	// Balance ゲーム内通貨の残高
	Balance map[string]XgCurrencyDetail `json:"balance"`

	// Added 発行されたゲーム内通貨の数量
	Added map[string]XgCurrencyDetail `json:"added"`
}

// ゲーム内通貨の消費とアイテム付与の状態管理する構造体
transactionStatus := TransactionConsumeStatus{}

// リクエストの準備
storeId := "googleplay"

method := http.MethodPut                                                               // HTTPメソッドの種類
path := fmt.Sprintf("/currency/v1/users/%v/stores/%v/consume/cancel", userId, storeId) // APIのパス

// ゲーム内通貨消費の取消し
requestBodyBytes, err := json.Marshal(Body{
	TransactionId: transactionId,                 // ゲーム内通貨の消費で指定した取引ID
	Description:   "アイテムの使用期限切れによりアイテム付与が出来ないため", // 取消し理由
})
if err != nil {
	panic(err)
}
body := string(requestBodyBytes)

response, err := xgApi.SendRequest(method, path, body)
if err != nil {
	panic(err)
}
defer response.Body.Close()

// レスポンスの解析
fmt.Printf("%s : HTTP status: %v\n", path, response.StatusCode)
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
	panic(err)
}

if response.StatusCode == http.StatusOK {
	var res XgConsumeResponse
	if err := json.Unmarshal(bodyBytes, &res); err != nil {
		// エラー
		panic(err)
	}

	// ゲーム内通貨の残高
	fmt.Println("ゲーム内通貨の残高")
	for currencyId, balance := range res.Balance {
		fmt.Printf("%s 無償: %d, 有償: %d\n", currencyId, balance.Free, balance.Paid)
	}

	// 発行されたゲーム内通貨の数量
	fmt.Println("発行されたゲーム内通貨の数量")
	for currencyId, added := range res.Added {
		fmt.Printf("%s 無償: %d, 有償: %d\n", currencyId, added.Free, added.Paid)
	}

	// ゲーム内通貨の消費とアイテム付与の状態レコードを削除
	err = transactionStatus.deleteConsumeCurrencyStatus(transactionId, userId)
	if err != nil {
		// エラー処理
		painc(err)
	}
} else {
	// エラー処理
}

履歴の取得

ゲーム内商品の購入履歴の取得

各ストアやゲーム内通貨を使用して購入した商品を取得します。

下記の各履歴を最大各100件まで取得できます。

  • 各ストアでゲーム内商品(ゲーム内通貨の購入や直課金など)を購入した履歴("store")

  • ゲーム内通貨を消費してゲーム内商品(アイテムや権利行使など)を購入した履歴("ingame")

<?php
// リクエストの準備
$param = [];
if (!empty($purchasedWith)) {
    $param["purchasedWith"] = implode(",", $purchasedWith);
}
if (!is_null($startAt)) {
    $startAt->setTimezone(new DateTimeZone("UTC"));
    $param["startAt"] = $startAt->format(DateTime::RFC3339);
}
if (!is_null($endAt)) {
    $endAt->setTimezone(new DateTimeZone("UTC"));
    $param["endAt"] = $endAt->format(DateTime::RFC3339);
}
if (!is_null($limit)) {
    $param["limit"] = $limit;
}
if (!is_null($pageNumber)) {
    $param["pageNumber"] = $pageNumber;
}
if (!is_null($transactionId)) {
    $param["transactionId"] = $transactionId;
}

$method = 'GET';                                                                        // HTTPメソッドの種類
$path = sprintf("/history/v1/users/%s/products?%s", $userId, http_build_query($param)); // APIのパス

try {
    $response = $xgApi->request($method, $path, "");
} catch (BadResponseException $e) {
    errorResponse($e->getResponse()->getBody());
}

// レスポンスの解析
if ($response->getStatusCode() == 200) {
    $res = json_decode($response->getBody());
    if (is_null($res)) {
        // エラー
        $err = json_last_error_msg();
        throw new \Exception(sprintf("JSON Decode error: %s, response: %s", $err, $response->getBody()));
    }
    printf("各ストアの履歴\n");
    foreach($res->store as $store) {
        printf("%s %s(%s) %d個 価格:%d\n", $store->storeId, $store->productName, $store->productId, $store->quantity, $store->price);
    }
    printf("ゲーム内通貨の履歴\n");
    foreach($res->ingame as $ingame) {
        $cur = json_encode($ingame->currency);
        printf("%s %s %d個 消費したゲーム内通貨:%s\n", $ingame->storeId, $ingame->description, $ingame->quantity, $cur);
    }
} else {
    // エラー処理
}
// レスポンス用の構造体
type Store struct {
	StoreId            string    `json:"storeId"`
	ProductId          string    `json:"productId"`
	ProductName        string    `json:"productName"`
	Quantity           int64     `json:"quantity"`
	Price              int64     `json:"price"`
	StoreTransactionId string    `json:"storeTransactionId"`
	TransactionAt      time.Time `json:"transactionAt"`
}
type IngameCurrencyDetail struct {
	Total int64 `json:"total"`
	Paid  int64 `json:"paid"`
	Free  int64 `json:"free"`
}
type Ingame struct {
	StoreId       string                          `json:"storeId"`
	Description   string                          `json:"description"`
	Quantity      int64                           `json:"quantity"`
	Currency      map[string]IngameCurrencyDetail `json:"currency"`
	TransactionId string                          `json:"transactionId"`
	TransactionAt time.Time                       `json:"transactionAt"`
}
type XgHistoryProductsResponse struct {
	Store  []Store
	Ingame []Ingame
}

// リクエストの準備
method := http.MethodGet                                         // HTTPメソッドの種類
basePath := fmt.Sprintf("/history/v1/users/%s/products", userId) // APIのパス
pathUrl, err := url.Parse(basePath)
if err != nil {
	panic(err)
}
param := url.Values{}
if len(purchasedWith) > 0 {
	param.Add("purchasedWith", strings.Join(purchasedWith, ","))
}
if startAt != nil {
	param.Add("startAt", startAt.UTC().Format(time.RFC3339))
}
if endAt != nil {
	param.Add("startAt", startAt.UTC().Format(time.RFC3339))
}
if limit != nil {
	param.Add("limit", fmt.Sprintf("%d", *limit))
}
if pageNumber != nil {
	param.Add("pageNumber", fmt.Sprintf("%d", *pageNumber))
}
if transactionId != nil {
	param.Add("transactionId", *transactionId)
}
pathUrl.RawQuery = param.Encode()
path := pathUrl.String()

response, err := xgApi.SendRequest(method, path, "")
if err != nil {
	panic(err)
}
defer response.Body.Close()

// レスポンスの解析
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
	panic(err)
}

if response.StatusCode == http.StatusOK {
	var res XgHistoryProductsResponse
	if err := json.Unmarshal(bodyBytes, &res); err != nil {
		// エラー
		panic(err)
	}

	fmt.Println("各ストアの履歴")
	for _, store := range res.Store {
		fmt.Printf("%s %s(%s) %d個 価格:%d\n", store.StoreId, store.ProductName, store.ProductId, store.Quantity, store.Price)
	}
	fmt.Println("ゲーム内通貨の履歴")
	for _, ingame := range res.Ingame {
		cur, err := json.Marshal(ingame.Currency)
		if err != nil {
			fmt.Printf("%v\n", err)
		}
		fmt.Printf("%s %s %d個 消費したゲーム内通貨:%s\n", ingame.StoreId, ingame.Description, ingame.Quantity, string(cur))
	}
} else {
	// エラー処理
}

履歴(V2)の取得

課金アイテムの購入履歴の取得

課金アイテムの購入履歴を取得します。

注釈

「transactionId」がある場合

  • 「startAt」と「endAt」の値は無視されます。

  • 「storeId」が必須になります。

<?php
// リクエストの準備
$param = [];
if (!is_null($storeId)) {
    $param["storeId"] = implode(",", $storeId);
}
if (!is_null($startAt)) {
    $startAt->setTimezone(new DateTimeZone("UTC"));
    $param["startAt"] = $startAt->format(DateTime::RFC3339);
}
if (!is_null($endAt)) {
    $endAt->setTimezone(new DateTimeZone("UTC"));
    $param["endAt"] = $endAt->format(DateTime::RFC3339);
}
if (!is_null($limit)) {
    $param["limit"] = $limit;
}
if (!is_null($pageNumber)) {
    $param["pageNumber"] = $pageNumber;
}
if (!is_null($transactionId)) {
    $param["transactionId"] = $transactionId;
}
if (!is_null($sortAsc)) {
    $param["sort"] = $sortAsc ? "asc": "desc";
}
if (!is_null($timeZone)) {
    $param["timeZone"] = $timeZone;
}

$method = 'GET';                                                                         // HTTPメソッドの種類
$path = sprintf("/history/v2/users/%s/purchases?%s", $userId, http_build_query($param)); // APIのパス

try {
    $response = $xgApi->request($method, $path, "");
} catch (BadResponseException $e) {
    errorResponse($e->getResponse()->getBody());
}

// レスポンスの解析
if ($response->getStatusCode() == 200) {
    $res = json_decode($response->getBody());
    if (is_null($res)) {
        // エラー
        $err = json_last_error_msg();
        throw new \Exception(sprintf("JSON Decode error: %s, response: %s", $err, $response->getBody()));
    }
    printf("購入履歴\n");
    foreach($res->purchases as $row) {
        printf("%s %s %s(%s) %d個 価格:%d\n", $row->transactionAt, $row->storeId, $row->productName, $row->productId, $row->quantity, $row->price);
    }
} else {
    // エラー処理
}
// レスポンス用の構造体
type purchase struct {
	TransactionAt time.Time `json:"transactionAt"`
	TransactionId string    `json:"transactionId"`
	StoreId       string    `json:"storeId"`
	ProductId     string    `json:"productId"`
	ProductName   string    `json:"productName"`
	Quantity      int64     `json:"quantity"`
	Price         int64     `json:"price"`
}
type XgHistoryV2PurchasesResponse struct {
	TotalCount int64      `json:"totalCount"`
	Purchases  []purchase `json:"purchases"`
}

// リクエストの準備
method := http.MethodGet                                          // HTTPメソッドの種類
basePath := fmt.Sprintf("/history/v2/users/%s/purchases", userId) // APIのパス
pathUrl, err := url.Parse(basePath)
if err != nil {
	return err
}
param := url.Values{}
if len(storeId) > 0 {
	param.Add("storeId", strings.Join(storeId, ","))
}
if startAt != nil {
	param.Add("startAt", startAt.UTC().Format(time.RFC3339))
}
if endAt != nil {
	param.Add("startAt", startAt.UTC().Format(time.RFC3339))
}
if limit != nil {
	param.Add("limit", fmt.Sprintf("%d", *limit))
}
if pageNumber != nil {
	param.Add("pageNumber", fmt.Sprintf("%d", *pageNumber))
}
if transactionId != nil {
	param.Add("transactionId", *transactionId)
}
if sortAsc != nil {
	if *sortAsc {
		param.Add("sort", "asc")
	} else {
		param.Add("sort", "desc")
	}
}
if timeZone != nil {
	param.Add("timeZone", *timeZone)
}
pathUrl.RawQuery = param.Encode()
path := pathUrl.String()

response, err := xgApi.SendRequest(method, path, "")
if err != nil {
	panic(err)
}
defer response.Body.Close()

// レスポンスの解析
fmt.Printf("%s : HTTP status: %v\n", path, response.StatusCode)
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
	panic(err)
}

if response.StatusCode == http.StatusOK {
	var res XgHistoryV2PurchasesResponse
	if err := json.Unmarshal(bodyBytes, &res); err != nil {
		// エラー
		panic(err)
	}
	fmt.Println("購入履歴")
	for _, row := range res.Purchases {
		fmt.Printf("%s %s %s(%s) %d個 価格:%d\n", row.TransactionAt.String(), row.StoreId, row.ProductName, row.ProductId, row.Quantity, row.Price)
	}
} else {
	// エラー処理
}

ゲーム内通貨の取引履歴の取得

ゲーム内通貨の取引履歴を取得します。

注釈

「transactionId」がある場合

  • 「startAt」と「endAt」の値は無視されます。

  • 「storeId」が必須になります。

<?php
// リクエストの準備
$param = [];
if (!is_null($storeId)) {
    $param["storeId"] = implode(",", $storeId);
}
if (!is_null($startAt)) {
    $startAt->setTimezone(new DateTimeZone("UTC"));
    $param["startAt"] = $startAt->format(DateTime::RFC3339);
}
if (!is_null($endAt)) {
    $endAt->setTimezone(new DateTimeZone("UTC"));
    $param["endAt"] = $endAt->format(DateTime::RFC3339);
}
if (!is_null($limit)) {
    $param["limit"] = $limit;
}
if (!is_null($pageNumber)) {
    $param["pageNumber"] = $pageNumber;
}
if (!is_null($transactionId)) {
    $param["transactionId"] = $transactionId;
}
if (!is_null($transactionType)) {
    $param["transactionType"] = implode(",", $transactionType);
}
if (!is_null($currencyId)) {
    $param["currencyId"] = implode(",", $currencyId);
}
if (!is_null($currencyType)) {
    $param["currencyType"] = $currencyType;
}
if (!is_null($sortAsc)) {
    $param["sort"] = $sortAsc ? "asc": "desc";
}
if (!is_null($timeZone)) {
    $param["timeZone"] = $timeZone;
}

$method = 'GET';                                                                                     // HTTPメソッドの種類
$path = sprintf("/history/v2/users/%s/currency-transactions?%s", $userId, http_build_query($param)); // APIのパス

try {
    $response = $xgApi->request($method, $path, "");
} catch (BadResponseException $e) {
    errorResponse($e->getResponse()->getBody());
}

if ($response->getStatusCode() == 200) {
    $res = json_decode($response->getBody());
    if (is_null($res)) {
        // エラー
        $err = json_last_error_msg();
        throw new \Exception(sprintf("JSON Decode error: %s, response: %s", $err, $response->getBody()));
    }
    printf("取引履歴\n");
    foreach($res->currencyTransactions as $row) {
        $balance = "";
        if ($row->balance != null) {
            $balance = sprintf("残高=%d", $row->balance);
        }
        printf("%s %s %s %s(%s)=%d %s\n", $row->transactionAt, $row->storeId, $row->transactionType, $row->currencyId, $row->currencyType, $row->quantity, $balance);
    }
} else {
    // エラー処理
}
// レスポンス用の構造体
type currencyTransaction struct {
	TransactionAt   time.Time `json:"transactionAt"`
	TransactionId   string    `json:"transactionId"`
	TransactionType string    `json:"transactionType"`
	StoreId         string    `json:"storeId"`
	Description     string    `json:"description"`
	CurrencyId      string    `json:"currencyId"`
	CurrencyType    string    `json:"currencyType"`
	Quantity        int64     `json:"quantity"`
	Balance         *int64    `json:"balance,omitempty"`
}
type XgHistoryV2CurrencyTransactionsResponse struct {
	TotalCount           int64                 `json:"totalCount"`
	CurrencyTransactions []currencyTransaction `json:"currencyTransactions"`
}

// リクエストの準備
method := http.MethodGet                                                      // HTTPメソッドの種類
basePath := fmt.Sprintf("/history/v2/users/%s/currency-transactions", userId) // APIのパス
pathUrl, err := url.Parse(basePath)
if err != nil {
	return err
}
param := url.Values{}
if len(storeId) > 0 {
	param.Add("storeId", strings.Join(storeId, ","))
}
if startAt != nil {
	param.Add("startAt", startAt.UTC().Format(time.RFC3339))
}
if endAt != nil {
	param.Add("startAt", startAt.UTC().Format(time.RFC3339))
}
if limit != nil {
	param.Add("limit", fmt.Sprintf("%d", *limit))
}
if pageNumber != nil {
	param.Add("pageNumber", fmt.Sprintf("%d", *pageNumber))
}
if transactionId != nil {
	param.Add("transactionId", *transactionId)
}
if transactionType != nil && len(*transactionType) > 0 {
	param.Add("transactionType", strings.Join(*transactionType, ","))
}
if currencyId != nil && len(*currencyId) > 0 {
	param.Add("currencyId", strings.Join(*currencyId, ","))
}
if currencyType != nil {
	param.Add("currencyType", *currencyType)
}
if sortAsc != nil {
	if *sortAsc {
		param.Add("sort", "asc")
	} else {
		param.Add("sort", "desc")
	}
}
if timeZone != nil {
	param.Add("timeZone", *timeZone)
}
pathUrl.RawQuery = param.Encode()
path := pathUrl.String()

response, err := xgApi.SendRequest(method, path, "")
if err != nil {
	panic(err)
}
defer response.Body.Close()

// レスポンスの解析
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
	panic(err)
}

if response.StatusCode == http.StatusOK {
	var res XgHistoryV2CurrencyTransactionsResponse
	if err := json.Unmarshal(bodyBytes, &res); err != nil {
		// エラー
		panic(err)
	}
	fmt.Println("取引履歴")
	for _, row := range res.CurrencyTransactions {
		balance := ""
		if row.Balance != nil {
			balance = fmt.Sprintf("残高=%d", *row.Balance)
		}
		fmt.Printf("%s %s %s %s(%s)=%d %s\n", row.TransactionAt.String(), row.StoreId, row.TransactionType, row.CurrencyId, row.CurrencyType, row.Quantity, balance)
	}
} else {
	// エラー処理
}