High-Level In-app Payment Service
Many apps require payment to unlock features or content1. But the APIs I’ve used, like Apple’s, are too low-level. Using it required a person-quarter of effort for Futurecam. Multiply that by the millions of apps and it’s a massive waste of resources. A high-level service would have saved this time. Using Apple’s APIs felt like assembly language, and I want to use Python instead.
Further, a high-level service would also abstract out the underlying APIs, be it Apple, Android or Stripe (for apps outside the Apple and Google app stores). So your payment code is cross-platform.
This is how the proposed high-level in-app payment service would work:
First, in iOS, if you have multiple payment options for the same plan, like a monthly and an annual, iOS does not know that the same set of features should be unlocked for both these plans. It treats them as unrelated, so you have to write code to check for it yourself. If you have ten features, and two plans, that’s 20 cases you have to cover.
To fix this, the high-level payment service I’m proposing would have two concepts: a plan, which is a set of features, which has one more payment plans, each of which has a price and a frequency of payment like monthly, yearly or one-time. So if you have a Standard and a Premium plan, you could offer a choice of monthly or annual payment plans for each of them. The API would understand that these payment plans unlock the same features.
Second, if you have a Standard plan and a Premium plan, the iOS payments API doesn’t know that the Premium plan is supposed to be a superset. Each time you check if a feature should be enabled, you should check it for all the plans. For example if a feature is offered in both plans, when the user tries to use it, you should check is_plan_active(standard) || is_plan_active(premium). This is again a combinatorial explosion: if you have ten features, and two plans, you have to cover 20 cases.
To fix this, the high-level payments service would let you explicitly declare one plan to be a superset of another. Then, when you check if the user is in the Standard plan, the API would also return true if the user is in the Premium plan. You don’t need to check for both yourself. And if you introduce an Enterprise plan tomorrow, you shouldn’t need to hunt for all the places in the code to change.
Third, if you want to remove some features from a plan, low-level APIs like iOS require you to put in extra effort to grandfather existing users.
The high-level service would take care of it via supersets: if you have a Standard plan that has three features A, B and C, and you want to remove C from the Standard plan while grandfathering existing users, you just define a new plan Standard 2.0, make the Standard plan a superset of Standard 2.0, and offer Standard 2.0 to new users instead of Standard. You modify the code for features A and B to check for Standard 2.0, while keeping the check for C the same. In this case, Standard 2.0 is an internal name at the API layer; the UI name can still be “Standard”. You won’t have to look for all code sites, add date checks, and other code that seems simple on the surface, but becomes messy and buggy when you get to it.
Fourth is automatic price optimisation. Low-level payment systems like iOS require you to manually run pricing experiments to figure out what price maximises revenue. Companies leave thousands of dollars on the table every month because they don’t have time for this major effort. The high-level payment service can do that. You can set an initial price and let the service run experiments on your behalf. You can optionally set a minimum and/or a maximum price to tell the algorithm to play only within that range. For example, if you have an app that’s not free, but has a Lite and a Full plan, and you want the Lite plan to be affordable even if it means leaving money on the table, you can limit the price of the Lite plan. The algorithm will also take supersets into account, and always price the Full plan above Lite.
The algorithm will start off by running coarse experiments, like starting with $5, getting 20 sales, then doubling the price, then getting 20 sales. If the revenue went up by 70%, it can be enough data to give you a conclusion on the direction, such as that you’ll increase revenue by charging more. Perhaps not statistically significant to pinpoint the exact price, or to conclude the experiment, but enough to give a direction for future experiments, specifically that you should focus on prices above $5 rather than below. Then, the algorithm can try doubling the price again to $20 and wait for 20 more sales. This time, if the revenue falls off a cliff, we know that the optimal price is between $5 and $20. Then, to fine-tune the price, we may need a lot more data points than 20. In this way, the algorithm can make rapid progress without waiting for months. And if you’re operating at scale, all the pricing experiments might conclude overnight.
Doing this manually is a huge pain. When I did it, I made mistakes recording the data correctly. I made the mistake of offering the product to 20 people and drawing a conclusion based on it rather than requiring 20 sales. Most of us don’t know statistics, and we shouldn’t have to. An algorithm can also use more sophisticated statistics than we can manually, to answer questions like “If A1 people paid out of B1 people who were made an offer at price P1, and A2 people paid out of B2 people who were made an offer at price P2, and A3 people paid out of C3 people who were made an offer at price P3, where P1 < P2 < P3, what’s the probability distribution of the optimal price?”
Further, the service can explore different factors to optimise revenue. Will charging Indians half of what we charge Americans increase revenue? Most products are priced the same across countries, which doesn’t make sense when you consider different incomes, and that a dollar goes much further in India than the US.
How about offering a discount to villagers? People using older phones? People on 3G or 2G? There are a lot of factors to explore to optimise revenue.
The algorithm can also explore one-time payments vs subscriptions, and monthly vs annual subscriptions. Basically you’ll be able to tell the high-level payment service “I want to make money” and it figures out the rest.
Fifth, the iOS in-app purchase API is too low-level, and requires you to jump through hoops like fetching products, refreshing your app receipt, restoring purchases… The high-level purchase service will, as its name says, offer a single high-level call ensure_plan(plan_name, callback). This will ensure that the user has subscribed to the given plan. Or a superset. It will prompt the user to purchase, if needed, including asking for a card 2. Or start a free trial, prompting the user to log in if needed. The library will come with UI components for purchase and login, so you don’t have to build your own. Unless you want to, in which case you’ll be able to plug in your components instead of the default ones. Whether you use the inbuilt components or yours, they’re wired together by the service, so the log in screen can appear followed by the purchase screen.
Sixth, the high-level service will be customisable. For example, in a mobile app, say a document scanner app, you’ll be able to allow users to start a free trial without having to log in, for reduced friction. Some people will uninstall the app if they have to log in to scan a document. But having a free trial without a login allows people to extend their free trial indefinitely by reinstalling the app. So it’s a tradeoff. You as the developer can choose whichever option is right for you. And in a declarative manner, by setting a property allow_trail_without_login = true in a config file.
Similarly, if you want to offer a 60-day no-questions-asked money back guarantee if a user isn’t happy with the app, you should be able to set a property money_back_duration = 60.
As a third example, if you want to have a card on file to begin a free trial, you should be able to set a property require_card_for_trial = true.
As a fourth example, you can customise how a free trial works. If you’re building a document scanner app, the trial might be 10 documents scanned, rather than a certain number of days. Or a video player app might allow 2 hours of playback before the trial ends. Whatever your criteria are, you’ll be able to track it, and make an API call free_trial_exhausted() when the user has exceeded these limits.
Seventh, the high-level service will come with a web UI to be used by team members to track business health, do analytics, issue refunds, etc.
In summary, there’s a lot of scope for a high-level payment service to make developers’ lives easier.
In this blog post, I’m not talking about payment for real-world services like food delivery; only for app features.
Except when operating on top of iOS or Android.