Publishing private enterprise apps to the managed Google Play Store

Here’s a quick how to guide for publishing a private app to the managed Google Play store using the Custom App Publishing API and then surfacing it to your enterprise users through your EMM/MDM solution.

When you use this API your apps are;

  • permanently private, meaning they can't be made public
  • not subject to the public Google Play Store policies such as API target levels, application permission restrictions, etc
  • going through a streamlined verification process and appear in the managed Google Play Store in as little as five minutes, compared to over two hours via the Play Console
  • The only store listing details required to publish an app are its title and default listing language.

Before you can run the code below you need to set up a service account that has permissions to publish private apps. You can follow the guide here to set it up.

  1. Enable the Google Play Custom App Publishing API
  2. Create a service account
  3. Obtain private app publishing rights
  4. Retrieve the developer account id

At the end of this step you will have created a service account (eg myappname@turnkey-crowbar-123456.iam.gserviceaccount.com) and retreieved your developer account id (eg 1234567890123456789).

Next you need to generate the private key that will be used to authenticate against the API. To do this click on the Create Key button in the Service Account details page and save the json file to your local disk (keep this key very secure).

Create private key for service account

Next fire up your favourite code editor, reference one of the pre-built Google API libraries (or directly use the REST API) to publish the app.

Here’s how I did it with Visual Studio.

Start a new console project and add a nuget reference to Google.Apis.Playcustomapp.v1.

Update the devAccountId, apkPath and clientSecretsJson variables in the code below and hit run. If all goes well the response returned is ‘Complete’. If you receive a ‘Failed’ response take a look in the exception message for why it could have failed.

    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                new Program().Run().Wait();
            }
            catch (AggregateException ex)
            {
                foreach (var e in ex.InnerExceptions)
                {
                    Console.WriteLine("ERROR: " + e.Message);
                }
            }
            Console.WriteLine("Press any key to continue...");
            Console.ReadKey();
        }

        private async Task Run()
        {
            long devAccountId = 1234567890123456789;
            var apkPath = @"C:\apps\MyPrivateApp.apk";
            var clientSecretsJson = @"C:\secrets\apppublishkey.json";

            var appMetaDda = new CustomApp
            {
                Title = "My Private Company App",
                LanguageCode = "en_AU"
            };

            GoogleCredential credential;
            using (var stream = new FileStream(clientSecretsJson, FileMode.Open, FileAccess.Read))
            {
                credential = GoogleCredential.FromStream(stream)
                    .CreateScoped("https://www.googleapis.com/auth/androidpublisher");
            }
          
            var svc = new Google.Apis.Playcustomapp.v1.PlaycustomappService(new Google.Apis.Services.BaseClientService.Initializer { HttpClientInitializer = credential });
            var request = svc.Accounts.CustomApps.Create(appMetaDda, devAccountId, File.OpenRead(apkPath), "application/octet-stream");
            var response = request.Upload();
            Console.WriteLine("Status = " + response.Status);
        }
    }
Written on February 8, 2019