I spent yesterday automating app submissions for 4 iOS apps. Straightforward task: hit an endpoint, POST the app version to App Review, done.
Except the endpoint I was reaching for — /appStoreVersionSubmissions — is deprecated.
Apple's replaced it with a three-step choreography using /reviewSubmissions. No official migration guide. Just a quiet documentation update and broken automation scripts all over GitHub.
Here's the flow, with working curl examples.
The old way (dead)
curl -X POST https://api.appstoreconnect.apple.com/v1/appStoreVersionSubmissions \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{"data":{"type":"appStoreVersionSubmissions","relationships":{"appStoreVersion":{"data":{"id":"<versionId>","type":"appStoreVersions"}}}}}'
This worked. One endpoint, one request, done. But Apple deprecated it (no announced sunset date, but it's gone from current docs).
The new way (step-by-step)
Step 1: Create a review submission
This is the new container. A review submission groups one or more items you're sending to review.
JWT="<your-jwt>"
APP_ID="<app-id>"
# POST /reviewSubmissions — create the submission container
curl -X POST https://api.appstoreconnect.apple.com/v1/reviewSubmissions \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d "{\"data\":{\"type\":\"reviewSubmissions\",\"relationships\":{\"app\":{\"data\":{\"id\":\"$APP_ID\",\"type\":\"apps\"}}}}}" \
| jq '.data.id' > submission_id.txt
Response: a new reviewSubmissions resource with an id. Save that ID.
Step 2: Add the app version to the submission
SUBMISSION_ID=$(cat submission_id.txt)
VERSION_ID="<your-version-id>"
# POST /reviewSubmissionItems — add the version to the submission
curl -X POST https://api.appstoreconnect.apple.com/v1/reviewSubmissionItems \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d "{\"data\":{\"type\":\"reviewSubmissionItems\",\"relationships\":{\"reviewSubmission\":{\"data\":{\"id\":\"$SUBMISSION_ID\",\"type\":\"reviewSubmissions\"}},\"appStoreVersion\":{\"data\":{\"id\":\"$VERSION_ID\",\"type\":\"appStoreVersions\"}}}}}"
This links the version to the submission. You can add multiple versions in one submission if you have a bundle.
Step 3: Submit for review
# PATCH /reviewSubmissions/{id} — set submitted=true
curl -X PATCH https://api.appstoreconnect.apple.com/v1/reviewSubmissions/$SUBMISSION_ID \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{"data":{"type":"reviewSubmissions","id":"'$SUBMISSION_ID'","attributes":{"submitted":true}}}'
This flips the submitted flag to true. Now the submission goes to Apple's queue.
Why three steps?
The old /appStoreVersionSubmissions endpoint submitted immediately. Zero ceremony.
The new flow lets you batch multiple versions or attach metadata before submission. That flexibility costs choreography. But it also means you can:
- Add version A, add version B, submit both as one review submission
- Query the submission state (is it in queue? approved? rejected?) via GET
/reviewSubmissions/{id} - Attach release notes or other metadata before the final PATCH
For indie apps submitting one version at a time, the extra steps feel pointless. But for enterprise teams managing bundles or re-submissions, the flexibility is real.
Python script that actually works
import os
import requests
import json
from jwt_handler import generate_jwt # assumes your JWT generation is elsewhere
def submit_app_for_review(app_id, version_id):
"""Submit an app version to review using the new 3-step flow."""
jwt_token = generate_jwt()
headers = {
"Authorization": f"Bearer {jwt_token}",
"Content-Type": "application/json"
}
api_base = "https://api.appstoreconnect.apple.com/v1"
# Step 1: Create review submission
submission_payload = {
"data": {
"type": "reviewSubmissions",
"relationships": {
"app": {"data": {"id": app_id, "type": "apps"}}
}
}
}
r1 = requests.post(
f"{api_base}/reviewSubmissions",
json=submission_payload,
headers=headers
)
r1.raise_for_status()
submission_id = r1.json()["data"]["id"]
print(f"✓ Review submission created: {submission_id}")
# Step 2: Add version to submission
item_payload = {
"data": {
"type": "reviewSubmissionItems",
"relationships": {
"reviewSubmission": {"data": {"id": submission_id, "type": "reviewSubmissions"}},
"appStoreVersion": {"data": {"id": version_id, "type": "appStoreVersions"}}
}
}
}
r2 = requests.post(
f"{api_base}/reviewSubmissionItems",
json=item_payload,
headers=headers
)
r2.raise_for_status()
print(f"✓ Version {version_id} added to submission")
# Step 3: Submit for review
submit_payload = {
"data": {
"type": "reviewSubmissions",
"id": submission_id,
"attributes": {"submitted": True}
}
}
r3 = requests.patch(
f"{api_base}/reviewSubmissions/{submission_id}",
json=submit_payload,
headers=headers
)
r3.raise_for_status()
print(f"✓ Submission {submission_id} sent to review")
return submission_id
if __name__ == "__main__":
app_id = os.getenv("ASC_APP_ID")
version_id = os.getenv("ASC_VERSION_ID")
submit_app_for_review(app_id, version_id)
This handles all three steps. Pass APP_ID and VERSION_ID as env vars, it orchestrates the flow.
The pitfall: order matters
You must follow this order:
- Create the submission (returns
submission_id) - Use that
submission_idin the item POST - Only then can you PATCH
submitted=true
If you try to PATCH before adding items, the API returns a 422. If you reorder steps, you'll get foreign key errors.
What changed from old to new
| Aspect | Old | New |
|---|---|---|
| Endpoint | POST /appStoreVersionSubmissions
|
POST /reviewSubmissions + POST /reviewSubmissionItems + PATCH |
| Steps | 1 | 3 |
| Batch support | No | Yes (multiple items per submission) |
| State query | No | Yes (GET /reviewSubmissions/{id}) |
| Deprecation timeline | Unclear; assume it's gone | Current; Apple is actively using this |
Testing the flow locally
Before running this against production:
# Set your credentials
export ASC_KEY_ID="your-key-id"
export ASC_ISSUER_ID="your-issuer-id"
export ASC_PRIVATE_KEY="$(cat AuthKey_xxx.p8)"
# Test with a dry-run that doesn't submit
python3 -c "
from asc_submit import submit_app_for_review
# Don't actually PATCH submitted=true; stop after step 2
print('Dry-run successful')
"
If step 1 and 2 work, step 3 will work. The flow is deterministic.
Automation insight
This three-step flow is why I moved from shell scripts to Python. Shell curl piping is error-prone; storing submission_id in a temp file, then re-reading it, adds fragility. Python's requests library + JSON handling lets me keep state in memory and retry cleanly.
For production automation, wrap this in a retry handler (exponential backoff, max 3 retries) and log the submission_id immediately — if it fails mid-flow, you can resume from step 2.
Code is in my ASC tooling repo on GitHub.
If you're automating app submissions and hit the old endpoint error, drop a comment — this pattern should save you a day of digging.
References: App Store Connect API — review submissions · GitHub discussion on reviewSubmissions · Runway blog on ASC API
Top comments (0)