Side Project: Promo Codes (Part 1 – Intro and Promo Code Creation)

For my recent sponsorship of AppStories’ 99th episodewith Yoink for Mac, iPad and iPhone, I also offered a couple of promo codes to the lucky first people who got to them.
(Developers can create create promo codes for their app in the Mac- and iOS App Store, to give away to others so they can download their app for free. This way, members of the press can try out your app for free, or you can hold a give-away of your app).

Some users find promo code redemption a little clumsy – and I agree. You have to copy the promo code, navigate to the iOS App Store (or Mac App Store, or iTunes on Mac for iOS apps), paste the code and redeem.

I thought it would be nice to have a “landing page” for each promo code, where the user would be able to click a link and be taken to the correct redemption page with the code already filled in, or at least explain how to do it manually.
It also occurred to me it would be neat to track promo codes – like their redemption status and expiration dates.

Born was the idea for ‘Promo Codes’.

In detail, I wanted the following:

  • Have a Mac-, iPhone- and iPad app, well integrated into their respective OS
  • Be able to create promo codes without having to go to App Store Connect
  • Store those (and manually created) promo codes in a web database so I can access them from anywhere using the apps (basically, sync)
  • Track them
    – when were they created
    – what were they created for (or for whom)
    – when would they expire
    – whether they were redeemed or still unused
    – the popularity of a code (view count, last access date)
  • Have a landing page for users to make redemption easier

Of course, the whole thing would hang on my ability to be able to programmatically request promo codes from App Store Connect, but first, I wanted to check out the alternatives.

The Alternatives

“Alternatives” is saying too much, as there’s not a huge market around this, apparently. It’s more like “Alternative”, because I really found just one good solution:

Alternative 1 – Tokens.app

Tokens is an awesome app that lets you do exactly what I detailed above (apart from an iOS app), and – although I haven’t used it personally – I can only recommend it, as I hear only good things, and it’s being recommend all over the place by developers.
If you’re looking for a trusted, tested way of managing your promo codes – go try it out, they do have a demo!

Alternative 2 – No tracking at all (duh)

The other alternative was to forget about it and keep doing what I have been doing regarding promo codes – nothing.
After all, I’ve been doing just fine not worrying about them at all ever since I became an indie developer.

But what fun would that have been?

My Own Solution

I almost purchased Tokens, but for some reason saw a personal challenge in this, to see if I could pull off creating my own service.
I wasn’t worried about the Mac/iOS side of the project. Rather, I was apprehensive about my server-side “skills” – of which I have none.
I “know” a little PHP, and that just from piecing together code from tutorials and StackOverflowquestions, as well as the occasional, small service script here and there (for example, to handle newsletter sign-up from my apps).
Another big question mark was whether I could manage to programmatically create promo codes using App Store Connect – there wouldn’t have been much of a point in the apps if I wasn’t able to do that.

Having a clear vision of what I wanted to achieve, there would be four parts to this project:

  1. A PHP web service with a database (MySQL) for storing all necessary information:
    I wanted a JSON-based REST API that would allow the following:
    – Storing promo codes in a database (with all necessary metadata like dates, campaign titles and view counts)
    – Deleting promo codes
    – Updating specified promo codes
    – Retrieving all promo codes
    – APNs Push to my apps when codes are added or deleted
  2. A user-facing PHP script that would act as the landing page for the promo code they’re about to redeem:
    – Shows the promo code, the app the promo code is for and whether the code is still available
    – Optionally, if the promo code has already been claimed, offer another one from the same pool of that campaign
    – Allow for easy redemption by just clicking a link
  3. A Mac app that:
    – shows me all promo codes and their metadata
    – can request promo codes for any of my apps via App Store Connect
    – can add manually created promo codes
    – can delete promo codes
    – has deep system integration / services for convenience
    – reminds me about expiring promo codes via notifications
  4. An iPad and iPhone app that:
    – does the same as the Mac app

And with that, I started looking into promo code creation.

Programmatically Requesting Promo Codes

The credit regarding the REST APIs used here goes entirely to fastlane (github).
Those people are doing amazing work and without their open source code, I wouldn’t have been able to do any of this – my code was pieced together from theirs.

Requesting promo codes consists of these 7 steps:

  1. Request a “service key” to communicate with App Store Connect
  2. Log in to App Store Connect using the account’s username and password
  3. Retrieve the account’s session data
  4. Selecting a team
  5. Selecting an app for which to request promo codes and retrieving the promo code info (how many codes left?)
  6. Request a certain amount of promo codes
  7. Load the app’s promo code history to retrieve the newly created codes

1) Request a “service key”

We’re off to an easy start:
Call GET https://olympus.itunes.apple.com/v1/app/config?hostname=itunesconnect.apple.com
and get ‘authServiceKey’ from the JSON this returns – done.

2) Log in to App Store Connect

Custom UI for logging in to App Store Connect

This is where it gets a bit more complicated.
First, call POST ‘https://idmsa.apple.com/appleauth/auth/signin’ with this JSON body:

{
“accountName” : <username, apple id>,
“password” : <password>,
“rememberMe” : true
}

and these headers:

Content-Type: application/json
X-Requested-With: XMLHttpRequest
X-Apple-Widget-Key: <the “service key” you retrieved in step 1>
Accept: application/json, text/javascript
Connection: keep-alive

If you receive an HTTP Status of 200 in the response, you’re free to go to the next step (Retrieve the account’s session data) – the user either doesn’t have two-factor authorization set up, or the cookies are still valid from a previous session.
Chances are, though, that you won’t get 200, especially now that Apple is enforcing two-factor authentication.

So you’re more likely to receive an HTTP Status of 409.
– Remember the response’s “x-apple-id-session-id” and “scnt” header values, you’ll need them for the next few calls.
– Call GET ‘https://idmsa.apple.com/appleauth/auth‘ with these headers:

X-Apple-Id-Session-Id: <the according value you just saved>
X-Apple-Widget-Key: <the “service key” you got in step 1>
Accept: application/json
scnt: <the according value you just saved>

This will give us all the necessary information for proceeding – like the registered phone numbers (obfuscated, like you would see on the website, with ‘••••’ instead of digits).
Depending on the information we receive here, we can also figure out if a code has just been sent to the user’s devices (or to a phone number), or if we need to request one.

The user will have received a code at this point if:
– The returned dictionary contains a key “noTrustedDevices” with a boolean value of NO, or does not contain this key at all or
– The returned dictionary only contains one trusted phone number

Otherwise, the user hasn’t set up any devices to be trusted with their login credentials or the account has more than one phone number which the user has to pick from for verification.
In the latter case, we need to present the user with their obfuscated phone numbers, let them pick one and request a code via SMS for that number.
One phone number is represented in the response like this:

{
id = 1,
numberWithDialCode = “+1 •••• ••••••00”,
obfuscatedNumber = “•••• •••••00”,
pushMode = sms
}

The id is what we need for requesting a code via sms:
We call PUT ‘https://idmsa.apple.com/appleauth/verify/phone‘ with the following headers:

X-Apple-Id-Session-Id : <like before>
scnt : <like before>
X-Apple-Widget-Key : <like before>
Accept : application/json
Content-Type : application/json

and a JSON body like this:

{
phoneNumber : {id : <the id of the phone number the user picked>},
mode : sms
}

which, if everything goes well, will return an empty response and send a code via sms to the user’s chosen phone number.

Now that the user has a code, we can show the UI for entering the confirmation code, which we then again send back to Apple:
Depending on whether the code was sent to a trusted device, or by SMS, the URL to call and the JSON body to send are a bit different.
For a “trusted device code”, the URL is POST ‘https://idmsa.apple.com/appleauth/auth/verify/trusteddevice/securitycode‘, with this JSON body:

{
securityCode: {code : <the code the user received and then entered>}
}

For a code received by SMS, the URL is POST ‘https://idmsa.apple.com/appleauth/auth/verify/phone/securitycode‘, and this JSON body:

{
securityCode : {code : <the code the user received and then entered> },
phoneNumber : {id : <the id of the selected phone number>},
mode : sms
}

Call either of those with the usual headers and, if everything goes well, you’ll receive an empty response – otherwise, errors are returned (if the supplied code was wrong, for example).

3) Retrieve the account’s session data

Now that we’re logged in, we can continue with our actual objective – requesting promo codes.
First, we need to know what we’re dealing with – mainly, the teams the user is part of.
We do that by calling GET ‘https://olympus.itunes.apple.com/v1/session‘ with the headers

X-Requested-With : XMLHttpRequest
X-Apple-Widget-Key : <the auth key>
Accept : application/json, text/javascript

which will give us a JSON response and everything we need to know – the current user info (“user”), the currently signed-in team (“provider”) and all available teams (“availableProviders”).
In my implementation, I follow that call with another one to retrieve the currently selected team’s apps right away before updating the UI, but for the sake of this post, I’ll explain that in the following points.

4) Selecting a team

Custom UI for selecting a team and an app of that team

As the user might be part of several teams, we need to be able to change the currently selected team so we can retrieve its apps for promo code creation.
This is done by calling POST ‘https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/v1/session/webSession‘ with the usual headers and this JSON body:

{
contentProviderId : <the selected team id>,
dsId : <the logged in user’s personID>
}

This will update your session’s cookies so that the selected team is the active one.

5) Selecting an app and retrieving its promo code info

Custom UI with promo code info loaded for an app

Let’s retrieve the apps for the currently selected team.
It’s a simple GET call (with the usual headers) to ‘https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/apps/manageyourapps/summary/v2‘ and it’ll return a JSON with all the infos we need about the apps. Depending on the UI implementation, we’re particularly interested in an app’s id (“adamId”), its name (“name”), its bundle ID (“bundleId”), vendor ID / sku (“vendorId”) and the platform it’s running on (“platformString”).
We can now present the apps to the user and let them select one.

With a selected app, we can retrieve its promo code info – front and foremost, if there are any promo codes left to be requested for the current version.
We call GET ‘https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/apps/<adamIdOfTheSelectedApp>/promocodes/versions‘ and receive a JSON where we can
– the version and versionID of the app and
– calculate how many codes are left

6) Request a certain amount of promo codes

Knowing everything we need to know, we can finally request the desired amount of promo codes for the app.
We call POST ‘https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/apps/<adamIdOfTheSelectedApp>/promocodes/versions/‘ with a JSON body like:

[
{
numberOfCodes : <amount of codes to request>,
agreedToContract : true
versionId : <the version id of the app retrieved earlier>
}
]

Careful, it’s an array this time, not a dictionary. Yup – that cost me a couple of hours.

It would be convenient if the response would contain the desired promo codes, but sadly, it doesn’t. All it contains is whether the creation was successful or not.
To actually get to the promo codes we need to

7) Load the app’s promo code history

The promo code history contains all promo codes created previously and just now, so we need to know when we started the request process so we can filter out codes we didn’t just create.
We call GET ‘https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/apps/<adamIdOfTheSelectedApp>/promocodes/history‘ and parse the resulting JSON.
Specifically, we’re interested in the code itself, its creation- and expiration dates, its platform and version.

Source Code

You can find the source code (in form of a sample project) over at GitHub.
Enjoy :)

Thoughts on App Review

As some of you may know, getting Yoink for iOS through Apple’s App Review was, to say it lightly, a bit of a pain.
In the end, I was able to release it, but a month late. Had this been my first app as an indie developer, there’s a good chance I would have had to declare bankruptcy now.
I am fortunate enough to have a couple of apps out already that create a steady income, but still, I spent about two months exclusively on this app, so it’s still scary thinking about how I got rejected over and over.

Long story short, here’s a couple of thoughts I’ve had during all of this.

TestFlight App Review

From the very beginning, I’ve had Yoink available to a couple of (awesome) testers via TestFlight.
Now, when you add a new app or a new version of the app to TestFlight, it has to go through a review before testers can download and test it.

My question, then, is: Why not reject the app right there if it doesn’t comply with the rules in the App Reviewer’s eyes?
If this is not checked, why have a review for TestFlight apps in the first place?

Or if that’s unrealistic for some reason, perhaps TestFlight App Review could give sort of a “likelihood of getting through the ‘real’ App Review”. Maybe on the levels of “yea, good luck with that” to “possibly, tentatively not going to be rejected”.

It would have saved me (and the App Review person) a *lot* of time and nerves had, for example, the File Provider extension been rejected right then and there for not being cloud-storage based. Or the keyboard, for not having a traditional method of input. Those are all things that could have been avoided, had TestFlight App Review caught these things.

Reasons for Rejection

Yoink was rejected for different reasons and in different areas of the app.
But those reasons were given to me one by one, one submission and “Waiting for Review” -> “In Review” cycle after another.
That’s *such* a waste of time (not only for the developer, but also for the reviewer. But *especially* for the developer).
Why not keep going after finding a reason for rejection and see if there are other issues after that? If so, the reviewer could note them all down and give them to the developer all at once, not one by one.

Notice of Escalation

When an App Reviewer isn’t sure about an app, the review is “escalated”, meaning it goes up one instance in the App Reviewer hierarchy to be reviewed by a “superior”.
That would be the perfect time to let the developer know in advance that, “look, review is going to take a little longer because we’ve run into an issue with your app. Please stay tuned, we’re working on it.”.
Not only would the developer know that it’s going to take longer for the app to be reviewed, they’d also have reassurance that the App Reviewer hasn’t forgotten about the app – anybody who had an app “In Review” for more than twelve hours knows that feeling ;)

In closing, I’d like to say that I have nothing but respect for App Reviewers. Their job is difficult and, mostly, unthankful.
But I believe a lot of grievances on both sides could be avoided if some of these suggestions were put in place.

Eternal Storms Software Logo

– – – Do you enjoy my blog and/or my software? – – –
Stay up-to-date on all things Eternal Storms Software and join my low-frequency newsletter (one mail a month at most).
Thank you :)

iTunes Affiliate Program – One Year In

Last year, I started using the iTunes Affiliate Program.
The program allows you to make a little money by linking to your apps with your affiliate token and lets you track where clicks and purchases come from, as well.
I thought it’d be interesting to do a little retrospective, now that a year has gone by.

Using Affiliate Links

Turning an (Mac) App Store link (or almost any other iTunes – based link) into an affiliate link couldn’t be simpler.
All you need to do is append your affiliate token (like &at=your_token).
Additionally, you can provide context with your links so you can track them more easily later on.
For instance, I “tag” my links with “twitter”, “facebook”, “blog”, “website”, “newsletter”, “yoinkdemo” and so on.
That makes it very easy to track where your customers come from.

I use affiliate links for every iTunes link I share – be it an app of my own or that of another developer.

Tools and Info

There are tools available to you to make affiliate linking even easier, like John Voorhees’ Blink (for iOS).
I personally don’t use any extra software – I have a note in Notes.app with all my affiliate links and then just change the aforementioned context string.

If you’re interested in an in-depth look at the iTunes Affiliate Program, I recommend you read John Voorhees’ excellent comprehensive guide about it on MacStories.
It’ll get you up to speed on how to set it all up.

Where do Clicks Come From?

A very interesting aspect of the iTunes Affiliate Program is its tracking capabilities.
When someone clicks your link, you know:

  • the country the user is in
  • when the link was clicked
  • the context string you provided for the link (like “website”, “blog”, etc)
Live-View of Clicks on Links

A click presented in the live-view of iTunes Affiliate Program’s dashboard. I can see the click came from the US, and from within Yoink‘s “Demo Expired” window.

Furthermore, it tracks what the user purchased – a Mac app, an iOS app, a song, a book, etc.

Top 5 Countries

Based on the number of clicks, the top 5 countries from where people click on my links are:

  1. United States
    22,498 clicks
    $254.15
  2. Brazil
    924 clicks
    $4.96
  3. Germany
    570 clicks
    $76.70
  4. United Kingdom
    91 clicks
    $46.10
  5. Spain
    50 clicks
    $19.17

Apparently, more clicks don’t automatically equal a higher payout.

Conversion Rates

The program allows you to see the conversion rates for your links.
Who really purchased after clicking my link? Here’s what I found out:

  1. 38.62% purchased an app after clicking a link on my website
  2. 25.70% purchased an app after clicking on the link from the “demo expired” window inside the app
    (I haven’t had the affiliate link in the “demo expired” window in Yoink for the entirety of the year, more like only half a year)
  3. 00.72% purchased something when they clicked on a link on my blog

I can’t be sure what the user purchased – once a user clicks a link with an affiliate token, that token is used for the next 24 hours for that user.
Which means that they might click on a link for Yoink, but don’t purchase it. Then, some hours later, they purchase a book on the iBook Store – that will count towards that link’s conversion rate.

A Year of iTunes Affiliate Program Links

Here’s the gist of my first year in the iTunes Affiliate Program:

  • Clicks: 24,647
  • Items Bought: 4,465
  • Revenue Generated: $ 9791.12
  • Payout: $ 685.38
  • Average Conversion Rate: 18.12%

Granted, $685.38 isn’t a lot, but it’s money I wouldn’t have seen otherwise, and as an indie developer, every bit counts ;)

Eternal Storms Software Logo

– – – Do you enjoy my blog and/or my software? – – –
Stay up-to-date on all things Eternal Storms Software and join my low-frequency newsletter (one mail a month at most).
Thank you :)