DEV Community

孫昊
孫昊

Posted on

The ASC API's 3-step review submission flow (and why appStoreVersionSubmissions is gone)

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"}}}}}'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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\"}}}}}"
Enter fullscreen mode Exit fullscreen mode

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}}}'
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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:

  1. Create the submission (returns submission_id)
  2. Use that submission_id in the item POST
  3. 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')
"
Enter fullscreen mode Exit fullscreen mode

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)