跳至主要内容

Swift SDK reference

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.

The Aghanim Swift SDK that allows you to use the Checkout within your iOS app.

In-app browser
In-app browser

In-app browser

Everything hangs off the Aghanim actor. In the Swift examples below, aghanim is that instance, and one import covers every symbol they use:

import AghanimCheckout

Aghanim is an actor, so await every call on it. Your AghanimPresenter and Checkout delegate conformances are called on the main actor, so declare them @MainActor.

Integration

To integrate the SDK, see its prerequisites and the detailed instruction on Integrate → iOS.

Method reference

Initialize SDK

To create the SDK instance, build an AghanimConfig and pass it to Aghanim(config:). Hold the instance for the lifetime of the process: the SDK is designed to be created once at app launch.

guard let apiKey = ApiKey("YOUR_API_KEY") else {
fatalError("API key cannot be blank.")
}

let config = AghanimConfig.builder(apiKey: apiKey)
.with(logLevel: .debug)
.build()

let aghanim = Aghanim(config: config)

ApiKey(_:) is failable and returns nil for a blank or whitespace-only key, so unwrap it before building the config.

ParameterTypeRequiredDescription
apiKeyApiKeyYesThe SDK key for your game, from the Dashboard.
loggerany LoggerNoWhere the SDK writes its logs. Defaults to OSLogLogger().
logLevelLogLevelNoThe lowest level the SDK writes. Defaults to .warning.

LogLevel has these cases:

LevelMeaning
.debugDetailed debug information on almost every event.
.infoGeneral information on the SDK instance state and its events.
.warningWarnings and recoverable errors. Used by default.
.errorCritical and fatal errors.
.noneNo logging.

Dispose the SDK instance

To release an instance you are done with, use the dispose() method. Calling it more than once is harmless, and afterwards API calls on that instance fail with ApiError.disposed. A normal app never needs it: create one instance at launch and keep it.

await aghanim.dispose()

Get Order

To fetch a single Order by its ID, use the orders.get(id:) method. Returns an Order. See Order reference for its fields.

let order = try await aghanim.orders.get(id: orderId)

print("Order \(order.id) is \(order.status)")
for orderItem in order.items {
print("\(orderItem.quantity) x \(orderItem.name)")
}
ParameterTypeRequiredDescription
idStringYesUnique ID for the Order.

Get unconsumed Orders

To know what Orders have been paid for but not granted yet, use the orders.getUnconsumed() method. Requires the player ID to be set via Set player ID.

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

Returns [String], the list of unconsumed Order IDs.

Consume paid Order

To mark a paid Order as handled, use the orders.consume(orderId:) method. Consuming the same Order twice fails, so grant its items once the consume succeeds. Requires the player ID to be set via Set player ID.

try await aghanim.orders.consume(orderId: orderId)
ParameterTypeRequiredDescription
orderIdStringYesUnique ID for the Order.

Get items

To retrieve items with localized prices, use the items.get(skus:locale:) method. It returns items created in SKU Management → Items with prices localized to the player's region.

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)")
}
ParameterTypeRequiredDescription
skus[String]YesItem SKUs to retrieve. Must not be empty.
localeAghanimLocaleNoLocale for localization. Find the full list of supported locales in Checkout → Locales.

skus can be of any length: there is no SKU-count cap for you to work around. Duplicate SKUs 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 skus throws ApiError.invalidArgument.

Each returned Item carries:

PropertyTypeDescription
skuStringSKU identifier of the item.
nameStringName of the item.
descriptionString?Description of the item.
typeItemTypeType of the item.
priceItemPriceLocalized price of the item.
imageUrlString?Image URL of the item.
quantityIntBase quantity the item grants.
isStackableBoolWhether the item is stackable.
isCurrencyBoolWhether the item is a virtual currency.

ItemPrice carries:

PropertyTypeDescription
amountIntPrice in the smallest currency unit, for example cents.
amountDecimalDecimalPrice in major currency units.
currencyStringISO 4217 currency code.
displayStringFormatted price string, for example $9.99.

ItemType has these cases:

CaseMeaning
.itemRegular item.
.currencyIn-game currency.
.bundleBundle of items.
.lootboxLootbox with random contents.
.subscriptionSubscription item.
.virtualCurrencyVirtual currency item.
.unknown(String)A type this SDK version does not recognize.

A type added on the server after your SDK version shipped arrives as .unknown, carrying the server's original string, so keep a default branch when you switch on it.

Get price points

To retrieve the price points available to the player, use the pricePoints.get(locale:) method. Use it to render a price ladder without creating an Order first.

let response = try await aghanim.pricePoints.get()

print("Prices for \(response.context.country) in \(response.context.localCurrency)")
for pricePoint in response.pricePoints {
print("\(pricePoint.id): \(pricePoint.localPrice.display)")
}
ParameterTypeRequiredDescription
localeAghanimLocaleNoLocale for localization. Find the full list of supported locales in Checkout → Locales.

PricePointsResponse carries:

PropertyTypeDescription
contextPricePointsContextCountry and currencies the prices were resolved for.
pricePoints[PricePoint]Price points available for that context.

PricePointsContext carries:

PropertyTypeDescription
countryStringCountry code the prices were resolved for.
baseCurrencyStringISO 4217 code of the base currency.
localCurrencyStringISO 4217 code of the player's local currency.

PricePoint carries:

PropertyTypeDescription
idStringUnique ID of the price point.
basePricePricePrice in the base currency.
localPriceLocalPricePrice in the player's local currency.

Price carries amount (Int, smallest currency unit) and amountDecimal (Decimal, major units). LocalPrice carries the same two plus display (String), the formatted string to show the player.

Set player ID

To set the player ID once for the current SDK instance, use the setPlayerId(_:) method. The SDK will use the ID in all following method calls. Surrounding whitespace is trimmed, and a blank or whitespace-only ID throws ApiError.invalidArgument.

try await aghanim.setPlayerId("player_42")
ParameterTypeRequiredDescription
playerIdStringYesUnique ID for the player.

Clear player ID

To remove the player ID from the SDK instance, for example when the player signs out, use the clearPlayerId() method.

await aghanim.clearPlayerId()

Handle redirect

To route a deep link or Universal Link back into the SDK so it can complete an in-progress checkout, use the handleRedirect(_:) method.

let matched = await aghanim.handleRedirect(url)
ParameterTypeRequiredDescription
urlURLYesThe URL iOS handed your app via the deep link.

Returns true when the URL belonged to an active checkout, and false when it did not, so you can fall through to your own deep-link routing. The method is @discardableResult, so you may ignore the value, but true is your cue that the player has come back from a payment and it is worth checking unconsumed Orders.

Create Checkout item

To create an item representation, use the CheckoutItem initializer. The item must already exist in SKU Management → Items, either created there or through the S2S API. Checkout rejects a SKU that is not in the game's catalog, and takes every price from the catalog; the client cannot set or override a price.

let item = CheckoutItem(sku: "items.new.ba68a028-2d51-46b4-a854-68fc16af328a")
ParameterTypeRequiredDescription
skuStringYesItem SKU from Dashboard.
nameStringNoOverrides the catalog name for this Order.
descriptionStringNoOverrides the catalog description for this Order.
imageUrlStringNoOverrides the catalog image for this Order.

Create redirect behavior

To choose the behavior of redirecting the player after they have completed the payment successfully, use the RedirectSettings initializer.

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

let redirectSettings = RedirectSettings(mode: .immediate)
ParameterTypeRequiredDescription
modeRedirectModeNoRedirect mode. Possible values: .immediate, .delayed, .noRedirect. Defaults to no redirect when left unset.
delaySecondsIntNoDelay before the SDK redirects the player when mode is .delayed. The server accepts up to 300 seconds and defaults to 5 when left unset.

Create UI settings

To set the appearance mode for the Checkout, use the CheckoutUiSettings initializer.

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

let uiSettings = CheckoutUiSettings(mode: .auto)
ParameterTypeRequiredDescription
modeCheckoutUiModeNoUI mode. Possible values: .auto, .dark, .light.

Create Checkout params

To create Checkout params, a representation of what the player sees on the payment form, use the CheckoutParams initializer. The initializer does not throw: an empty items or a blank sku surfaces later, when you launch the Checkout, as ApiError.invalidArgument.

For the WebView launch mode, pass backToGameUrl so the SDK can detect the post-payment redirect and dismiss the WebView. Configure that URL as a Universal Link (preferred) or custom URL scheme so payment flows that leave the WebView can return to your app.

let params = CheckoutParams(
items: [item],
backToGameUrl: "https://yourdomain.com/checkout/return"
)
ParameterTypeRequiredDescription
items[CheckoutItem]YesNon-empty list of items.
metadata[String: String]NoMetadata structured as "key-value" pairs for tracking purposes.
localeAghanimLocaleNoLocale for localization. Find the full list of supported locales in Checkout → Locales.
backToGameUrlStringNoDeep link URL to return the player to your app after payment.
redirectSettingsRedirectSettingsNoPost-payment redirect behavior.
uiSettingsCheckoutUiSettingsNoCheckout appearance settings.

Choose a launch mode

Both startCheckout and presentCheckout take a CheckoutLaunchMode that decides where the Checkout opens:

CasePayloadOpens the Checkout in
.defaultBrowserThe player's default browser, outside your app.
.inAppBrowser(presenter:delegate:)presenter: any AghanimPresenter, delegate: (any InAppBrowserCheckoutDelegate)?, defaulting to nilAn SFSafariViewController presented over your app.
.webView(presenter:config:delegate:)presenter: any AghanimPresenter, config: WebViewConfig, delegate: (any WebViewCheckoutDelegate)?, defaulting to nilA WKWebView in an SDK-owned bottom sheet over your app.

AghanimPresenter is a protocol with one @MainActor requirement, presentationContext: UIViewController?, that tells the SDK what to present from. Use KeyWindowPresenter() for a single-scene app, or your own conformance for a multi-scene one, as described under FAQ. If the presenter cannot supply a view controller, the launch throws ApiError.invalidArgument with name .presenter and reason .unusable.

WebViewConfig has one property:

PropertyTypeDescription
allowsBackForwardNavigationBoolWhether the WebView allows back and forward navigation. Defaults to false.

InAppBrowserCheckoutDelegate and WebViewCheckoutDelegate each declare a single @MainActor requirement, didCloseCheckout(orderId: String), called when the Checkout closes, whether the player dismissed it or the redirect returned them to your app. Both refine AnyObject, so a class conforms and a struct cannot: in UIKit the presenting view controller can adopt them directly, and in SwiftUI use a separate class. One object can adopt both when your app offers more than one mode.

The Default browser mode has no delegate: once the player leaves your app, iOS gives you no close signal, so the only notification that they came back is the deep link routed to Handle redirect.

Launch Checkout

To launch the Checkout process, use the startCheckout(_:mode:) method. The method creates an Order from the provided Checkout params and opens the Checkout UI in the chosen mode. Returns the Order ID on success and throws an ApiError on failure. Requires the player ID to be set via Set player ID.

The launch mode opens the Checkout in a WKWebView inside an SDK-owned bottom sheet over your app.

let orderId = try await aghanim.startCheckout(
params,
mode: .webView(
presenter: presenter,
config: WebViewConfig(),
delegate: checkoutDelegate
)
)
ParameterTypeRequiredDescription
paramsCheckoutParamsYesCheckout configuration.
modeCheckoutLaunchModeYesWhere the Checkout opens. See Choose a launch mode.

If you link a single launch-mode module instead of the AghanimCheckout umbrella, call that module's direct overload rather than passing a mode: startCheckout(_:) from AghanimCheckoutDefaultBrowser, startCheckout(_:presenter:delegate:) from AghanimCheckoutInAppBrowser, or startCheckout(_:presenter:webViewConfig:delegate:) from AghanimCheckoutWebView. The WebView overload spells the label webViewConfig:, where the enum case spells it config:.

Present Checkout

To present the Checkout UI for an existing Order, use the presentCheckout(orderId:mode:) method. Use this when you have an Order ID from server-to-server order creation. Unlike startCheckout, it does not require a player ID.

The launch mode opens the existing Order in a WKWebView inside an SDK-owned bottom sheet over your app.

try await aghanim.presentCheckout(
orderId: orderId,
mode: .webView(
presenter: presenter,
config: WebViewConfig(),
delegate: checkoutDelegate
)
)
ParameterTypeRequiredDescription
orderIdStringYesID of the existing Order to open.
modeCheckoutLaunchModeYesWhere the Checkout opens. See Choose a launch mode.

The per-module overloads are presentCheckout(orderId:), presentCheckout(orderId:presenter:delegate:), and presentCheckout(orderId:presenter:webViewConfig:delegate:).

Order reference

orders.get(id:) returns an Order from AghanimCore. Its properties are all let. Every field below is non-optional unless the type is marked with ?.

The types an Order reports back are not the types you send in CheckoutParams: you send RedirectSettings and CheckoutUiSettings, and you read OrderRedirectSettings and UiSettings. Only the read types carry an unknown case, for values the server adds after your SDK version shipped.

PropertyTypeDescription
idStringUnique ID of the Order.
playerIdStringID of the player who placed the Order, as you set it via setPlayerId(_:).
userIdStringIdentifier of the user who created the Order.
countryStringCountry code for the Order, for example US.
currencyStringISO 4217 currency code of the Order.
priceMinorUnitIntNumber of decimal places for currency, for example 2 for USD.
amountIntTotal amount in the smallest currency unit, for example cents for USD.
items[OrderItem]Items included in the Order. See Order item.
statusOrderStatusCurrent status of the Order. See Order status.
checkoutUrlStringURL of the payment form for this Order.
emailString?Email address associated with the Order.
ipAddressString?IP address the Order was created from.
userAgentString?User agent the Order was created from.
localeAghanimLocale?Locale of the Order. Find the full list in Checkout → Locales.
platformPlatform?Platform the Order was created on. See Platform.
meta[String: String]?Developer-defined metadata attached to the Order, for example via CheckoutParams.metadata.
couponsEnabledBool?Whether coupons are enabled for this Order.
countryLockedBool?Whether the country selection is locked for this Order.
uiSettingsUiSettings?Appearance applied to the Checkout. See UI settings.
redirectSettingsOrderRedirectSettings?Post-payment redirect applied to the Checkout. See Redirect settings.
eligibleForRewardPointsInt?Reward points this Order is eligible for.
eligibleForLoyaltyPointsInt?Loyalty points this Order is eligible for.
rewardsOrderRewards?Virtual currency rewards attached to the Order. See Order rewards.
backToGameUrlString?URL that returns the player to the game after payment.
backToGameSettingsBackToGameSettings?Per-platform return links configured for the game. See Back-to-game settings.

To render amount as a human-readable price, divide it by 10 to the power of priceMinorUnit. An amount of 1999 with a priceMinorUnit of 2 is 19.99.

Order status

OrderStatus has these cases:

CaseMeaning
.createdOrder created, awaiting payment.
.capturedPayment captured and processing.
.paidPayment completed; goods and rewards granted.
.canceledOrder canceled.
.refundedOrder refunded.
.refundRequestedA refund has been requested.
.reattemptedPayment was reattempted.
.disputedOrder is under dispute.
.unknown(String)A status this SDK version does not recognize.

A status added on the server after your SDK version shipped arrives as .unknown, carrying the server's original string, so keep a default branch when you switch on it.

Order item

PropertyTypeDescription
skuStringSKU identifier of the item.
nameStringName of the item.
currencyStringISO 4217 currency code for the item price.
quantityIntQuantity of the item in the Order.
descriptionString?Description of the item.
imageUrlString?Image URL of the item.

An OrderItem reports no price of its own. Use the Order's amount and priceMinorUnit for the total the player paid, or Get items for a per-SKU price.

Order rewards

OrderRewards carries:

PropertyTypeDescription
available[OrderVirtualCurrencyReward]Available rewards for this Order.

Each OrderVirtualCurrencyReward carries:

PropertyTypeDescription
virtualCurrencyIdString?ID of the virtual currency the reward grants.
amountIntReward amount.
nameString?Display name of the reward, for example Gems.
iconUrlString?Icon URL of the reward.

Back-to-game settings

BackToGameSettings holds the iOS return links configured for the game:

PropertyTypeDescription
iosCustomUrlSchemeString?Custom URL scheme link, for example mygame://aghanim/return.
iosUniversalLinkString?Universal Link, for example https://mygame.example.com/aghanim/return.

Platform

Platform has these cases:

CaseMeaning
.anyAny platform.
.iosiOS.
.androidAndroid.
.otherOther platform.
.unknown(String)A platform this SDK version does not recognize.

UI settings

Order.uiSettings reports the appearance the server applied:

PropertyTypeDescription
modeUiMode?Appearance applied to the Checkout.
showPlayerInfoBool?Whether the Checkout showed the player's account info.

UiMode has these cases:

CaseMeaning
.autoDetect and apply the appropriate mode.
.darkForce dark mode appearance.
.lightForce light mode appearance.
.unknown(String)A mode this SDK version does not recognize.

Redirect settings

Order.redirectSettings reports the post-payment redirect the server applied:

PropertyTypeDescription
modeOrderRedirectMode?Redirect applied after a successful payment.
delaySecondsInt?Delay before redirecting, used with .delayed.
targetRedirectTarget?Where the redirect sends the player.

OrderRedirectMode has these cases:

CaseMeaning
.noRedirectNo automatic redirect.
.immediateRedirect immediately.
.delayedRedirect after a visible countdown.
.unknown(String)A mode this SDK version does not recognize.

RedirectTarget has these cases:

CaseMeaning
.gameRedirect to the game, via backToGameUrl.
.storeRedirect to the store.
.unknown(String)A target this SDK version does not recognize.

Error reference

Throwing SDK methods fail with an ApiError, except a cancelled call, which throws CancellationError. Match on the case in a do/catch to map an error to user-visible behavior. Every case except .playerIdNotSet and .disposed carries a debugMessage meant for your logs.

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)")
}
CasePayloadWhen fired
.networkdebugMessage: StringNo connectivity, DNS failure, and similar transport errors.
.timeoutdebugMessage: StringHTTP 408 or 504, or a client-side request timeout.
.notAuthenticateddebugMessage: StringHTTP 401.
.notAuthorizeddebugMessage: StringHTTP 403.
.badRequestdebugMessage: StringHTTP 400.
.notFounddebugMessage: StringHTTP 404.
.conflictdebugMessage: StringHTTP 409.
.validationdebugMessage: String, errors: [ValidationErrorDetail]HTTP 422.
.rateLimitExceededdebugMessage: StringHTTP 429.
.serverUnavailabledebugMessage: StringHTTP 503.
.servercode: Int, debugMessage: StringAny other HTTP 5xx.
.playerIdNotSetAn API that needs a player was called before setPlayerId(_:).
.invalidArgumentname: ArgumentName, reason: ArgumentReason, debugMessage: StringAn argument you passed failed client-side validation.
.disposedThe operation was issued against an instance whose dispose() was called.
.unknowndebugMessage: StringCatch-all: an unmapped status code, an undecodable response, or a URL the SDK could not open.

Validation error detail

.validation carries a list of ValidationErrorDetail, one per field-level error:

PropertyTypeDescription
location[String]Path to the invalid field, for example ["body", "email"].
messageStringHuman-readable error message.
typeStringError type identifier, for example value_error.email.

Invalid argument

.invalidArgument is raised by the SDK before any request goes out. It is distinct from .badRequest, which is an HTTP 400 from the server.

name identifies the offending argument:

CaseArgument
.orderIdThe orderId of Order and present-checkout methods.
.playerIdThe playerId of setPlayerId(_:).
.skuThe sku of a single Checkout item.
.skusThe skus of catalog lookups.
.itemsThe items of a Checkout.
.presenterThe presenter of a launch mode that presents over your app.

reason describes how it failed validation:

CaseMeaning
.blankA required identifier was empty or whitespace.
.emptyA required collection had no entries.
.unusableThe value was supplied but could not be used.

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]