Accept in-app payments on iOS via Xsolla Pay Station

Why is it important?

In April 2025, Apple updated its guidelines to allow apps distributed on the US App Store to include external links, buttons, and other calls to action that lead users to a developer-controlled website for purchasing digital content or services.

You can now add visible buttons, banners, and messages that take users directly from your game to your Web Shop in a single click — without violating Apple’s rules or risking enforcement.

You’re no longer required to hide messaging behind indirect flows or pay commission on these external purchases.

Both the Web Shop-based integration and the Xsolla Mobile SDK offer a wide range of payment methods — including Apple Pay, which provides users with effortless and familiar checkout experience.

How to choose between Web Shop-based integration and Xsolla Mobile SDK

Web Shop opens the payment UI for a specific item in the browser, based on your existing Web Shop configuration. The Xsolla Mobile SDK triggers a payment flow that also opens the checkout in the browser, but gives you more control over how the flow is initiated and what parameters are passed.

For most use cases, we strongly recommend using the Web Shop-based integration. It offers:

  • Broader game commerce capabilities

Web Shop includes a wide range of built-in game commerce features such as discounts, bonuses, promo codes, personalized offers, free items, and more. These features can be configured in Publisher Account and are automatically applied — without requiring additional client-side logic or UI development.

  • Faster implementation

If you already have a Web Shop, this is the simplest option. Just add a link in your game that opens the Web Shop with the necessary parameters.

Since Web Shop is not part of your app’s codebase, it doesn’t require updates through the App Store. This also makes it easier to expand into new countries as they become available.

  • Full compliance with Apple’s updated guidelines

Web Shop is designed to meet Apple’s external purchase flow requirements by launching in a browser, outside the app.

The Xsolla Mobile SDK offers more flexibility but requires additional development effort. Use the SDK if your integration requires:

  • Advanced authentication methods beyond those supported by Web Shop.

  • Passing additional parameters (e.g., current game state, or session metadata like level, platform, and trigger event) — useful for game-specific scenarios that are not applicable to Web Shop.

  • Fully customizable purchase experience, including complete control over the payment UI and transaction logic.

When using the SDK, game commerce capabilities are also available via API, but the developer is responsible for implementing all business logic, offer calculations, and the app's UI.

Details on how to use the SDK in compliance with Apple’s requirements are provided below.

How it works

In this guide, you’ll learn how to integrate Xsolla for processing payments using Xsolla Mobile SDK, in compliance with Apple’s requirements.

注意

Apple’s requirements:

  • In-app WebViews for external purchases are not allowed — payments must occur via Safari or the default browser.

  • External purchase links are currently permitted only for iOS applications on the United States storefront. Note that Apple’s app review guidelines refer to the United States storefront, not user location.

User flow:

  1. The user opens the game application on iOS.
  2. The user clicks the purchase button next to the desired item.
  3. The application opens Safari (or the default browser) with a Pay Station link that contains the payment token. The SDK handles authorization and item selection.
  4. The user is automatically authorized. Pay Station opens for the specific item.
  5. The user selects a payment method and completes the purchase.
  6. Pay Station redirects the user to the game application.
  7. The application receives the purchase confirmation via a webhook.

Quick start

Sign up for your Publisher Account and create a project

Publisher Account is the main tool to configure Xsolla features, as well as to work with analytics and transactions.

To sign up, go to Publisher Account and create an account. To create a project, click Create project in the side menu and provide any necessary information. You can modify the settings later.

During the integration process, you need to provide the project ID, found in your Publisher Account next to the project name.

お知らせ
For more details, refer to Create project manually.

Set up Xsolla Login

  1. In your project inside Publisher Account, go to the Login section.
  2. Click Create Login project.
  3. Select Standard Login project and click Create and set up. Wait until your new Login project is created. You will then see the Login project page.
  4. In the Login methods block, select any option and click Configure.
  5. In the top settings block, click Login API integration.
  6. Set the Login with device ID toggle to On.
  1. Click Save changes.

During the integration process, you will need your Login ID. To get it, click the name of your Login project in the breadcrumb trail to return to the Login project page, and click Copy ID beside the name of the Login project.

Configure In-App Purchase products (virtual items)

注意
In-App Purchase (IAP) products are represented through virtual items in the Xsolla ecosystem. They have a name, description, SKU, and a price.

Choose one of the following methods to set up your IAP SKU product catalog:

  • Import items – upload a JSON file to quickly add your catalog to Publisher Account.
  • Use API calls – ideal for automated or large-scale updates.

Install SDK

注意
To get access to the Xsolla Mobile SDK during the Developer Preview phase, please fill out the Contact Us form.

The Xsolla Mobile SDK is provided as an XCFramework named XsollaMobileSDK.xcframework.

To install it manually in your Xcode project:

  1. Open your project in Xcode.
  2. Select your app target and go to the General tab.
  3. In the Frameworks, Libraries, and Embedded Content section, drag and drop the XsollaMobileSDK.xcframework file.
  4. In the drop-down list next to the framework, ensure Embed & Sign is selected.

Configure SDK

For SDK configuration you need the following IDs from Publisher Account:

  • Project ID. It can be found in Publisher Account next to the name of your project.

  • Login ID. To access, open Publisher Account, go to the Login > Dashboard > your Login project section, and click Copy ID next to the name of the Login project.

As a starting point, example IDs can be used.

注意
To comply with US requirements when redirecting users to the web, add settings.openExternalBrowser = YES; (Objective-C) or settings.openExternalBrowser = true; (Swift) to your SDK configuration.
Copy
Full screen
Small screen

obj-c

  • obj-c
  • swift
 1#import <XsollaMobileSDK/StoreKitWrapper.h>
 2
 3SKPaymentSettings* settings = [[SKPaymentSettings alloc] initWithProjectId: 77640
 4                                                         loginProjectId:@"026201e3-7e40-11ea-a85b-42010aa80004"
 5                                                         platform: SKPaymentPlatformStandalone
 6                                                         paystationUIThemeId: SKPaystationThemeDark
 7                                                         paystationUISize: SKPaystationSizeMedium];
 8
 9settings.useSandbox = YES;
10settings.enablePayments = YES;
11settings.openExternalBrowser = YES;
12
13SKPaymentQueue* queue = [SKPaymentQueue defaultQueue];
14[queue startWithSettings: settings];
15[queue addTransactionObserver: self];
 1import XsollaMobileSDK
 2
 3let settings = SKPaymentSettings(projectId: 77640,
 4                                 loginProjectId: "026201e3-7e40-11ea-a85b-42010aa80004",
 5                                 platform: .standalone)
 6
 7settings.useSandbox = true;
 8settings.enablePayments = true;
 9settings.openExternalBrowser = true;
10
11SKPaymentQueue.default().start(settings)
12SKPaymentQueue.default().add(self)

Configure deep linking to return users to game app after purchase

Set up URL scheme for the target:

  1. Select your project in the project navigator.
  2. Select your target.
  3. Open the Info tab.
  4. Click the button in the URL Types section.
  5. Set the URL Scheme to $(PRODUCT_BUNDLE_IDENTIFIER).

Initialize SDK

After configuring, the Xsolla Mobile SDK needs to be initialized and connected to Xsolla services. Place this logic in your app’s initialization flow; for example, inside AppDelegate’s didFinishLaunchingWithOptions method.

Copy
Full screen
Small screen

obj-c

  • obj-c
  • swift
 1SKPaymentQueue* queue = [SKPaymentQueue defaultQueue];
 2[queue startWithSettings: settings]; // settings from the previous step
 3
 4// conform your class to SKPaymentTransactionObserver and implement its method
 5@interface YourClass (SKPaymentTransactionObserver) <SKPaymentTransactionObserver>
 6@end
 7
 8@implementation YourClass (SKPaymentTransactionObserver)
 9
10- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
11    for(SKPaymentTransaction *transaction in transactions) {
12        switch (transaction.transactionState) {
13            case SKPaymentTransactionStateFailed:
14                // purchase failed, present an error
15                break;
16            case SKPaymentTransactionStatePurchased:
17                // award the player with the purchase of the SKU - transaction.payment.productIdentifier
18                break;
19            default: break;
20        }
21    }
22}
23
24@end
 1SKPaymentQueue.default().start(settings)
 2SKPaymentQueue.default().add(self)
 3
 4// conform your class to SKPaymentTransactionObserver and implement its method
 5extension YourClass: SKPaymentTransactionObserver {
 6    func paymentQueue(_: SKPaymentQueue, updatedTransactions: [SKPaymentTransaction]) {
 7        for transaction in updatedTransactions {
 8            switch transaction.transactionState {
 9            case .failed:
10                // purchase failed, present an error
11            case .purchased:
12                // award the player with the purchase of the SKU - transaction.payment.productIdentifier
13            default:
14                break
15            }
16        }
17    }
18}

After starting SKPaymentQueue and adding a transaction observer, the application can request SKU information as follows:

Copy
Full screen
Small screen

obj-c

  • obj-c
  • swift
 1NSSet *skus = [NSSet setWithArray:@[@"your_sku1", @"your_sku2"]];
 2SKProductsRequest* req = [[SKProductsRequest alloc] initWithProductIdentifiers:skus];
 3
 4req.delegate = self;
 5[req start];
 6
 7// conform your class to SKProductsRequestDelegate and implement its method
 8@interface YourClass (SKProductsRequestDelegate) <SKProductsRequestDelegate>
 9@end
10
11@implementation YourClass (SKProductsRequestDelegate)
12
13- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
14    // save loaded products somewhere
15    self.products = response.products
16}
17
18@end
 1let skus: Set<String> = ["your_sku1", "your_sku2"]
 2let req = SKProductsRequest(productIdentifiers: skus)
 3
 4req.delegate = self
 5req.start()
 6
 7// conform your class to SKProductsRequestDelegate and implement its method
 8extension YourClass: SKProductsRequestDelegate {
 9    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
10        // save loaded products somewhere
11        self.products = response.products
12    }
13}

Make purchase

The purchasing flow is a multistage process, which involves payment collection, purchase validation, and consumption.

Start purchase flow

To create a payment and initiate a purchase, use previously acquired SKProduct info:

Copy
Full screen
Small screen

obj-c

  • obj-c
  • swift
1SKProduct *product = ...;  // previously fetched product
2SKPayment *payment = [SKPayment paymentWithProduct:product];
3[[SKPaymentQueue defaultQueue] addPayment:payment];
1let product = ... // previously fetched product
2let payment = SKPayment(product: product)
3SKPaymentQueue.default().add(payment)

Validate purchase

注意
Although validation is NOT a mandatory step, we strongly recommended it to mitigate fraud and any other malevolent actions.

Each purchase should be validated before delivering it to the end-user to help prevent unauthorized purchases.

The most effective way to assure the validity of a purchase is to use the server-to-server (S2S) calls, removing the client from the decision making process; thus, avoiding creation of a potential security hole.

Commonly, S2S validation approach follows these steps:

  • The application client sends the purchase’s order ID to the backend. This ID is obtained when the payment transaction is finalized on the client side (typically via a transaction callback). See Start purchase flow for how purchases are initiated.

  • The server receives the order ID and validates its authenticity using the webhook approach (triggered by Xsolla services) as soon as the purchase completes server notification. This allows for asynchronous receipt handling without resorting to polling and can be done in background (the result gets cached) before even the request from a client comes in. For more information on webhooks, see the Set up webhook section.

Consume purchased content

お知らせ
It is strongly recommended to perform at least basic validation before consuming the purchases.

The very final step in the purchasing flow chain is to award the purchases to the user and mark these purchases as “awarded”. The process is also known as, “purchase consumption”.

The purchase result is delivered through the paymentQueue:updatedTransactions: callback in Objective-C, or paymentQueue(_:updatedTransactions:) in Swift.

Copy
Full screen
Small screen

obj-c

  • obj-c
  • swift
 1- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
 2    for(SKPaymentTransaction *transaction in transactions) {
 3        switch (transaction.transactionState) {
 4            case SKPaymentTransactionStateFailed:
 5                // Always acknowledge transaction and finish it
 6                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
 7                break;
 8            case SKPaymentTransactionStatePurchased:
 9                // here you can save the purchase and award it to the user
10                // Always acknowledge transaction and finish it after it was saved
11                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
12                break;
13            default: break;
14        }
15    }
16}
 1func paymentQueue(_: SKPaymentQueue, updatedTransactions: [SKPaymentTransaction]) {
 2    for transaction in updatedTransactions {
 3        switch transaction.transactionState {
 4        case .failed:
 5            // Always acknowledge transaction and finish it
 6            SKPaymentQueue.default().finishTransaction(transaction)
 7        case .purchased:
 8            // here you can save the purchase and award it to the user
 9            // Always acknowledge transaction and finish it after it was saved
10            SKPaymentQueue.default().finishTransaction(transaction)
11        default:
12            break
13        }
14    }
15}

Set up webhooks

Webhooks are notifications about events occurring in the system. When a specific event occurs, Xsolla sends an HTTP request, transmitting event data to your game server. These webhooks are essential for the game client and/or server to receive notification on successful and unsuccessful payments and user authentication attempts.

Enabling webhooks

  1. In your project inside Publisher Account, go to the Project setting section.
  2. Go to the Webhooks section.
  3. In the Webhook URL field, specify the server URL—where you want to receive webhooks in the https://example.com format. You can also specify the URL you find in a tool for testing webhooks.
  4. A secret key to sign project webhooks is generated by default. If you want to generate a new secret key, click the refresh icon.
  5. Click Enable webhooks.

Testing webhooks

お知らせ
More information about all webhooks can be found in the Webhooks section.

In the Payments tab, you can test the following webhooks:

User validation (“notification_type”:“user_validation”):

Copy
Full screen
Small screen
 1curl -v 'https://your.hostname/your/uri' \
 2-X POST \
 3-H 'Accept: application/json' \
 4-H 'Content-Type: application/json' \
 5-H 'Authorization: Signature 13342703ccaca5064ad33ba451d800c5e823db8f' \
 6-d '{
 7    "notification_type":"user_validation",
 8    "settings": {
 9      "project_id": 18404,
10      "merchant_id": 2340
11    },
12    "user": {
13        "ip": "127.0.0.1",
14        "phone": "18777976552",
15        "email": "[email protected]",
16        "id": "1234567",
17        "name": "John Smith",
18        "country": "US"
19    }
20}'

Payment (“notification_type”: “payment”):

Copy
Full screen
Small screen
  1curl -v 'https://your.hostname/your/uri' \
  2-X POST \
  3-d '{
  4    "notification_type": "payment",
  5    "settings": {
  6      "project_id": 18404,
  7      "merchant_id": 2340
  8    },
  9    "purchase": {
 10        "subscription": {
 11            "plan_id": "b5dac9c8",
 12            "subscription_id": "10",
 13            "product_id": "Demo Product",
 14            "date_create": "2014-09-22T19:25:25+04:00",
 15            "date_next_charge": "2014-10-22T19:25:25+04:00",
 16            "currency": "USD",
 17            "amount": 9.99
 18        },
 19        "checkout": {
 20            "currency": "USD",
 21            "amount": 50
 22        },
 23        "total": {
 24            "currency": "USD",
 25            "amount": 200
 26        },
 27        "promotions": [{
 28            "technical_name": "Demo Promotion",
 29            "id": 853
 30        }],
 31        "coupon": {
 32            "coupon_code": "ICvj45S4FUOyy",
 33            "campaign_code": "1507"
 34        },
 35        "order": {
 36          "id": 1234
 37          "lineitems": [
 38              {
 39                "sku": "test_1",
 40                "quantity": 1,
 41                "price": {
 42                  "currency": "EUR",
 43                  "amount": 6.5
 44                  }
 45              }
 46          ]
 47        }
 48    },
 49    "user": {
 50        "ip": "127.0.0.1",
 51        "phone": "18777976552",
 52        "email": "[email protected]",
 53        "id": "1234567",
 54        "name": "John Smith",
 55        "country": "US"
 56    },
 57    "transaction": {
 58        "id": 1,
 59        "external_id": 1,
 60        "payment_date": "2014-09-24T20:38:16+04:00",
 61        "payment_method": 1,
 62        "payment_method_name": "PayPal",
 63        "payment_method_order_id": 1234567890123456789,
 64        "dry_run": 1,
 65        "agreement": 1
 66    },
 67    "payment_details": {
 68        "payment": {
 69            "currency": "USD",
 70            "amount": 230
 71        },
 72        "vat": {
 73            "currency": "USD",
 74            "amount": 0,
 75            "percent": 20
 76        },
 77        "sales_tax": {
 78            "currency": "USD",
 79            "amount": 0,
 80            "percent": 0
 81        },
 82        "direct_wht": {
 83            "currency": "USD",
 84            "amount": 0,
 85            "percent": 0
 86        },
 87        "payout_currency_rate": "1",
 88        "payout": {
 89            "currency": "USD",
 90            "amount": 200
 91        },
 92        "xsolla_fee": {
 93            "currency": "USD",
 94            "amount": 10
 95        },
 96        "payment_method_fee": {
 97            "currency": "USD",
 98            "amount": 20
 99        },
100        "repatriation_commission": {
101            "currency": "USD",
102            "amount": 10
103        }
104    },
105    "custom_parameters": {
106        "parameter1": "value1",
107        "parameter2": "value2"
108    }
109}'

Test on sandbox

This section contains code snippets and examples demonstrating how to configure the sandbox environment for testing payments, enabling detailed logging, and other related tasks.

Enabling sandbox mode

注意
Remember to turn the sandbox mode OFF before going LIVE.
Copy
Full screen
Small screen

obj-c

  • obj-c
  • swift
1SKPaymentSettings* settings = ...;
2
3settings.useSandbox = YES;
1let settings = SKPaymentSettings(...)
2
3settings.useSandbox = true

Enabling additional logging

注意
Make sure the debug logging is OFF before going LIVE.
Copy
Full screen
Small screen

obj-c

  • obj-c
  • swift
1SKPaymentSettings* settings = ...;
2
3settings.logLevel = SKLogLevelDebug;
1let settings = SKPaymentSettings(...)
2
3settings.logLevel = .debug

Test cards

お知らせ
Test cards only work in sandbox mode.

For a list of cards to simulate payments in sandbox mode, see the Test cards list section.

Go LIVE

注意
Before you sign the licensing agreement, you can open the payment UI only in sandbox mode.
  1. Sign the licensing agreement. To do this, in Publisher Account, go to the Agreements & Taxes > Agreements section, complete the agreement form.
  2. Set sandbox to false from the SDK configuration code.
Copy
Full screen
Small screen

obj-c

  • obj-c
  • swift
1SKPaymentSettings* settings = ...;
2
3settings.useSandbox = NO;
4settings.logLevel = SKLogLevelError;
1let settings = SKPaymentSettings(...)
2
3settings.useSandbox = false
4settings.logLevel = .error

How to detect iOS Storefront

To determine the current iOS storefront and adjust SDK functionality based on the region, use the following code snippets:

Copy
Full screen
Small screen

obj-c

  • obj-c
  • swift
1[SKPaymentQueue loadCurrentStorefrontCountryCodeWithCompletion:^(NSString* _Nullable countryCode) {
2    settings.enablePayments = countryCode && [countryCode isEqualToString:@"USA"];
3
4    [[SKPaymentQueue defaultQueue] startWithSettings:settings];
5}];
1SKPaymentQueue.loadCurrentStorefrontCountryCode { countryCode in
2    settings.enablePayments = countryCode == "USA"
3
4    SKPaymentQueue.default().start(settings)
5}

The loadCurrentStorefrontCountryCode method asynchronously retrieves the three-letter country code for the current Storefront. You can use this information to enable or disable SDK functionality for specific regions.

Alternatively, you can use Apple’s native Storefront directly, as shown below:

注意
We recommend avoiding the Objective-C SKStorefront implementation, as it performs synchronous loading that blocks the main thread. This can lead to UI freezes and degraded user experience, as noted in Apple’s official documentation.
Copy
Full screen
Small screen
1let storefront = await Storefront.current   
2let countryCode = storefront?.countryCode
3
4settings.enablePayments = countryCode == "USA"
5
6SKPaymentQueue.default().start(settings)
この記事は役に立ちましたか?
ありがとうございます!
改善できることはありますか? メッセージ
申し訳ありません
この記事が参考にならなかった理由を説明してください。 メッセージ
ご意見ありがとうございました!
あなたのメッセージを確認し、体験を向上させるために利用させていただきます。
最終更新日: 2025年5月16日

誤字脱字などのテキストエラーを見つけましたか? テキストを選択し、Ctrl+Enterを押します。

問題を報告する
当社は常にコンテンツを見直しています。お客様のご意見は改善に役立ちます。
フォローアップ用のメールをご提供してください
ご意見ありがとうございました!
フィードバックを送信できませんでした
後でもう一度お試しいただくか、[email protected]までお問い合わせください。
OSZAR »