Python sample: app submission with game options and trailers

This article provides Python code examples that demonstrate how to use the Microsoft Store submission API for these tasks:

  • Obtain an Azure AD access token to use with the Microsoft Store submission API.
  • Create an app submission
  • Configure Store listing data for the app submission, including the gaming and trailers advanced listing options.
  • Upload the ZIP file containing the packages, listing images, and trailer files for the app submission.
  • Commit the app submission.

Create an app submission

This code calls other example classes and functions to use the Microsoft Store submission API to create and commit an app submission that contains game options and a trailer. To adapt this code for your own use:

import time

from devcenterclient import DevCenterClient, DevCenterAccessTokenClient
import submissiondatasamples as samples

# Add your tenant ID, client ID, and client secret here.
tenant = ""
client = ""
secret = ""
acc_token_client = DevCenterAccessTokenClient(tenant, client, secret)

acc_token = acc_token_client.get_access_token("https://manage.devcenter.microsoft.com")
dev_center = DevCenterClient("manage.devcenter.microsoft.com", acc_token)

# The application ID is taken from your app dashboard page's URI in Dev Center,
# e.g. https://developer.microsoft.com/en-us/dashboard/apps/{application_id}/
application_id = ""

# Get the application object, and cancel any in progress submissions.
is_ok, app = dev_center.get_application(application_id)
assert is_ok

if "pendingApplicationSubmission" in app:
    in_progress_submission_id = app["pendingApplicationSubmission"]["id"]
    is_ok = dev_center.cancel_in_progress_submission(application_id, in_progress_submission_id)
    assert is_ok

# Create a new submission, based on the last published submission.
is_ok, submission = dev_center.create_submission(application_id)
assert is_ok
submission_id = submission["id"]

# The following fields are required:
submission["applicationCategory"] = "Games_Fighting"
submission["listings"] = samples.get_listings_object()
submission["Pricing"] = samples.get_pricing_object()
submission["packages"] = [samples.get_package_object()]
submission["allowTargetFutureDeviceFamilies"] = samples.get_device_families_object()

# The app must have the hasAdvancedListingPermission set to True in order for gaming options
# and trailers to be applied. If that's not the case, you can still update the app and
# its submissions through the API, but gaming options and trailers won't be saved.
if not "hasAdvancedListingPermission" in app or not app["hasAdvancedListingPermission"]:
    print("This application does not support gaming options or trailers.")
else:
    submission["gamingOptions"] = [samples.get_gaming_options_object()]
    submission["trailers"] = [samples.get_trailer_object()]

# Continue updating the submission_json object with additional options as needed.
# After you've finished, call the Update API with the code below to save it:
is_ok, submission = dev_center.update_submission(application_id, submission_id, submission)
assert is_ok

# All images and packages should be located in a single ZIP file. In the submission JSON, 
# the file names for all objects requiring them (icons, packages, etc.) must exactly 
# match the file names from the ZIP file.
zip_file_path = ""
is_ok = dev_center.upload_zip_file_for_submission(application_id, submission_id, zip_file_path)
assert is_ok

# Committing the submission will start the submission process for it. Once committed,
# the submission can no longer be changed.
is_ok = dev_center.commit_submission(application_id, submission_id)
assert is_ok

# After committing, you can poll the commit API for the status of the submission's process using
# the following code.
waiting_for_commit_start = True
while waiting_for_commit_start:
    is_ok, submission_status = dev_center.get_submission_status(application_id, submission_id)
    assert is_ok
    waiting_for_commit_start = submission_status == "CommitStarted"
    if waiting_for_commit_start:
        time.sleep(60)

Obtain an Azure AD access token and invoke the submission API

The following example defines the following classes:

  • The DevCenterAccessTokenClient class defines a helper method that uses the your tenantId, clientId and clientSecret values to create an Azure AD access token to use with the Microsoft Store submission API.
  • The DevCenterClient class defines helper methods that invoke a variety of methods in the Microsoft Store submission API and upload the ZIP file containing the packages, listing images, and trailer files for the app submission.
import http.client
import json
import requests

class DevCenterAccessTokenClient(object):
    """A client for acquiring access tokens from AAD to use with the Dev Center Client."""
    def __init__(self, tenant_id, client_id, client_secret):
        self.tenant_id = tenant_id
        self.client_id = client_id
        self.client_secret = client_secret

    def get_access_token(self, resource):
        """Acquires an access token to the specific resource via the AAD tenant."""
        body_format = "grant_type=client_credentials&client_id={0}&client_secret={1}&resource={2}"
        body = body_format.format(self.client_id, self.client_secret, resource)
        access_headers = {"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"}
        token_conn = http.client.HTTPSConnection("login.microsoftonline.com")
        token_relative_path = "/{0}/oauth2/token".format(self.tenant_id)
        token_conn.request("POST", token_relative_path, body, headers=access_headers)

        token_response = token_conn.getresponse()
        token_json = json.loads(token_response.read().decode())
        token_conn.close()
        return token_json["access_token"]

class DevCenterClient(object):
    """A client for the Dev Center API."""
    def __init__(self, base_uri, access_token):
        self.base_uri = base_uri
        self.request_headers = {
            "Authorization": "Bearer " + access_token,
            "Content-type": "application/json",
            "User-Agent": "Python"
        }

    def get_application(self, application_id):
        """Returns the application as defined in Dev Center."""
        path = "/v1.0/my/applications/{0}".format(application_id)
        return self._get(path)

    def cancel_in_progress_submission(self, application_id, submission_id):
        """Cancels the in-progress submission."""
        path = "/v1.0/my/applications/{0}/submissions/{1}".format(application_id, submission_id)
        return self._delete(path)

    def create_submission(self, application_id):
        """Creates a new submission in Dev Center. This is identical to clicking
        the Create Submission button in Dev Center."""
        path = "/v1.0/my/applications/{0}/submissions".format(application_id)
        return self._post(path)

    def update_submission(self, application_id, submission_id, submission):
        """Updates the submission in Dev Center using the JSON provided."""
        path = "/v1.0/my/applications/{0}/submissions/{1}"
        path = path.format(application_id, submission_id)
        return self._put(path, submission)
    
    def get_submission(self, application_id, submission_id):
        """Gets the submission in Dev Center."""
        path = "/v1.0/my/applications/{0}/submissions/{1}"
        path = path.format(application_id, submission_id)
        return self._get(path)

    def commit_submission(self, application_id, submission_id):
        """Commits the submission to Dev Center. Once committed, Dev Center will
        begin processing the submission and verify package integrity and send
        it for certification."""
        path = "/v1.0/my/applications/{0}/submissions/{1}/commit"
        path = path.format(application_id, submission_id)
        return self._post(path)

    def get_submission_status(self, application_id, submission_id):
        """Returns the current state of the submission in Dev Center,
        such as is the submission in certification, committed, publishing,
        etc."""
        path = "/v1.0/my/applications/{0}/submissions/{1}/status"
        path = path.format(application_id, submission_id)
        response_ok, response_obj = self._get(path)
        if "status" in response_obj:
            return (response_ok, response_obj["status"])
        else:
            return (response_ok, "Unknown")

    def upload_zip_file_for_submission(self, application_id, submission_id, zip_file_path):
        """Uploads a ZIP file for the Submission API for the submission object."""
        is_ok, submission = self.get_submission(application_id, submission_id)
        if not is_ok:
            raise "Failed to get submission."

        zip_file = open(zip_file_path, 'rb')
        upload_uri = submission["fileUploadUrl"].replace("+", "%2B")
        upload_headers = {"x-ms-blob-type": "BlockBlob"}
        upload_response = requests.put(upload_uri, zip_file, headers=upload_headers)
        upload_response.raise_for_status()

    def _get(self, path):
        return self._invoke("GET", path)

    def _post(self, path, obj=None):
        return self._invoke("POST", path, obj)

    def _put(self, path, obj=None):
        return self._invoke("PUT", path, obj)

    def _delete(self, path):
        return self._invoke("DELETE", path)

    def _invoke(self, method, path, obj=None):
        body = ""
        if not obj is None:
            body = json.dumps(obj)
        conn = http.client.HTTPSConnection(self.base_uri)
        conn.request(method, path, body, self.request_headers)
        response = conn.getresponse()
        response_body = response.read().decode()
        response_body_length = int(response.headers["Content-Length"])
        response_obj = None
        if not response_body is None and response_body_length != 0:
            response_obj = json.loads(response_body)
        response_ok = self._response_ok(response)
        conn.close()
        return (response_ok, response_obj)

    def _response_ok(self, response):
        status_code = int(response.status)
        return status_code >= 200 and status_code <= 299

Get app submission listing data

The following example defines helper functions that return JSON-formatted listing data for a new sample app submission.

def get_listings_object():
    """Gets a sample listings map for a submission."""
    listings = {
        # Each listing is targeted at a specific language-locale code, e.g. EN-US.
        "en-us" : {
            # This structure holds basic information to display in the store.
            "baseListing" : {
                "copyrightAndTrademarkInfo" : "(C) 2017 Microsoft",
                # Up to 7 keywords may be provided in a listing.
                "keywords"  : ["SampleApp", "SampleFightingGame", "GameOptions"],
                "licenseTerms" : "http://example.com/licenseTerms.aspx",
                "privacyPolicy" : "http://example.com/privacyPolicy.aspx",
                "supportContact" : "support@example.com",
                "websiteUrl" : "http://example.com",
                "description" : "A sample game showing off gameplay options code.",
                "features" : ["Doesn't crash", "Likes to eat chips"],
                "releaseNotes" : "Initial release",
                "recommendedHardware" : [],
                # If your app works better with specific hardware (or needs it), you can
                # add or update values here.
                "hardwarePreferences": ["Keyboard", "Mouse"],
                # The title of the app must match a reserved name for the app in Dev Center.
                # If it doesn't, attempting to update the submission will fail.
                "title" : "Super Dev Center API Simulator 2017",
                "images" : [
                    # There are several types of images available; at least one screenshot
                    # is required.
                    {
                        # The file name is relative to the root of the uploaded ZIP file.
                        "fileName" : "img/screenshot.png",
                        "description" : "A basic screenshot of the app.",
                        "imageType" : "Screenshot"
                    }
                ]
            },
            # If there are any specific overrides to above information for Windows 8,
            # Windows 8.1, Windows Phone 7.1, 8.0, or 8.1, you can add information here.
            "platformOverrides" : {}
        }
    }
    return listings

def get_package_object():
    """Gets a sample package for the submission in Dev Center."""
    package = {
        # The file name is relative to the root of the uploaded ZIP file.
        "fileName" : "bin/super_dev_ctr_api_sim.appxupload",
        # If you haven't begun to upload the file yet, set this value to "PendingUpload".
        "fileStatus" : "PendingUpload"
    }
    return package

def get_pricing_object():
    """Gets a sample pricing object for a submission."""
    pricing = {
        # How long the trial period is, if one is allowed. Valid values are NoFreeTrial,
        # OneDay, SevenDays, FifteenDays, ThirtyDays, or TrialNeverExpires.
        "trialPeriod" : "NoFreeTrial",
        # Maps to the default price for the app.
        "priceId" : "Free",
        # If you'd like to offer your app in different markets at different prices, you
        # can provide priceId values per language/locale code.
        "marketSpecificPricing" : {}
    }
    return pricing

def get_device_families_object():
    """Gets a sample device families object for a submission."""
    device_families = {
        # Supported values are Desktop, Mobile, Xbox, and Holographic. To make
        # the app available on that specific platform, set the value to True.
        "Desktop" : True,
        "Mobile" : False,
        "Xbox" : True,
        "Holographic" : False
    }
    return device_families

def get_gaming_options_object():
    """Gets a sample gaming options object for a submission."""
    gaming_options = {
        # The genres of your app.
        "Genres" : ["Games_Fighting"],

        # Set this to True if your game supports local multiplayer. This field is required.
        "IsLocalMultiplayer" : True,

        # If local multiplayer is supported, you must provide the minimum and maximum players
        # supported. Valid values are between 2 and 1000 inclusive.
        "LocalMultiplayerMinPlayers" : 2,
        "LocalMultiplayerMaxPlayers" : 4,

        # Set this to True if your game supports local co-op play. This field is required.
        "IsLocalCooperative" : True,

        # If local co-op is supported, you must provide the minimum and maximum players
        # supported. Valid values are between 2 and 1000 inclusive.
        "LocalCooperativeMinPlayers" : 2,
        "LocalCooperativeMaxPlayers" : 4,

        # Set this to True if your game supports online multiplayer. This field is required.
        "IsOnlineMultiplayer" : True,

        # If online multiplayer is supported, you must provide the minimum and maximum players
        # supported. Valid values are between 2 and 1000 inclusive.
        "OnlineMultiplayerMinPlayers" : 2,
        "OnlineMultiplayerMaxPlayers" : 4,

        # Set this to true if your game supports online co-op play. This field is required.
        "IsOnlineCooperative" : True,

        # If online co-op is supported, you must provide the minimum and maximum players
        # supported. Valid values are between 2 and 1000 inclusive.
        "OnlineCooperativeMinPlayers" : 2,
        "OnlineCooperativeMaxPlayers" : 4,

        # If your game supports broadcasting a stream to other players, set this field to True.
        # The field is required.
        "IsBroadcastingPrivilegeGranted" : True,

        # If your game supports cross-device play (e.g. a player can play on an Xbox One with
        # their friend who's playing on a PC), set this field to True. This field is required.
        "IsCrossPlayEnabled" : True,

        # If your game supports Kinect usage, set this field to "Enabled", otherwise, set it to
        # "Disabled". This field is required.
        "KinectDataForExternal" : "Disabled",

        # Free text about any other peripherals that your game supports. This field is optional.
        "OtherPeripherals" : "Supports the usage of all fighting joysticks."
    }
    return gaming_options

def get_trailer_object():
    """Gets a sample trailer object for the submission in Dev Center."""
    trailer = {
        # This is the filename of the trailer. The file name is a relative path to the
        # root of the ZIP file to be uploaded to the API.
        "VideoFileName" : "trailers/main/my_awesome_trailer.mpeg",

        # Aside from the video itself, a trailer can have image assets such as screenshots
        # or alternate images. These are separated by language-locale code, e.g. EN-US.
        "TrailerAssets" : {
            "en-us" : {

                # The title of the trailer to display in the store.
                "Title" : "Main Trailer",

                # The list of images provided with the trailer that are shown
                # when the trailer isn't playing.
                "ImageList" : [
                    {
                        # The file name of the image. The file name is a relative
                        # path to the root of the ZIP
                        # file to be uploaded to the API.
                        "FileName" : "trailers/main/thumbnail.png",

                        # A plaintext description of what the image represents.
                        "Description" : "The thumbnail for the trailer shown " +
                                        "before the user clicks play"
                    },
                    {
                        "FileName" : "trailers/main/alt-img.png",
                        "Description" : "The image to show after the trailer plays"
                    }
                ]
            }
        }
    }
    return trailer