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:
- The user opens the game application on iOS.
- The user clicks the purchase button next to the desired item.
- 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.
- The user is automatically authorized. Pay Station opens for the specific item.
- The user selects a payment method and completes the purchase.
- Pay Station redirects the user to the game application.
- 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.

Set up Xsolla Login
- In your project inside Publisher Account, go to the Login section.
- Click Create Login project.
- 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.
- In the Login methods block, select any option and click Configure.
- In the top settings block, click Login API integration.
- Set the Login with device ID toggle to On.

- 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)
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
The Xsolla Mobile SDK is provided as an XCFramework
named XsollaMobileSDK.xcframework
.
To install it manually in your Xcode project:
- Open your project in Xcode.
- Select your app target and go to the General tab.
- In the Frameworks, Libraries, and Embedded Content section, drag and drop the
XsollaMobileSDK.xcframework
file. - 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.
settings.openExternalBrowser = YES;
(Objective-C) or settings.openExternalBrowser = true;
(Swift) to your SDK configuration.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:
- Select your project in the project navigator.
- Select your target.
- Open the Info tab.
- Click the ➕ button in the URL Types section.
- 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.
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:
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:
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
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
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.
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
- In your project inside Publisher Account, go to the Project setting section.
- Go to the Webhooks section.
- 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. - A secret key to sign project webhooks is generated by default. If you want to generate a new secret key, click the refresh icon.
- Click Enable webhooks.

Testing webhooks
In the Payments tab, you can test the following webhooks:
User validation (“notification_type”:“user_validation”):
- curl
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”):
- curl
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
obj-c
- obj-c
- swift
1SKPaymentSettings* settings = ...;
2
3settings.useSandbox = YES;
1let settings = SKPaymentSettings(...)
2
3settings.useSandbox = true
Enabling additional logging
obj-c
- obj-c
- swift
1SKPaymentSettings* settings = ...;
2
3settings.logLevel = SKLogLevelDebug;
1let settings = SKPaymentSettings(...)
2
3settings.logLevel = .debug
Test cards
For a list of cards to simulate payments in sandbox mode, see the Test cards list section.
Go LIVE
- Sign the licensing agreement. To do this, in Publisher Account, go to the Agreements & Taxes > Agreements section, complete the agreement form.
- Set
sandbox
tofalse
from the SDK configuration code.
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:
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:
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.- swift
1let storefront = await Storefront.current
2let countryCode = storefront?.countryCode
3
4settings.enablePayments = countryCode == "USA"
5
6SKPaymentQueue.default().start(settings)
誤字脱字などのテキストエラーを見つけましたか? テキストを選択し、Ctrl+Enterを押します。