6.2 - Custom Events
Purchasely SDK 6.2.0 is a minor release that lets your app send its own business events to Purchasely, open a campaign when one fires, and measure them as conversion KPIs.
This version also links every purchase to the paywall that started it, and lets audiences see every active and expired subscription of the user. There are no breaking API changes. Read the Behavior Changes section if your server reads appAccountToken.
Highlights
- Custom Events, sent from your app or from a Screen
- Campaigns triggered by a Custom Event
- Purchases attributed to the paywall that started them
- Audiences that see every active and expired subscription
Version per platform
Detailed changelogs are available on each platform's GitHub repository:
🚀 Features
📣 Custom Events
Send your own business events to Purchasely:
Purchasely.emit(name: "recipe_viewed", properties: ["recipe_id": 42, "title": "Ratatouille"])
Purchasely.emit(name: "checkout_started")Purchasely.emit("recipe_viewed", mapOf("recipe_id" to 42, "title" to "Ratatouille"))
Purchasely.emit("checkout_started")Declare each event in the Console, in Targeting > Events. The SDK sends only the declared event names. It compares names exactly, case and spaces included, so "Recipe Viewed" and "recipe_viewed" are two different events. The SDK ignores an event that is not declared.
Four rules are worth knowing:
- Properties travel as given. Dates are sent as ISO 8601 strings. The SDK drops a value that it cannot send, such as
NaN, logs a warning, and sends the rest of the event. - Your user attributes come along. Each event carries your user attributes and the built-in attributes as they were at the moment of the call.
- Consent is respected. When the user refuses the
analyticspurpose, the SDK sends no new Custom Events. - Separate from the SDK events. Custom Events have their own queue and their own retries. They never reach your
PLYEventDelegateor your event listener.
You can call emit before start().
New page: Custom Events
🎯 Trigger a Campaign with a Custom Event
Link a campaign to one of your Custom Events in the Console. When the event fires, the SDK opens the campaign Screen or Flow. The rules of the APP_STARTED trigger apply: dates, capping, exposure, and the campaigns consent purpose. A Custom Event never opens the campaigns of a Purchasely event that has the same name.
Reference: Campaign configuration
👆 Track Event Screen Action
The new Track event action of the Screen Composer sends one of your Custom Events from a Screen. The event carries the context of the Screen: presentation, placement, audience, A/B test and variant, campaign, and flow and step. Use it to measure a success KPI on a paywall.
Your app cannot intercept this action, and the action never blocks the actions next to it. A button that tracks an event and then purchases still closes the paywall after the purchase.
Reference: Action types
🧾 Purchases Attributed to the Paywall That Started Them
Every purchase is now linked to the paywall, placement, campaign and A/B test that started it. On iOS, this holds when the same product is bought twice, for Ask to Buy, and when the purchase completes after the app relaunches. Conversion reporting is more accurate as a result.
iOS, Observer mode. signPromotionalOffer has a new variant that gives a purchase context token with the signature. The previous variants are deprecated, but continue to work.
Purchasely.signPromotionalOffer(storeProductId: productId,
storeOfferId: offerId,
purchaseContextToken: nil,
success: { signature, token in
// StoreKit 2: pass `token` as .appAccountToken(token)
// StoreKit 1: set applicationUsername = token.uuidString.lowercased()
}, failure: { error in })Reference: Implementing promotional offers
👥 Audiences See Every Subscription
Audience targeting now sees the user's current active and expired subscriptions on every request. This includes subscriptions bought on the web (Stripe, web-to-app). The SDK sends them in the new built-in attributes ply_active_subscriptions and ply_expired_subscriptions.
On iOS, ply_custom_events_tracked counts the events sent for each name, and ply_custom_events_last_tracked holds the time of the last one.
⚠️ Behavior Changes
- iOS:
appAccountTokenandapplicationUsernamenow carry a purchase context token. Before, the SDK put the anonymous user id there. It now puts a new random UUID for each purchase. If your server readsappAccountTokenin App Store Server Notifications to identify the user, use your own user mapping or Purchasely webhooks instead. - Subscriptions whose plan is not in your app catalog are now returned, for example a web subscription. Their plan and product can have an empty
vendorId, so handle that case. - Android, source compatibility:
PLYEventhas a new subclass. If your code useswhen (event)onPLYEventwithout anelsebranch, add anelsebranch. - Android, source compatibility:
StoreType.WEB_CHECKOUT_STRIPEis nowStoreType.STRIPE, andPLYPurchaseResponse.expiredSubscriptionsis now nullable.
🍎 iOS
✨ Features & Improvements
Purchasely.emit(name:properties:)to send Custom Events- Campaigns triggered by a Custom Event
track_eventScreen action- Purchases attributed to the paywall, placement, campaign and A/B test that started them
signPromotionalOffer(storeProductId:storeOfferId:purchaseContextToken:success:failure:)for Observer mode. The previous variants are deprecated.ply_active_subscriptions,ply_expired_subscriptions,ply_custom_events_trackedandply_custom_events_last_trackedbuilt-in attributes- New consent purpose
PLYDataProcessingPurpose.refundHandling, to record that the user refused to share consumption data with Apple for refund requests. It is not part of.allNonEssentials.
⚠️ Action Required
- If your server reads
appAccountTokento identify the user, use your own user mapping or Purchasely webhooks. revokeDataProcessingConsent(for:)replaces the whole list each time you call it. Pass every refused purpose in the same call.
🐛 Fixes & Reliability
- Taps inside a tappable container run only the inner action. Before the fix, a button or a plan picker inside a tappable block could also run the block's action. Dragging a scroll view no longer triggers an action.
- Stack backgrounds and borders display on iOS 27.
- Bold and italic text keeps its size when a custom font has no bold or italic face.
- Labels such as
1. Premieror- 50%no longer show a\on screen. When a label has two links, each keeps its own URL. synchronize()no longer sends the same receipts again on every call. This mostly affected non-renewing subscriptions.clearBuiltInAttributes()also clears the cached subscriptions used for audience targeting.- Concurrent starts no longer create a duplicate session, so some apps will see fewer
APP_STARTEDevents.
🤖 Android
✨ Features & Improvements
Purchasely.emit(name, properties)to send Custom Events. Supported property types:Int,Long,Float,Double,Boolean,String,Dateand a list ofString.- Campaigns triggered by a Custom Event
- "Track event" Screen action
ply_active_subscriptionsandply_expired_subscriptionsbuilt-in attributesPLYPlan.dump()returns a full text report of a plan and of its Google Play product. Attach it to a support request.- The SDK starts faster when the user has a long subscription history.
- Media3 update to 1.11.1 in the
playermodule.
⚠️ Action Required
- Add an
elsebranch to awhen (event)onPLYEvent. - Replace
StoreType.WEB_CHECKOUT_STRIPEwithStoreType.STRIPE. - Handle a
nullvalue ofPLYPurchaseResponse.expiredSubscriptions.
🐛 Fixes & Reliability
- A paywall fetch without an API key now fails with
PLYError.Configurationand sends no request. - A Google Play network error during a purchase ends the purchase without an alert. Your
onErrorcallback receives the error. - The callbacks of
purchase(),restoreAllProducts()andsilentRestoreAllProducts()answer only the call that started them. - A paywall no longer freezes after two purchase attempts that end with the same result.
- A paywall purchase button no longer stays blocked after an error that shows no alert.
- A login transfers the subscriptions that the user bought or redeemed while anonymous, and tries a failed transfer again at the next login.
- An inline paywall (
buildView) no longer sends aCloseaction when its host screen goes away. - On Android TV, the paywall works with the remote control (D-pad).
Purchasely.closeAllScreens()closes all stacked screens.- A flow sheet or modal no longer crashes or closes when a late dismiss or navigation request arrives.
- The SDK no longer reports "In-app purchase failed" events when no purchase is in progress.
- A deeplink with an upper-case
PLYhost now opens. - A property with a
NaNor infinite number inside a list or a map no longer causes the loss of the full request.
Full list of the fixes: Purchasely-Android 6.2.0

