Skip to content

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

Restore Apple Store Purchases

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

Grant a Profile the access its App Store subscription carries. Use it behind a "Restore Purchases" button, after a reinstall, or when the user moves to a new device. It takes a StoreKit 2 original transaction ID, not an app receipt.

This route accepts $BOTSI_SECRET_KEY or $BOTSI_PUBLIC_KEY in the Authorization header, with no Bearer prefix. The sample sends the secret key. From inside the app, send the public key. See Authentication.

When to call it#

Call it only when StoreKit reports at least one transaction in Transaction.currentEntitlements. If StoreKit reports nothing, there's nothing to restore, so read the Profile with Get a Profile instead.

Request body#

ParameterTypeRequiredDescription
profileIdstringRequiredBotsi profile ID of the user restoring
originalTransactionIdstringRequiredTransaction.originalID of the newest entry in Transaction.currentEntitlements

Send the original transaction ID, not the transaction's own id. Apple matches the subscription on the original ID.

There's no environment field. Botsi looks the transaction up in production first, then in sandbox.

# Authorization: $BOTSI_SECRET_KEY or $BOTSI_PUBLIC_KEY
curl -X POST "https://api.botsi.com/v2/purchases/apple-store/restore" \
  -H "Authorization: $BOTSI_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profileId": "fff3c120-0ca8-480f-9b19-3955a5428e39",
    "originalTransactionId": "2000000261699248"
  }'

What a restore does#

Botsi asks Apple for the subscription behind the transaction, then handles one of four cases:

CaseWhat happens
The Profile already owns the purchase. For example, the user tapped Restore on the device that bought itNothing changes, and the Profile is returned as it is. Calling repeatedly is safe
A different Botsi Profile owns the purchase. For example, after a reinstall or on a second deviceThe restoring Profile receives that Profile's Access Level, subscription state, and custom attributes, and the two Profiles are linked. The original Profile keeps its access
Botsi has a record of the purchase, with no Profile attachedThe purchase history is attached to the restoring Profile
Botsi has never seen the purchase. For example, it was bought before your app integrated BotsiBotsi rebuilds its history from Apple and grants access

Response#

A successful restore returns 200 with the Profile after the restore. It's the same shape Validate Apple Store Purchase returns: the Profile object from Get a Profile, without custom.

{
  "ok": true,
  "data": {
    "profileId": "fff3c120-0ca8-480f-9b19-3955a5428e39",
    "appUserId": "user-123",
    "state": "subscribed",
    "totalRevenueUsd": 49.99,
    "accessLevels": {
      "premium": {
        "isActive": true,
        "store": "app_store",
        "sourceProductId": "premium_annual",
        "expiresAt": "2027-10-07",
        "willRenew": true,
        "isLifetime": false
      }
    },
    "subscriptions": {
      "premium_annual": {
        "isActive": true,
        "store": "app_store",
        "sourceProductId": "premium_annual",
        "expiresAt": "2027-10-07",
        "willRenew": true,
        "isLifetime": false
      }
    },
    "nonSubscriptions": {}
  }
}

The entitlements above are trimmed to a few fields. The response carries every field listed under Entitlement fields.

Errors#

StatuscodeCause
400bad_requestprofileId or originalTransactionId is missing or empty. Also returned when your app's App Store connection isn't set up, with the message No ios settings found
401unauthorizedThe Authorization header is missing or holds an unknown key
404not_foundNo Profile with that ID belongs to your app. A Profile owned by another app returns the same response
404nothing_to_restoreApple has no subscription for that transaction

For example, a transaction Apple has no subscription for returns:

{
  "error": "Apple has no subscription for originalTransactionId: 2000000261699248",
  "code": "nothing_to_restore"
}