· Developers · 6 min read

Play Store listing updates via Google Play Developer API

Play Store listing updates via Google Play Developer API
TL;DR. Updating a Play listing through the Android Publisher API is one transaction. You open an edit (edits.insert), change the listing text and images inside it (edits.listings.update, edits.images.upload), then edits.commit to validate and publish — or edits.abandon to discard. Nothing goes live until the commit. Auth is a Google Cloud service account, and the one step that trips almost everyone up: the service account must be invited in the Play Console, not just given a role in Google Cloud IAM.

The Google Play Developer API (its formal name is the Android Publisher API) lets you change a store listing — title, descriptions, screenshots, feature graphic — without opening the Play Console. That is what you want if you are pushing localized copy from a CMS, syncing screenshots from a build pipeline, or updating dozens of language listings at once. This is the end-to-end flow for doing it correctly, including the parts the reference docs bury.

An edit is a transaction, not a set of live writes

The mental model that saves you the most grief: you never edit the live listing directly. You open an edit, which is a private staging copy of the app's current deployed state — listings, images, tracks, everything is copied in. You make all your changes against that copy. Then you commit the whole thing at once, or you abandon it and nothing ever happened.

Google's own wording is blunt: "Changes made within an edit are not live until the edit is committed." On commit, if there are no validation errors, every change in the edit goes live together, replacing the current state. If validation fails, the API throws and the live listing is untouched. So the lifecycle is exactly four moves:

  • edits.insert — create the edit, get back an editId.
  • modifyedits.listings.update for text per language, edits.images.upload / deleteall for screenshots and graphics.
  • edits.commit — validate everything, then publish it all atomically.
  • edits.abandon — throw the draft away, live listing unchanged.

One hard constraint to design around: a given account may have only one edit open at a time, and if anyone commits an edit or edits the app through the Play Console UI, every other open edit for that app is invalidated. Treat an edit as short-lived — open it, write it, commit it. Do not hold one open for hours while a human clicks around the console.

Auth: a service account, plus the invite everyone forgets

For an automated updater you want a service account, not user OAuth. Two systems are involved, and they are genuinely separate:

  1. Google Cloud. Create a service account, enable the Google Play Android Developer API on the project, and download a JSON key. The only scope you need is https://www.googleapis.com/auth/androidpublisher.
  2. Play Console. Go to Users & permissions, click Invite new users, paste the service account's email (the ...@...iam.gserviceaccount.com address), and grant it access to the app. Only then can that key touch your listing.
The 403 that eats an afternoon. Granting the service account a role in Google Cloud IAM is not the same as inviting it in the Play Console. They are two independent permission systems. A service account with perfect IAM roles but no Play Console invite gets a flat permission-denied on every call. If your first request 403s, this is almost always why — check Users & permissions in the Play Console, not IAM.

One more precondition: the app must already exist and have had at least one release (at least one APK/AAB uploaded through the console). You cannot bootstrap a brand-new app purely over the API.

The four calls, as REST

# The whole update is one transaction. Nothing is live until step 4.

# 1. Open an edit — a private, staging copy of the current live listing.
POST /androidpublisher/v3/applications/{packageName}/edits
     -> 200  { "id": "05121...", "expiryTimeSeconds": "..." }

# 2. Replace the listing for one language. listings.update is a full PUT:
#    fields you omit are cleared, not left alone.
PUT  /androidpublisher/v3/applications/{packageName}/edits/{editId}/listings/en-US
     body: {
       "language": "en-US",
       "title": "Acme - habit tracker",
       "shortDescription": "Build habits that actually stick.",
       "fullDescription": "The long description..."
     }

# 3. Swap screenshots for an image type (multipart image/png).
POST /upload/androidpublisher/v3/applications/{packageName}/edits/{editId}/listings/en-US/phoneScreenshots

# 4. Commit. Google validates the whole edit, then it goes live.
POST /androidpublisher/v3/applications/{packageName}/edits/{editId}:commit

#    ...or throw the entire draft away with no trace:
POST /androidpublisher/v3/applications/{packageName}/edits/{editId}:abandon

Updating the listing text

edits.listings.update is a PUT — a full replace of that language's listing. Whatever you send becomes the listing; fields you leave out are cleared, not preserved. So if you only want to change the short description, you still send the title and full description along with it, or you will wipe them. When you genuinely want a partial change, there is a separate edits.listings.patch that merges only the fields you provide. For most pipelines the full PUT is cleaner — you are rendering the complete listing from your source of truth anyway, so replacing it wholesale is exactly right.

The three text fields and their limits: title up to 30 characters, shortDescription up to 80, fullDescription up to 4000. One listing resource per language, keyed by the BCP-47 language tag in the URL (en-US, de-DE, ja-JP, and so on). To update ten languages you make ten listings.update calls inside the same edit — then one commit publishes them together.

Uploading screenshots and the feature graphic

Images are attached per language and per image type. The image type is an enum, and each asset slot in the listing maps to one of these values:

  • phoneScreenshots, sevenInchScreenshots, tenInchScreenshots — the phone and tablet screenshot sets.
  • tvScreenshots, wearScreenshots — Android TV and Wear OS.
  • featureGraphic — the 1024×500 banner shown at the top of the listing.
  • icon, tvBanner — the app icon and the TV banner.

edits.images.upload adds one image of a given language and type to the edit. There is no "set the whole array" call, so the reliable pattern for replacing screenshots is edits.images.deleteall for that language and image type first, then upload the new set in the order you want them shown. edits.images.list reads what is currently in the edit, and edits.images.delete removes a single image by id if you need surgical changes. All of it stays inside the edit until you commit.

Committing — and what "live" actually means

// Node.js with the official googleapis client.
import { google } from 'googleapis';
import { createReadStream } from 'node:fs';

const packageName = 'com.acme.app';
const language = 'en-US';
const screenshotFiles = ['01.png', '02.png', '03.png'];

// Service-account JSON key + the single androidpublisher scope.
const auth = new google.auth.GoogleAuth({
  keyFile: 'service-account.json',
  scopes: ['https://www.googleapis.com/auth/androidpublisher'],
});
const play = google.androidpublisher({ version: 'v3', auth });

// 1. Open the edit.
const { data: edit } = await play.edits.insert({ packageName });
const editId = edit.id;

// 2. Replace the localized listing (full PUT — send every field you want kept).
await play.edits.listings.update({
  packageName, editId, language,
  requestBody: {
    language,
    title: 'Acme - habit tracker',            // max 30 chars
    shortDescription: 'Build habits that stick.', // max 80 chars
    fullDescription: 'The full description...',    // max 4000 chars
  },
});

// 3. Clear the old phone screenshots, then upload the new set in order.
await play.edits.images.deleteall({
  packageName, editId, language, imageType: 'phoneScreenshots',
});
for (const file of screenshotFiles) {
  await play.edits.images.upload({
    packageName, editId, language, imageType: 'phoneScreenshots',
    media: { mimeType: 'image/png', body: createReadStream(file) },
  });
}

// 4. Commit. Nothing above was live until this line succeeds.
await play.edits.commit({ packageName, editId });

A few things worth being precise about, because they surprise people:

  • No new build required. Committing a listing-only edit does not need a fresh APK/AAB. Text and images are metadata; you can update them any number of times against the existing release. (The app just needs that one prior release to exist.)
  • Commit validates, then publishes. If a screenshot is the wrong dimension or a field is too long, the commit fails and the live listing never changes — you fix and re-commit.
  • It is not instant. After a successful commit, changes can take up to several hours to appear, the same as edits made by hand in the Play Console. Do not treat a 200 on commit as "already visible to users."
  • Abandon is free. If a dry run looks wrong, edits.abandon discards the draft with zero effect on the live listing. Useful for validating a pipeline without risk.

The no-code path: design, translate, publish

The API above is the right tool if you have engineering time to spend and a source of truth to sync from. What it does not do is make the assets. You still have to design the screenshots, write the title and both descriptions, and produce all of that per language — the API only ships what you hand it.

That is the part Mokbi handles. You design the screenshots in the browser, draft the title, short description and full description alongside them, and translate the entire listing into 50 languages in one pass — so the ten listings.update calls above have real, localized copy to send instead of placeholder text.

And the publish itself? Mokbi does that too. For Google Play it runs exactly this flow under the hood (edits.insertlistings.update → image upload → commit), so your localized listing and assets go live without you writing a line of the code above. For the App Store it stages the version in App Store Connect, filled in and ready to submit, since Apple requires you to press the final Submit and pass review. Designing the screenshots and feature graphic, writing the listing, translating it into 50 languages, and pushing it live are one continuous pass.

What to read next

Open the editor →