“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
- Re-uploading the bundle — the upload was never the problem.
- Switching to the internal testing track, hoping the rule is per-track. It is about the app's published state, not the track name.
- Waiting for "processing" to finish. Play is not still thinking; it rejected the request.
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:
- Parse the HTTP status of every call. A 200 on upload says nothing about the track update.
- Read the tracks back after committing and assert the expected
statusandversionCode. If they are not there, exit non-zero.
Related: manifest mistakes that get Play builds rejected, the 16 KB page-size rejection, and the pre-upload checklist.
Get new posts
One email when a new post ships. No spam.