주요 콘텐츠로 건너뛰기
비공개 페이지
이 페이지는 비공개입니다. 검색 엔진에서 색인되지 않으며, 직접 링크가 있는 사용자만 접근할 수 있습니다.

iOS

Experimental

The Swift SDK is experimental. Its public API may change or be removed in any minor release until the SDK is promoted to stable.

Integrate the Aghanim to start accepting payments for your game items online through a prebuilt checkout page. The Checkout on iOS uses our Swift SDK. For it to work properly, you need:

  • Xcode 15 or higher.
  • Minimum iOS deployment target 13.0 or higher.
  • Deployment target 14.0 or higher to follow the SwiftUI examples, which use the SwiftUI app lifecycle.

The Checkout integration mode that works in the player's default browser. Use when you want to redirect the players outside your game.

Swift SDK. Default browser
Swift SDK. Default browser

Register with Aghanim and link your game

First, register for an Aghanim account. At the end of registration, add the link to your mobile game. It should be published in Apple App Store or Google Play Store.

Set up environment

If you want to make real payments, you are all set as the live mode is used as default. Otherwise, use a sandbox, an isolated test environment, to simulate the Aghanim events to test payments without real money movement. To turn on the sandbox mode, set the Sandbox toggle to the active position.

Sandbox supports card payments only. Alternative methods (PayPal, wallets, local payment methods) appear only in live. See Test payments for the test cards.

While integrating, you will need an SDK key to authenticate requests to the Aghanim. Keep in mind that the sandbox and live modes have different keys. Find the SDK key in Integration → API keys.

Configure game client-side

Configure your game client to work with the Checkout by setting up the SDK and implementing the necessary code to process its methods.

Install SDK

To install the Swift SDK, download the latest XCFramework bundle from the Aghanim Artifact Registry, verify its checksum, and embed the frameworks in your Xcode project.

VERSION="1.2.0"
BUNDLE="swift-sdk-v${VERSION}-xcframeworks.zip"
EXPECTED_SHA256="152ec7da96abf098aee5301a6515d9502694deb629d134b4cc188c4c13f1cb21"

# Download the XCFramework bundle.
curl -L -o "${BUNDLE}" \
"https://artifactregistry.googleapis.com/v1/projects/ag-registry/locations/us-central1/repositories/swift-sdk/files/swift-sdk:v${VERSION}:${BUNDLE}:download?alt=media"

# Verify the SHA-256 against the value published in the release notes.
ACTUAL_SHA256=$(shasum -a 256 "${BUNDLE}" | awk '{print $1}')
if [ "${ACTUAL_SHA256}" != "${EXPECTED_SHA256}" ]; then
echo "Checksum mismatch — aborting." >&2
exit 1
fi

# Unzip alongside your Xcode project.
unzip -q "${BUNDLE}" -d swift-sdk

The bundle contains seven XCFrameworks. Drag every .xcframework directory into your Xcode project, then in General → Frameworks, Libraries, and Embedded Content set each one to Embed & Sign.

FrameworkRole
AghanimCheckout.xcframeworkUmbrella entry point. Re-exports the modules below so you can import AghanimCheckout.
AghanimCheckoutDefaultBrowser.xcframeworkDefault-browser launch mode (external Safari via UIApplication.open).
AghanimCheckoutInAppBrowser.xcframeworkIn-app browser launch mode (SFSafariViewController).
AghanimCheckoutWebView.xcframeworkWebView launch mode (WKWebView in an SDK-owned bottom sheet).
AghanimCheckoutCore.xcframeworkShared checkout types: CheckoutParams, CheckoutItem, AghanimPresenter, and the handleRedirect(_:) method.
AghanimCore.xcframeworkAghanim actor, AghanimConfig, ApiKey, ApiError, OrdersAPI, ItemsAPI, PricePointsAPI, and the Order, Item and PricePoint models.
AghanimLogging.xcframeworkLogger protocol, OSLogLogger, LogLevel.

Each framework ships ad-hoc signed; Embed & Sign re-signs them with your team identity at build time.

Optional: dSYMs for release symbolication

If you ship release-mode builds and want symbolicated crash reports, also download swift-sdk-v1.2.0-dSYMs.zip from the same Artifact Registry repository and add the .dSYM directories to your crash-reporter pipeline. They are not needed for development.

Initialize SDK

Build an AghanimConfig with your API key, then construct the Aghanim actor and hold it for the lifetime of the process. The SDK is designed to be instantiated once at app launch.

ApiKey(_:) is failable and rejects a blank key, so unwrap it before building the config.

import SwiftUI
import AghanimCheckout

@main
struct MyGameApp: App {
@StateObject private var session = GameSession(aghanim: Self.makeAghanim())

var body: some Scene {
WindowGroup {
RootView()
.environmentObject(session)
.aghanimCheckoutPresenter()
}
}

private static func makeAghanim() -> Aghanim {
guard let apiKey = ApiKey("YOUR_API_KEY") else {
fatalError("API key cannot be blank.")
}
let config = AghanimConfig.builder(apiKey: apiKey)
.with(logLevel: .debug)
.build()
return Aghanim(config: config)
}
}

@MainActor
final class GameSession: ObservableObject {
let aghanim: Aghanim

init(aghanim: Aghanim) {
self.aghanim = aghanim
}
}
Default log level

AghanimConfig.builder(apiKey:) defaults to LogLevel.warning and OSLogLogger(). Call .with(logLevel: .debug) during development to see SDK lifecycle and network activity in the console.

OptionalAdjust logger

With the SDK, you can read its logs from one of the supported levels. By default the SDK writes them through OSLogLogger, which routes into the unified logging system you already read in Console and Xcode.

The simple usage of the SDK log messages means setting the log level you are interested in the most:

  • .debug: detailed debug information on almost every event.
  • .info: general information on the SDK instance state and its events.
  • .warning: warnings and recoverable errors. Used by default.
  • .error: critical and fatal errors.
  • .none: no logging.
let config = AghanimConfig.builder(apiKey: apiKey)
.with(logLevel: .debug)
.build()
Keep player data out of your logs

The SDK marks its log values as private, so the unified logging system redacts them. A custom Logger receives the message already interpolated, so whatever you forward to a third-party service leaves the device unredacted.

Configure player ID

Since a mobile game has one instance per device, the SDK allows to set the player ID once to use it in all following method calls.

When your game client has the player ID, set it for the current SDK instance. The call is async throws. Surrounding whitespace is trimmed, and a blank or whitespace-only ID is rejected with ApiError.invalidArgument. When the player signs out, call clearPlayerId() so the SDK stops attaching the previous identity to API requests.

import SwiftUI
import AghanimCheckout

struct SignInView: View {
@EnvironmentObject private var session: GameSession

var body: some View {
Button("Sign in") {
Task {
let playerId = await authenticatePlayer()
do {
try await session.aghanim.setPlayerId(playerId)
} catch {
// Surface to the user as a sign-in error.
}
}
}
}

private func signOut() {
Task {
await session.aghanim.clearPlayerId()
}
}
}
Set it before launching the Checkout

Every call that needs a player throws ApiError.playerIdNotSet without one: aghanim.orders.getUnconsumed(), aghanim.orders.consume(orderId:), and all three startCheckout launch modes. Set the player ID immediately after authentication. aghanim.orders.get(id:) and presentCheckout do not need one.

Create item

The integration needs the items to be added to the Dashboard. When creating items, each should have its SKU, a unique identifier for the item within your game backend. You can add their prices, currency, sale configuration, and more.

To add an item to the Dashboard:

  1. Go to SKU Management → Items.
  2. Click Add Item. The site will open the Add Item page.
  3. Enter the item name New item.
  4. Enter the item SKU items.new.ba68a028-2d51-46b4-a854-68fc16af328a.
  5. In the Price block:
    1. Select the Fiat price type for a real money item.
    2. Enter the price 1.99.
  6. Click Add item.

For integration purposes, we have shortened an item setup. Before going live, use every suitable feature while adding items to the Dashboard.

Get items with localized prices

The SDK retrieves items created in the Dashboard with prices localized to the player's region. Use it to show accurate prices in your in-game store before the player proceeds to the Checkout.

let items = try await aghanim.items.get(skus: [
"items.new.ba68a028-2d51-46b4-a854-68fc16af328a",
"items.new.1f2c4d16-9b83-4b52-9e0d-72f3a1c5d904",
])

for storeItem in items {
// Use name, price.display and imageUrl to populate your store.
print("\(storeItem.name): \(storeItem.price.display)")
}

Ask for as many SKUs as your store needs: there is no SKU-count cap to work around. Duplicates are collapsed, items come back in the order you asked for them, and a SKU that is not in the catalog is left out of the result rather than failing the whole call. An empty array throws ApiError.invalidArgument.

Create Checkout item

Create a CheckoutItem value that references the SKU you added to the Dashboard. sku is the only required field; name, description, and imageUrl override the catalog entry for this Order.

let item = CheckoutItem(sku: "items.new.ba68a028-2d51-46b4-a854-68fc16af328a")

As the SDK launches the Checkout in the player's default browser (external Safari), the player needs to be returned to your app once they complete the payment. To return the player to your app, the SDK needs you to specify a deep link.

With Universal Links, the player goes directly to the app without an OS confirmation dialog. Universal Links use standard HTTPS URLs that iOS validates against the Apple App Site Association (AASA) file hosted on your domain. This is the recommended approach for production.

Create a variable for the deep link URL. We will use it later.

let backToGameUrl = "https://yourdomain.com/checkout/return"

Host an apple-app-site-association file at https://<YOUR_DOMAIN>/.well-known/apple-app-site-association. The file must be served over HTTPS with Content-Type: application/json, with no redirects.

{
"applinks": {
"details": [
{
"appIDs": ["ABCDE12345.com.yourcompany.yourgame"],
"components": [
{
"/": "/checkout/return*"
}
]
}
]
}
}

Add the Associated Domains capability to your target in Xcode and list the domain there.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:yourdomain.com</string>
</array>
</dict>
</plist>

When the player returns to your app via the Universal Link, iOS hands the URL to your app. Pass it to aghanim.handleRedirect(_:) so the SDK can complete the checkout flow.

import SwiftUI
import AghanimCheckout

struct RootView: View {
@EnvironmentObject private var session: GameSession

var body: some View {
ContentView()
.onOpenURL { url in
Task {
let handled = await session.aghanim.handleRedirect(url)
guard handled else { return }
// The player came back from a Checkout. Check unconsumed Orders to grant the purchase.
}
}
}
}

Create Checkout params

When all data variables are ready, create another one that represents Checkout params. Checkout params are the programmatic representation of what the player sees when they are on the payment form. Checkout params are associated with a player and items, they are crucial for the Checkout to work. Pass the backToGameUrl you defined earlier so the SDK can route the player back to your app after the payment.

let params = CheckoutParams(
items: [item],
backToGameUrl: backToGameUrl
)

OptionalUse metadata

You can attach custom metadata to the Checkout for item tracking purposes. You can access it through webhooks and in API responses from the Aghanim. Metadata has a structure of "key-value" pairs.

let params = CheckoutParams(
items: [item],
metadata: [
"campaign": "summer_sale",
"ab_variant": "B"
],
backToGameUrl: backToGameUrl
)

OptionalSet post-payment redirect behavior

You can choose the behavior of redirecting the player after they have completed the payment successfully. The difference in the provided by the SDK modes is a delay before redirecting or absence of redirecting.

When the player has completed the payment, the SDK redirects them immediately to the deep link from backToGameUrl.

let params = CheckoutParams(
items: [item],
backToGameUrl: backToGameUrl,
redirectSettings: RedirectSettings(mode: .immediate)
)

OptionalSet checkout appearance

You can set the appearance mode for the Checkout UI. The SDK supports automatic detection based on the system setting, or you can force a specific mode.

The SDK automatically detects and applies the appropriate appearance mode based on the system setting.

let params = CheckoutParams(
items: [item],
backToGameUrl: backToGameUrl,
uiSettings: CheckoutUiSettings(mode: .auto)
)

Launch Checkout

Add a checkout button to your game client that launches the payment form. The SDK creates an order from the provided checkout params and opens it in the player's default browser. On success, you receive the Order ID to track the order. On failure, startCheckout throws an ApiError with debug information for troubleshooting.

import SwiftUI
import AghanimCheckout

struct CheckoutButton: View {
@EnvironmentObject private var session: GameSession
let params: CheckoutParams

var body: some View {
Button("Buy") {
Task {
do {
let orderId = try await session.aghanim.startCheckout(params, mode: .defaultBrowser)
// Track the order ID so you can reconcile the purchase after the player returns.
print("Aghanim checkout started for order \(orderId)")
} catch {
// Log debug information for troubleshooting.
print("Aghanim checkout failed: \(error)")
// TODO: Surface the failure to the player.
}
}
}
}
}

The Default browser mode is designed for flows where the player leaves your app entirely. The SDK does not stay attached to the external browser, so the only signal that the player has returned is the deep link routed to aghanim.handleRedirect(_:).

OptionalHandle errors

Throwing SDK methods fail with an ApiError. Match on the case to decide what the player sees.

do {
let orderId = try await aghanim.startCheckout(params, mode: launchMode)
print("Aghanim checkout started for order \(orderId)")
} catch let error as ApiError {
switch error {
case .playerIdNotSet:
// Set the player ID after sign-in, then let the player retry.
break
case .invalidArgument(.presenter, _, _):
// No window to present over: the app is backgrounded, or several scenes are
// foreground-active. The Order was already created and its ID is not returned, so
// check the presenter before launching rather than calling startCheckout again.
break
case let .invalidArgument(name, reason, _):
// The SDK rejected an argument before the call reached the network.
print("Aghanim rejected \(name): \(reason)")
// TODO: Fix the value you passed.
case let .validation(_, details):
for detail in details {
print("\(detail.location.joined(separator: ".")): \(detail.message)")
}
case .network, .timeout, .serverUnavailable:
// Transient. Offer the player a retry.
break
case .rateLimitExceeded:
// Back off before retrying.
break
case .notAuthenticated, .notAuthorized:
// The API key is missing, wrong, or not permitted for this call. A retry will not help.
break
case .disposed:
// This instance was disposed. Build a new one before calling again.
break
default:
// Log debug information for troubleshooting.
print("Aghanim checkout failed: \(error)")
}
} catch is CancellationError {
print("Aghanim checkout cancelled")
} catch {
// Log debug information for troubleshooting.
print("Aghanim checkout failed: \(error)")
}

ApiError.invalidArgument carries a typed name and reason so you can tell which argument the SDK rejected, and ApiError.validation carries one ValidationErrorDetail per field the server rejected. Cases that carry a payload include a debugMessage meant for your logs, not for the player.

Find the full list of cases in the Swift SDK reference.

Check unconsumed Orders

After the Checkout has launched and you have an Order ID, the player can step away from the payment form, complete the payment, or abandon it entirely. The launch call only confirms that the Order was created and the form opened — it does not tell you whether the player paid. To know which Orders the player has actually paid for and should be granted to them, ask the SDK for the list of unconsumed paid Orders.

let unconsumedOrderIds = try await aghanim.orders.getUnconsumed()

for orderId in unconsumedOrderIds {
do {
try await aghanim.orders.consume(orderId: orderId)
} catch {
print("Aghanim could not consume order \(orderId): \(error)")
continue
}

grantItems(forOrder: orderId) // TODO: Grant the items to the player
}

Consume paid Orders

Consume the Order the player has paid for, then grant its items once the consume succeeds. Consuming the same Order twice fails, so the grant runs only once.

try await aghanim.orders.consume(orderId: orderId)

Full implementation code

import SwiftUI
import AghanimCheckout

@main
struct MyGameApp: App {
@StateObject private var session = GameSession(aghanim: Self.makeAghanim())

var body: some Scene {
WindowGroup {
RootView()
.environmentObject(session)
.onOpenURL { url in
Task {
let handled = await session.aghanim.handleRedirect(url)
guard handled else { return }
// The player came back from a Checkout. Check unconsumed Orders to grant the purchase.
}
}
}
}

private static func makeAghanim() -> Aghanim {
guard let apiKey = ApiKey("YOUR_API_KEY") else {
fatalError("API key cannot be blank.")
}
let config = AghanimConfig.builder(apiKey: apiKey)
.with(logLevel: .debug)
.build()
return Aghanim(config: config)
}
}

@MainActor
final class GameSession: ObservableObject {
let aghanim: Aghanim

init(aghanim: Aghanim) {
self.aghanim = aghanim
}
}

struct RootView: View {
@EnvironmentObject private var session: GameSession

var body: some View {
Button("Buy") {
Task { await buy() }
}
}

private func buy() async {
do {
try await session.aghanim.setPlayerId("player_42")
let item = CheckoutItem(sku: "items.new.ba68a028-2d51-46b4-a854-68fc16af328a")
let params = CheckoutParams(
items: [item],
backToGameUrl: "https://yourdomain.com/checkout/return"
)
let orderId = try await session.aghanim.startCheckout(params, mode: .defaultBrowser)
// Track the order ID so you can reconcile the purchase after the player returns.
print("Aghanim checkout started for order \(orderId)")
} catch {
// Log debug information for troubleshooting.
print("Aghanim checkout failed: \(error)")
// TODO: Surface the failure to the player.
}
}
}

Make payment

Make a payment. If you have set the sandbox mode, use the test card below. In the sandbox, you can make payments only with the test cards — alternative methods like PayPal, wallets, and local payment methods appear only in the live environment. The test cards accept any digits as CVV and any future date as expiry date. Don’t forget to fill in an email address to check the receipt is sent and any postal code as a billing address.

Successful payments

After you complete the payment, you will receive a receipt sent to the specified email address and a transaction record in Aghanim Dashboard → Transactions.

Card BrandCard NumberCVVExpiry dateCountry
VISA (credit)
4242 4242 4242 4242
Any 3 digitsAny future dateGB

Unsuccessful payments

Make an unsuccessful payment just in case you are curious. You will see the transaction in Aghanim Dashboard → Transactions as well.

NumberCVVExpiry dateResponse codeDescription
4832 2850 6160 9015
Any 3 digitsAny future date16Payment declined

OptionalAll payment methods

For the live mode, you can find all supported payment methods in Company settings → Payment methods. Turn on or off those you see suitable. Some payment methods are available globally by default. You can’t disable Credit cards, Apple Pay, Google Pay, and PayPal.

In Checkout, the Aghanim evaluates the currency and any restrictions, then dynamically presents only the payment methods available to the player based on evaluation.

OptionalSaving payment method

When you use the live mode, the payment form shows to the player a setting to save their payment method so they can make a one-click payment in the future.

Handle post-payment events on game server-side

To complete the Checkout, handle items’ granting and chargebacks on your game backend. To do so, implement a webhook system that accepts the item.add and item.remove webhooks. See the code example with the implementation.

Comply with the Aghanim requirements for these webhooks:

  • Use HTTPS schema for the single POST webhook endpoint.
  • Check that webhooks are generated and signed by the Aghanim.
  • Handle the idempotency_key field in the webhook payload to prevent processing duplicate webhooks.
  • Respond with the HTTP status codes:
    • 2xx for successfully processed webhooks.
    • 4xx and 5xx for errors.

Grant items to player

The Aghanim sends the item.add webhook to let you know about the purchased items and ask for your permission to grant them to the player.

When the Aghanim has your 2xx answer, it can complete the checkout logic and redirect the player to a deep link if provided.

Support refunds and chargebacks

The Aghanim sends the item.remove webhook when a bank or payment system reverses the transaction, or you have requested refund in Aghanim Dashboard → Transactions. Partial refunds are not supported.

OptionalUse suggested implementation

The suggested implementation handles the webhooks mentioned before:

  • item.add for granting items. You need it for integration.
  • item.remove for refunds and chargebacks. You might need it for integration.
# 통합에서 웹훅 이벤트를 처리하기 위해 이 샘플 코드를 사용하세요.
#
# 1. 이 코드를 `server.py`라는 새 파일에 붙여 넣으세요.
#
# 2. 의존성 설치:
# python -m pip install fastapi[all]
#
# 3. http://localhost:8000에서 서버를 실행합니다
# python server.py

import fastapi, hashlib, hmac, json, typing
from fastapi.responses import JSONResponse

app = fastapi.FastAPI()

@app.post("/webhook")
async def webhook(request: fastapi.Request) -> dict[str, typing.Any]:
secret_key = "<YOUR_S2S_KEY>" # 실제 웹훅 비밀 키로 교체하세요

raw_payload = await request.body()
payload = raw_payload.decode()
timestamp = request.headers["x-aghanim-signature-timestamp"]
received_signature = request.headers["x-aghanim-signature"]

if not verify_signature(secret_key, payload, timestamp, received_signature):
raise fastapi.HTTPException(status_code=403, detail="Invalid signature")

data = json.loads(payload)
event_type = data["event_type"]
event_data = data["event_data"]

if event_type == "item.add":
add_item(event_data)
return {"status": "ok"}

if event_type == "item.remove":
remove_item(event_data)
return {"status": "ok"}

raise fastapi.HTTPException(status_code=400, detail="Unknown event type")

def verify_signature(secret_key: str, payload: str, timestamp: str, received_signature: str) -> bool:
signature_data = f"{timestamp}.{payload}"
computed_hash = hmac.new(secret_key.encode(), signature_data.encode(), hashlib.sha256)
computed_signature = computed_hash.hexdigest()
return hmac.compare_digest(computed_signature, received_signature)

def add_item(event_data: dict[str, typing.Any]) -> None:
# 이벤트를 처리하고 항목을 추가하기 위한 플레이스홀더 로직입니다.
# 실제 애플리케이션에서는 이 함수가 데이터베이스나 인벤토리 시스템과 상호작용합니다.
player_id = event_data["player_id"]
for item in event_data["items"]:
sku = item["sku"]
print(f"Item {sku} has been credited to player's {player_id} account.")

def remove_item(event_data: dict[str, typing.Any]) -> None:
# 이벤트를 처리하고 항목을 제거하기 위한 플레이스홀더 로직입니다.
# 실제 애플리케이션에서는 이 함수가 데이터베이스나 인벤토리 시스템과 상호작용합니다.
player_id = event_data["player_id"]
for item in event_data["items"]:
sku = item["sku"]
print(f"Item {sku} has been removed from player's {player_id} account.")

if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

Add webhook endpoint to Aghanim

When the webhook handling is ready, add the endpoint to the account so the Aghanim could start sending the events.

  1. Go to Integration → Webhooks.
  2. Click Add webhook. The site will open the Create webhook window.
  3. Copy and paste the URL https://<YOUR_DOMAIN>/webhook.
  4. Click Select events. The site will open the Select events to send window.
  5. Expand the Main class and select the Item add, Item remove checkboxes.
  6. Click Apply.
  7. Click Add. The site will redirect you to the webhook page.
  8. Click Back.

Test your integration

After you have handled the webhooks, check that the purchased items are in your inventory. That’s all.

Next steps

FAQ

What platforms does your SDK support?

The Swift SDK targets iOS 13.0 and higher. It supports both physical devices (arm64) and the iOS Simulator (arm64 + x86_64). macOS, Mac Catalyst, tvOS, watchOS, and visionOS are not supported.

How do I handle apps with multiple scenes?

The AghanimPresenter protocol decouples the SDK from how your app composes its window hierarchy. The bundled KeyWindowPresenter works well for single-scene apps; in SwiftUI you can also read the presenter from the environment via .aghanimCheckoutPresenter() applied to a view inside a specific scene.

For apps that connect more than one foreground scene at the same time (multi-window iPad apps, multitasking-aware apps, Catalyst-style layouts), implement your own AghanimPresenter that returns the view controller of the scene you want to present from, then pass that instance to startCheckout(_:mode:) for that scene's checkout calls.

Should I grant items from the client or the server?

Both flows are supported and you can mix them.

Server-driven (recommended for games with a backend): Configure the item.add webhook in the Aghanim Dashboard. Aghanim calls your server when an order is paid; your server grants the items to the player. The client SDK is only responsible for launching the Checkout and routing the redirect.

Client-driven (for games without a dedicated server): Use aghanim.orders.getUnconsumed() on app launch and after the player returns from checkout to find paid orders the player hasn't been credited for yet, call aghanim.orders.consume(orderId:) for each one, and grant the items once the consume succeeds.

Pick server-driven when you can — webhooks survive the player closing your app mid-flow and don't require the player to re-open it before the grant happens.

도움이 필요하세요?
통합팀에 문의하십시오 [email protected]

On this page