Skip to content

Search the documentation by title, section, or page text.

Validate Apple Store Purchase

POST https://api.botsi.com/v2/purchases/apple-store/validate

Call this after a purchase completes in your app. It confirms the transaction with Apple and records the revenue against the Profile and the Paywall that produced it.

Identify the user with profileId.

Request body#

ParameterTypeRequiredDescription
profileIdstringRequiredBotsi profile ID from Create a Profile
productIdstringRequiredProduct identifier from the payment object. Must match the Product configured in Botsi
transactionIdstringRequiredTransaction identifier from the StoreKit payment object
originalTransactionIdstringRequiredOriginal transaction identifier from the StoreKit payment object
paywallSessionIdstringOptionalThe paywallSessionId from the Fetch Paywall response. When sent, the purchase is attributed from the token. See below
placementIdstringOptionalThe Placement used to fetch the Paywall
paywallIdnumberOptionalThe internal Paywall ID, from data.id on the Fetch Paywall response
abTestIdnumberOptionalThe A/B test that produced the Paywall
isExperimentbooleanOptionalThe value Fetch Paywall returned
aiPricingModelIdnumberOptionalThe model ID Fetch Paywall returned
environmentstringOptionalproduction for live apps, sandbox for testing
isSubscriptionbooleanOptionalWhether the purchase is a subscription
sourcestringOptionalrestore, purchasing, or observing. Defaults to purchasing

isExperiment and aiPricingModelId are optional on the v2 API, though the SDK surface requires them. Omitting them still validates the purchase, but read Attributing the purchase to a Paywall before you do.

source is load-bearing, even though it is optional

It defaults to purchasing when omitted. restore short-circuits validation entirely, and purchasing selects the deduplication key, so the value you send changes what the endpoint does rather than just labelling the call. Send it deliberately.

To give a returning user back the access their subscription carries, use Restore Apple Store Purchases.

curl -X POST "https://api.botsi.com/v2/purchases/apple-store/validate" \
  -H "Authorization: $BOTSI_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "placementId": "ai-placement-id",
    "paywallId": 42,
    "source": "purchasing",
    "productId": "premium_annual",
    "isSubscription": true,
    "originalTransactionId": "2000000261699248",
    "environment": "sandbox",
    "transactionId": "2000000871590381",
    "profileId": "fff3c120-0ca8-480f-9b19-3955a5428e39",
    "isExperiment": true,
    "aiPricingModelId": 32
  }'

Attributing the purchase to a Paywall#

Send the paywallSessionId from the Fetch Paywall response. Botsi reads the Paywall, Placement, A/B test, and AI Pricing Model from the token, and credits the revenue to the experiment that produced it. This is the preferred way to attribute a purchase.

curl -X POST "https://api.botsi.com/v2/purchases/apple-store/validate" \
  -H "Authorization: $BOTSI_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profileId": "fff3c120-0ca8-480f-9b19-3955a5428e39",
    "productId": "premium_annual",
    "transactionId": "2000000871590381",
    "originalTransactionId": "2000000261699248",
    "paywallSessionId": "v1.eyJhIjo0MDIxLCJ3Ijo5MDUsInAiOiJvbmJvYXJkaW5nIiwiaSI6MTc4NTMxMjAwMH0.QmzR1w",
    "source": "purchasing",
    "isSubscription": true,
    "environment": "sandbox"
  }'
  • The token wins. When paywallSessionId is sent, paywallId, placementId, abTestId, aiPricingModelId, and isExperiment all come from the token. Values you send for those fields are ignored, not merged.
  • Fields the token doesn't carry are cleared. They aren't filled in from the request.
  • There's no age limit. You can validate a purchase long after the Paywall was fetched, unlike a paywall_shown event, which expires after 24 hours.
  • profileId still identifies the buyer. The token attributes the sale, but it doesn't choose the Profile.

An invalid token fails the whole request

A paywallSessionId that isn't valid returns 400. Botsi never falls back to the explicit attribution fields, so the purchase isn't validated until you send a valid token or drop it.

Without a token, the explicit fields work exactly as before. Pass paywallId, isExperiment, and aiPricingModelId as Fetch Paywall returned them. Omit both the token and the fields and the purchase still validates, but it detaches from the experiment that produced it, so the model gets no credit for the revenue it earned.

Finding the identifiers#

StoreKit. transactionId and originalTransactionId come from the SKPaymentTransaction object. The Product identifier is SKPaymentTransaction.payment.productIdentifier, and the original price is on SKProduct.

StoreKit 2. All three come from the Transaction object, and the original price from Product.

Response#

A successful validation returns the updated Profile, including its Access Levels, subscriptions, and total revenue. It's the Profile object described on Get a Profile, without custom.

{
  "ok": true,
  "data": {
    "profileId": "fff3c120-0ca8-480f-9b19-3955a5428e39",
    "appUserId": "user-123",
    "state": "subscribed",
    "totalRevenueUsd": 49.99,
    "accessLevels": {},
    "subscriptions": {},
    "nonSubscriptions": {}
  }
}