Skip to main content

PLATFORM COMPLIANCE

Reporting External Purchases to Apple

When you sell through Xsolla on storefronts where Apple permits external purchases, Apple still requires you to report those transactions. This page explains what Apple expects, and how to build that report from the data Xsolla already sends you.

Who reports what

Reporting is part of your integration, not something Xsolla does on your behalf.

Handled by
Processing the payment, tax calculation, refundsXsolla
Notifying your backend of each orderXsolla (webhooks)
Obtaining Apple's external purchase token in your appYou
Filing the report with AppleYou, from your own server

Apple's reporting API is authenticated with your App Store Connect key and is tied to your app record, so no payment provider can file on your behalf. What Xsolla provides is the transaction data the report is built from.

When this applies

Apple's rules differ sharply by storefront, so check where you actually ship before building anything.

StorefrontEntitlementApple commissionReporting required
United StatesNot requiredNot currently chargedNo
JapanRequiredYesYes
BrazilRequiredYesYes
European UnionRequiredYesYes
note

Rates, deadlines and eligibility change often, and they differ per region and per developer program. This page deliberately avoids repeating Apple's numbers. Always check Apple's own pages for current terms: Payment options in Japan ↗, Payment options in Brazil ↗, and Payment options in the EU ↗.

Apple's EU terms change on October 1, 2026, including which payment options you may combine in one app. If you ship in the EU, re-read Apple's page before you settle on a design.

important

The entitlement is requested from and granted by Apple, and comes with its own addendum to your developer agreement. Sort that out before you write code, since the StoreKit APIs below return nothing without it.

To enable external payments only for the storefronts that permit them, see How to detect iOS Storefront?.

The flow end to end

  1. Your app checks eligibility and asks StoreKit for an external purchase token.
  2. Your app sends that token to your backend, which stores it against the customer.
  3. The player buys through Xsolla as normal.
  4. Xsolla sends your backend an order_paid webhook with the financial detail of that order.
  5. Your backend pairs the token with the order data and files the report with Apple.
  6. Refunds and chargebacks arrive as order_canceled and are reported to Apple as refund line items.

Getting the token

Tokens come from StoreKit, not from the Xsolla SDK. Apple's ExternalPurchaseCustomLink API is the current surface for custom links and in-app alternative payments. Apple requires these calls in a fixed order: canMakePayments, then isEligible, then the disclosure sheet.

Swift only

ExternalPurchaseCustomLink is a Swift enumeration with async members, so it has no Objective-C counterpart. This differs from the SKPaymentQueue-style API that Xsolla SDK mirrors, which is available in both languages. If your app is written in Objective-C, add a small Swift file to your target and expose it with @objc, as shown in the Objective-C tab below.

// Whether this player is allowed to pay at all. Must come first.
guard AppStore.canMakePayments else { return }

// False when the app lacks the entitlement, or the storefront doesn't permit external purchases.
guard await ExternalPurchaseCustomLink.isEligible else { return }

// Token type depends on your region and flow. Apple defines several,
// including in-app alternative payment and link-out variants.
guard let token = try await ExternalPurchaseCustomLink.token(for: "LINK_OUT") else { return }

// Send token.value to your own backend and store it before continuing.
await sendTokenToYourServer(token.value)

// Required. Must follow a deliberate action such as a button tap.
// Use .browser when leaving the app, .withinApp for a web view or native flow inside it.
let notice = try await ExternalPurchaseCustomLink.showNotice(type: .browser)
guard case .continued = notice else { return }

// ...then start your Xsolla purchase or link-out.

The token's value is a Base64URL-encoded JSON string. Decode it to read externalPurchaseId, which is the field Apple's report is keyed on. Send that to your backend and keep it.

Which token types apply, when to request them, and how long they stay valid all vary by region. See Apple's Receiving and decoding external purchase tokens ↗ for the current rules.

Disclosure sheet

In Japan, Brazil and the EU, Apple additionally requires an in-app disclosure sheet telling players they are transacting with you rather than with Apple, before you route them to an external purchase. It is system-provided, so showNotice is all you call.

Players can choose not to see it again, so branch on the result the call returns rather than on whether anything appeared. A silent call is not a failure.

Apple publishes the required wording and downloadable assets in the "In-app disclosure sheet" section of Payment options in Japan ↗, Payment options in Brazil ↗ and Payment options in the EU ↗.

Building the report from webhook data

Apple's report covers one token and the transactions attached to it. Most of what a line item needs is already in the order_paid payload, under billing.payment_details.

The mapping below is for one-time purchases, which is what Xsolla SDK sells. Apple reports subscriptions as a separate line item type with its own fields, including renewals and zero-charge events such as free-trial starts. If you sell subscriptions through a Web Shop link-out, don't reuse this mapping for them — see Apple's reporting guide ↗ for the subscription line item.

Apple line item fieldWhere it comes from
externalPurchaseIdDecoded from the Apple token you stored
lineItemIdYou generate it. Persist it, refunds reference it
creationDatebilling.transaction.payment_date, converted to epoch milliseconds
eventTypeorder_paid is a purchase, order_canceled is a refund
productTypeitems[].type, or the presence of billing.purchase.subscription
productIdentifieritems[].sku
quantityitems[].quantity
amountTaxInclusivebilling.payment_details.payment.amount, converted to minor units
taxAmountbilling.payment_details.vat.amount
amountTaxExclusiveTax-inclusive amount minus the tax amount
netAmountTaxExclusiveRunning net for the line item across purchases and refunds
reportingCurrencybilling.payment_details.payment.currency
pricingCurrencyorder.currency
taxCountryuser.country, converted to a three-letter code
referenceLineItemIdOn refunds, the lineItemId of the original purchase
Report what the player paid, not what you receive

billing.payment_details.payout is your share after Xsolla's fees. Apple's report expects the amount the player was charged, which is billing.payment_details.payment. Using payout under-reports your transactions and produces an incorrect commission calculation.

Tax fields

billing.payment_details.vat is the general consumption-tax field despite its name, not a EU-only one. It carries JCT in Japan and the local tax in Brazil just as it carries VAT in the EU, so it is the field to use for taxAmount in every market where Apple requires reporting.

user.country is the jurisdiction the tax was paid in, which is what Apple's taxCountry asks for. The only conversion needed is the format: user.country is a two-letter ISO 3166-1 alpha-2 code, while Apple expects the three-letter alpha-3 form, such as ES becoming ESP.

The fields above are the subset the Apple report needs. The payload carries considerably more — subscription and gift objects, promotions, coupons, fee breakdowns — and the same webhook drives your fulfillment. For the full structure, delivery guarantees and signature verification, see the webhooks documentation.

Amounts and rounding

Webhook amounts are decimal values, while Apple expects integer minor units. Convert with a decimal type rather than a float, and derive the tax-exclusive amount by subtracting the tax you reported rather than recomputing it from a percentage, so the three figures always reconcile.

Refunds

order_canceled covers both refunds and chargebacks. Report both as refund line items, referencing the lineItemId of the original purchase. Deduplicate on order.id, since webhooks are retried and a repeated delivery must not produce a second refund line item.

What Xsolla can't tell you

Some of Apple's requirements sit outside anything Xsolla observes. Your backend has to cover them:

  • Tokens that produced no purchase. Apple expects these reported too. There's no order and therefore no webhook, so your token store is the only record.
  • Tokens Apple knows about that you don't. Apple can notify you of a token your system has never seen, which has its own reporting status.
  • Line item identity. lineItemId values are yours to generate and persist. Without them, refunds can't reference the purchase they reverse.
  • Running totals. The net amount on a line item accumulates across purchases and refunds, so it can't be derived from a single webhook in isolation.

Testing

The External Purchase APIs return environment-specific tokens. A token created in the sandbox has an externalPurchaseId beginning with SANDBOX, so your backend can route it away from production reporting. Xsolla test orders are marked separately, with order.mode: "sandbox" on the webhook.

Further reading