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
United States
NORTH AMERICA
Related News
What Does "Building in Public" Actually Mean in 2026?
20h ago
The Agentic Headless Backend: What Vibe Coders Still Need After the UI Is Done
20h ago
Why I’m Still Learning to Code Even With AI
22h ago
Students Boo Commencement Speaker After She Calls AI the 'Next Industrial Revolution'
5h ago

Testing for ‘Bad Cholesterol’ Doesn’t Tell the Whole Story
5h ago