Boolean Build

“Only releases with status draft may be created on draft app”

Your upload succeeded. The bundle is on Google's servers. Then the track update returns a 400 that reads like a problem with the app, and you go looking for a missing setting that does not exist. The message is about one field: status.

When it fires

The Android Publisher API accepts four release statuses: draft, inProgress, halted, completed.

An app with no published production release — a draft app, which is what you have until your first production rollout goes live — only accepts draft. Ask for completed or inProgress and Play refuses, because there is no published baseline for a completed release to sit on top of.

This is why the error arrives on your first release and then disappears forever. The code did not change; the app's state did.

What people try first

The fix

Create the release as a draft, commit the edit, then send it for review in the Play Console — that last part is not exposed in the API, and it is the step that leaves people stuck at "uploaded but not reviewed".

PKG=com.example.app
TOKEN=$(...)

BASE=https://androidpublisher.googleapis.com/androidpublisher/v3
UPLOAD=https://androidpublisher.googleapis.com/upload/androidpublisher/v3

EDIT=$(curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  "$BASE/applications/$PKG/edits" | jq -r .id)

curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @app.aab \
  "$UPLOAD/applications/$PKG/edits/$EDIT/bundles?uploadType=media"

# draft is the only status a draft app accepts
curl -s -X PUT -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"releases\":[{\"versionCodes\":[\"42\"],\"status\":\"draft\"}]}" \
  "$BASE/applications/$PKG/edits/$EDIT/tracks/production"

curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  "$BASE/applications/$PKG/edits/$EDIT:commit"

After the commit, the release exists as a draft in the console. Open Play Console → Production → review the release → Send for review. Uploading has never meant publishing.

Confirm the state instead of guessing

The tracks endpoint tells you exactly what Play believes, without opening a browser:

EDIT=$(curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  "$BASE/applications/$PKG/edits" | jq -r .id)

curl -s -H "Authorization: Bearer $TOKEN" \\
  "$BASE/applications/$PKG/edits/$EDIT/tracks" \\
  | jq '.tracks[] | {
      track,
      statuses: [.releases[]?.status],
      versions: [.releases[]?.versionCodes]
    }'

If you see "draft" and your version code, the upload worked and the only thing left is human: the console click. If the track list is empty, your edit never committed — a common outcome when a script exits non-zero before the commit and nothing checks it.

Make the pipeline fail loudly

The failure mode worth engineering against is not this error — it is a pipeline that swallows it and reports success. Two cheap habits:

Related: manifest mistakes that get Play builds rejected, the 16 KB page-size rejection, and the pre-upload checklist.