In-App Purchase

Implementing In-App Purchase

Displaying Products In-Game

To show available products to your players, use the GetProductListAsync method provided by the Noctua SDK.


try {
    ProductList productList = await Noctua.IAP.GetProductListAsync();
    for (int i = 0; i < productList.Count; i++)
    {
        // Use the Id, Price and Currency for the PurchaseItemAsync() parameters.
        Debug.Log(productList[i].Id);
        Debug.Log(productList[i].Price);
        Debug.Log(productList[i].Currency);

        Debug.Log(productList[i].Description);
    }
} catch(Exception e) {
    if (e is NoctuaException noctuaEx)
    {
        Debug.Log("NoctuaException: " + noctuaEx.ErrorCode + " : " + noctuaEx.Message);
        outputConsole.text = "NoctuaException: " + noctuaEx.ErrorCode + " : " + noctuaEx.Message;
    } else {
        Debug.Log("Exception: " + e);
        outputConsole.text = "Exception: " + e;
    }
}

This method retrieves the latest product information, including prices in the user's local currency.

Callback for Items Delivery

To ensure players receive their purchased items, you need to set up a callback webhook. This webhook will be notified by the Noctua SDK Backend when a purchase is successful.

  • Implement an API endpoint in your game server to receive purchase notifications
  • Register this webhook URL in the Noctua Developer Dashboard. You can contact us if you need help with this.
  • When a purchase is completed, your webhook will receive a POST request with purchase details

Callback Idempotency

Please ensure that your callback handler is idempotent. This means that when it is called multiple times with the same exact request payload, it should produce an effect only once—for example, ensuring that an item is delivered only once per purchase.

Request Headers

The webhook request will include a custom header for security validation:

X-CALLBACK-TOKEN: [your_secret_token]

Validating the X-CALLBACK-TOKEN

To ensure the callback is genuine and comes from the Noctua SDK Backend:

  1. Retrieve the X-CALLBACK-TOKEN from the request headers.
  2. Compare this token with the secret token provided in your Noctua Developer Dashboard.
  3. If the tokens match, process the webhook. If not, reject the request.

Here's a simple example of how to validate the token

<?php

function validate_callback_token($headers) {
    $expected_token = 'your_secret_token_from_noctua_dashboard';

    // Check if the X-CALLBACK-TOKEN header exists
    if (isset($headers['X-CALLBACK-TOKEN'])) {
        $received_token = $headers['X-CALLBACK-TOKEN'];

        if ($received_token === $expected_token) {
            // Token is valid, process the webhook
            return true;
        }
    }

    // Invalid token or missing header, reject the request
    return false;
}

// Usage example:
$headers = getallheaders(); // This function gets all HTTP headers
if (validate_callback_token($headers)) {
    // Process the webhook
    process_webhook(json_decode(file_get_contents('php://input'), true));
} else {
    // Reject the request
    http_response_code(403);
    echo 'Invalid token';
}
?>

Request Body

The webhook will receive a JSON payload with the following structure:

{
    "data": {
        "order_id": 1234567890,
        "order_status": "completed",
        "order_time": "2024-01-01T12:00:00Z",
        "product_id": "com.yourgame.gems100",
        "amount": 16000,
        "currency": "IDR",
        "amount_in_usd": 1.99,
        "player_id": 987654321,
        "ingame_item_id": "gems_100_pack",
        "ingame_role_id": "player_role_123",
        "ingame_server_id": "game_server_1",
        "platform": "appstore",
        "os": "ios",
        "extra": {
            "promotion_code": "SUMMERSALE"
        }
    },
    "signed_data": "eyJhbGciOiJFUzI1NiIsIm..."
}

The data field contains raw data while the signed_data is the JWS (JSON Web Signature) version of the same data. It is recommended to load from signed_data instead of from data.

JWKS

The JWKS used to verify signed_data is the same as the one used to verify user access token.

Here are some examples on how to verify and load the data from signed_data.

<?php

use Jose\Component\Core\JWK;
use Jose\Component\Core\JWKSet;
use Jose\Component\Signature\JWSVerifier;
use Jose\Component\Signature\Algorithm\ES256;
use Jose\Component\Signature\Serializer\CompactSerializer;

// Fetch JWKS from the server
$jwksJson = file_get_contents('https://sdk-api-v2.noctuaprojects.com/api/v1/auth/jwks');
$jwks = JWKSet::createFromJson($jwksJson);

$signedData = 'SIGNED_DATA_STRING';

// Parse the signed data
$serializer = new CompactSerializer();
$jws = $serializer->unserialize($signedData);

// Get the key ID from the signed data header
$headers = $jws->getSignature(0)->getProtectedHeader();
$kid = $headers['kid'] ?? null;

if ($kid === null) {
    throw new Exception('No "kid" found in signed data header');
}

// Find the corresponding key in the JWKS
$jwk = $jwks->get($kid);

if ($jwk === null) {
    throw new Exception('No matching key found in JWKS');
}

// Verify the signature
$algorithm = new ES256();
$algorithmManager = new \Jose\Component\Core\AlgorithmManager([$algorithm]);
$jwsVerifier = new JWSVerifier($algorithmManager);

if (!$jwsVerifier->verifyWithKey($jws, $jwk, 0)) {
    throw new Exception('Signed data signature verification failed');
}

// If we reach here, the signed data is valid
$payload = json_decode($jws->getPayload(), true);

// Now $payload contains the decoded signed data

The order_status field is expected to have one of the following values:

ValueDescription
unknownOrder status is unknown. This requires further investigation on our side.
pendingOrder is pending. It could be awaiting payment or delivery.
completedOrder is completed successfully.
failedOrder is failed.
refundedOrder is refunded.
canceledOrder is cancelled by user.
expiredOrder is expired.

The platform field is expected to have one of the following values:

ValueDistribution
directDirect distribution
playstoreGoogle Play Store
appstoreApple App Store
huaweistoreHuawei App Gallery
playstationstorePlayStation Store
microsoftstoreMicrosoft Store
nintentdostoreNintendo Store

The os field is expected to have one of the following values:

ValueOperating System
androidAndroid
iosApple iOS
windowsMicrosoft Windows
playstation4Sony PlayStation 4
playstation5Sony PlayStation 5
xboxxsMicrosoft Xbox XS
nintendoswitchNintendo Switch

Response

We will treat any HTTP status code of 200 as a successful callback attempt. The response body can be any content; for example, {"success": true} is sufficient. If the callback API is called more than once for a specific transaction and the callback has already been processed on your end, please continue to return a 200 status code.

If your system is unable to handle the callback, whether due to an expected or unexpected issue, please respond with a non-200 status code and a clear message describing the problem.

  • Invalid request payload: 400 (Bad Request)
  • Invalid signature or decryption failure: 401 (Unauthorized)
  • Unexpected error: 500 (Internal Server Error)

Integrate In-App Purchase

Update Player Account

Make sure to update the player account with the Noctua SDK. This is important to ensure that the purchase is correctly tracked and associated with the player. See Updating Player Account for more information.

To initiate a purchase, use the PurchaseItemAsync method provided by the Noctua SDK. This method handles the platform-specific purchase flow.

try {
    var purchaseResponse = await Noctua.IAP.PurchaseItemAsync(new PurchaseRequest
        {
            // Mandatory parameters
            ProductId = productId,
            Price = 0.1m,
            Currency = "USD",
            IngameItemId = "789",
            IngameItemName = "Skin X"

            // Optional parameters
            //RoleId = "123",
            //ServerId = "456"
        });
    // Handle the purchase result
    if (purchaseResponse.Status == OrderStatus.Completed)
    {
        Debug.Log("Purchase was successful!");
    }
    else
    {
        Debug.LogError("Purchase failed: " + purchaseResponse.Status);
    }
} catch (Exception e) {
    if (e is NoctuaException noctuaEx)
    {
        Debug.Log("NoctuaException: " + noctuaEx.ErrorCode + " : " + noctuaEx.Message);
        outputConsole.text = "NoctuaException: " + noctuaEx.ErrorCode + " : " + noctuaEx.Message;
    } else {
        Debug.Log("Exception: " + e);
        outputConsole.text = "Exception: " + e;
    }
}

This method will trigger the appropriate store's purchase flow (App Store or Play Store) based on the user's device.

Testing Purchases

Use sandbox environments provided by App Store and Play Store to test your IAP integration without making real transactions.

Previous
Setup SKUs in Stores