Validate Apple Store Purchase
POST https://api.botsi.com/v2/purchases/apple-store/validateCall 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#
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
paywallSessionIdis sent,paywallId,placementId,abTestId,aiPricingModelId, andisExperimentall 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_shownevent, which expires after 24 hours. profileIdstill 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": {}
}
}