TL;DR
I submitted an iOS app update for review—build → upload → build association → whatsNew → review submission—entirely from the command line using the App Store Connect REST API. Unlike the initial release, metadata and screenshots carry over from the previous version, so the required operations are minimal.
Prerequisite: This guide assumes you’ve already completed the setup from Complete Guide to Submitting an iOS App for Review Using Only the App Store Connect API (API key, JWT generation, helper functions).
Overview
- Increment the build number
- Archive, export IPA, and upload (
xcodebuild+xcrun altool) - Confirm build processing is complete (API)
- Associate build with version (API)
- Set encryption compliance (API)
- Set whatsNew (release notes) (API)
- Submit for review (API)
1. Increment the Build Number
App Store Connect rejects uploads with duplicate build numbers. You need to increment CURRENT_PROJECT_VERSION.
If using XcodeGen, edit project.yml:
# project.yml
settings:
base:
MARKETING_VERSION: "1.1.0"
CURRENT_PROJECT_VERSION: "4" # Changed from 3 → 4
Note: The marketing version (
1.1.0) is the user-facing version number, while the build number (4) is a sequential number within the same version. For bug fix resubmissions with no user-visible changes, you only need to increment the build number.
2. Archive and Upload
# Set environment variables
export APP_STORE_API_KEY="YOUR_KEY_ID"
export APP_STORE_API_ISSUER="YOUR_ISSUER_ID"
# Regenerate project with XcodeGen
xcodegen generate
# Archive
xcodebuild archive \
-project KotenOCR.xcodeproj \
-scheme KotenOCR \
-archivePath build/KotenOCR.xcarchive \
-destination "generic/platform=iOS" \
-quiet
# Export IPA
xcodebuild -exportArchive \
-archivePath build/KotenOCR.xcarchive \
-exportPath build/export \
-exportOptionsPlist scripts/ExportOptions.plist \
-quiet
# Upload to App Store Connect
xcrun altool --upload-app \
--type ios \
--file build/export/KotenOCR.ipa \
--apiKey "$APP_STORE_API_KEY" \
--apiIssuer "$APP_STORE_API_ISSUER"
Important: Environment variables must be
exported forxcrun altoolto access them.source .envalone won’t pass them to subprocesses.
Example output on successful upload:
UPLOAD SUCCEEDED with no errors
Delivery UUID: cdf01bf8-...
Transferred 80033738 bytes in 7.487 seconds (10.7MB/s)
3. Confirm Build Processing
After upload, App Store Connect takes a few minutes to process the build. Wait until processingState becomes VALID.
result = api_request("GET",
f"builds?filter[app]={APP_ID}&filter[version]=4&sort=-uploadedDate&limit=1")
build = result["data"][0]
state = build["attributes"]["processingState"]
print(f"Build state: {state}") # VALID means ready
BUILD_ID = build["id"]
4. Associate Build with Version
Link the new build to the existing version (in PREPARE_FOR_SUBMISSION state).
# Get version ID
result = api_request("GET",
f"apps/{APP_ID}/appStoreVersions"
"?filter[versionString]=1.1.0&filter[platform]=IOS")
VERSION_ID = result["data"][0]["id"]
state = result["data"][0]["attributes"]["appStoreState"]
print(f"Version: {VERSION_ID}, state: {state}")
# Associate build
api_request("PATCH",
f"appStoreVersions/{VERSION_ID}/relationships/build", {
"data": {
"type": "builds",
"id": BUILD_ID
}
})
print("Build assigned to version")
5. Set Encryption Compliance
If your app only uses standard HTTPS and no custom encryption:
api_request("PATCH", f"builds/{BUILD_ID}", {
"data": {
"type": "builds",
"id": BUILD_ID,
"attributes": {
"usesNonExemptEncryption": False
}
}
})
Note: If already set, this returns a 409 error. You can safely skip it.
6. Set whatsNew (Release Notes)
The most critical step for update submissions. Each localization must have whatsNew set, or the review submission will be blocked.
# Get localizations
result = api_request("GET",
f"appStoreVersions/{VERSION_ID}/appStoreVersionLocalizations")
whats_new = {
"ja": "- チップ(Tip Jar)機能を追加しました\n- 不具合を修正しました",
"en-US": "- Added Tip Jar feature\n- Bug fixes"
}
for loc in result["data"]:
locale = loc["attributes"]["locale"]
loc_id = loc["id"]
if locale in whats_new:
api_request("PATCH",
f"appStoreVersionLocalizations/{loc_id}", {
"data": {
"type": "appStoreVersionLocalizations",
"id": loc_id,
"attributes": {
"whatsNew": whats_new[locale]
}
}
})
print(f"whatsNew set for {locale}")
Difference from initial release: The initial release requires setting
description,keywords, screenshots, and more. For updates, these carry over from the previous version. OnlywhatsNewis newly required.
7. Submit for Review
Use the reviewSubmissions API to submit for review (the older appStoreVersionSubmissions is deprecated).
# Step 1: Create review submission
result = api_request("POST", "reviewSubmissions", {
"data": {
"type": "reviewSubmissions",
"attributes": {
"platform": "IOS"
},
"relationships": {
"app": {
"data": {
"type": "apps",
"id": APP_ID
}
}
}
}
})
SUBMISSION_ID = result["data"]["id"]
# Step 2: Add version to submission
api_request("POST", "reviewSubmissionItems", {
"data": {
"type": "reviewSubmissionItems",
"relationships": {
"reviewSubmission": {
"data": {
"type": "reviewSubmissions",
"id": SUBMISSION_ID
}
},
"appStoreVersion": {
"data": {
"type": "appStoreVersions",
"id": VERSION_ID
}
}
}
}
})
# Step 3: Submit for review
result = api_request("PATCH", f"reviewSubmissions/{SUBMISSION_ID}", {
"data": {
"type": "reviewSubmissions",
"id": SUBMISSION_ID,
"attributes": {
"submitted": True
}
}
})
print(f"State: {result['data']['attributes']['state']}")
# => WAITING_FOR_REVIEW
8. Release After Review Approval
If your release method is set to “Manually release this version” in App Store Connect, the status after review approval will be PENDING_DEVELOPER_RELEASE. Use appStoreVersionReleaseRequests to release via API.
# Check version state
result = api_request("GET",
f"apps/{APP_ID}/appStoreVersions"
"?filter[versionString]=1.1.0&filter[platform]=IOS")
version = result["data"][0]
VERSION_ID = version["id"]
state = version["attributes"]["appStoreState"]
print(f"State: {state}") # PENDING_DEVELOPER_RELEASE
# Release
api_request("POST", "appStoreVersionReleaseRequests", {
"data": {
"type": "appStoreVersionReleaseRequests",
"relationships": {
"appStoreVersion": {
"data": {
"type": "appStoreVersions",
"id": VERSION_ID
}
}
}
}
})
print("Released!")
Note: After releasing, it may take a few hours for the update to appear on the App Store.
9. Setting Territory Availability
By default, your app may only be available in limited territories. To make it available worldwide, use the appAvailabilities API (v2).
# Get all territories
result = api_request("GET", "territories?limit=200")
all_territories = [t["id"] for t in result["data"]]
print(f"Total territories: {len(all_territories)}")
# Build inline creation payload
included = []
territory_refs = []
for tid in all_territories:
local_id = f"${{territory-{tid}}}"
territory_refs.append({"type": "territoryAvailabilities", "id": local_id})
included.append({
"type": "territoryAvailabilities",
"id": local_id,
"attributes": {"available": True},
"relationships": {
"territory": {
"data": {"type": "territories", "id": tid}
}
}
})
# Set availability via v2 API
token = generate_token()
url = "https://api.appstoreconnect.apple.com/v2/appAvailabilities"
data = {
"data": {
"type": "appAvailabilities",
"attributes": {"availableInNewTerritories": True},
"relationships": {
"app": {
"data": {"type": "apps", "id": APP_ID}
},
"territoryAvailabilities": {
"data": territory_refs
}
}
},
"included": included
}
# v2 endpoint requires direct request
import urllib.request
req = urllib.request.Request(
url, data=json.dumps(data).encode(), method="POST",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
)
resp = urllib.request.urlopen(req)
print(f"Status: {resp.status}") # 201 = success
Note: This API uses the v2 endpoint (
/v2/appAvailabilities), not v1. Theapi_requesthelper uses/v1/as the base URL, so you need to make the request directly. For inline creation, use the${local-id}format for IDs.
Important Notes on IAP Review Submission
First IAP Submission Requires Browser
When submitting IAPs for the first time for an app, you must link them to the version via the App Store Connect browser. This cannot be done entirely through the API.
This is an Apple requirement. Calling the inAppPurchaseSubmissions API returns:
STATE_ERROR.FIRST_IAP_MUST_BE_SUBMITTED_ON_VERSION
"The first In-App Purchase for an app must be submitted for review
at the same time that you submit an app version."
Additionally, reviewSubmissionItems has no relationship for IAPs:
# Supported relationships for reviewSubmissionItems (per Apple docs)
- reviewSubmission (required)
- appStoreVersion
- appEvent
- appCustomProductPageVersion
- appStoreVersionExperiment / V2
- gameCenterLeaderboardVersion, etc.
# No IAP-related relationship exists
First IAP Submission Steps
- Use the API to create the version, assign the build, and set whatsNew
- Open the version page in the App Store Connect browser
- In the “In-App Purchases and Subscriptions” section, click “Select In-App Purchases or Subscriptions”
- Check the IAP products and click “Done”
- Use the API to create a
reviewSubmissionand submit for review
# Step 5: After linking IAPs in browser, submit via API
result = api_request("POST", "reviewSubmissions", {
"data": {
"type": "reviewSubmissions",
"attributes": {"platform": "IOS"},
"relationships": {
"app": {"data": {"type": "apps", "id": APP_ID}}
}
}
})
SUBMISSION_ID = result["data"]["id"]
api_request("POST", "reviewSubmissionItems", {
"data": {
"type": "reviewSubmissionItems",
"relationships": {
"reviewSubmission": {"data": {"type": "reviewSubmissions", "id": SUBMISSION_ID}},
"appStoreVersion": {"data": {"type": "appStoreVersions", "id": VERSION_ID}}
}
}
})
api_request("PATCH", f"reviewSubmissions/{SUBMISSION_ID}", {
"data": {
"type": "reviewSubmissions",
"id": SUBMISSION_ID,
"attributes": {"submitted": True}
}
})
Subsequent IAP Additions
Once the first IAP is approved, additional IAPs can be submitted individually via the inAppPurchaseSubmissions API (no version submission needed).
api_request("POST", "inAppPurchaseSubmissions", {
"data": {
"type": "inAppPurchaseSubmissions",
"relationships": {
"inAppPurchaseV2": {
"data": {"type": "inAppPurchases", "id": IAP_ID}
}
}
}
})
Verification
After review approval, check IAP product states via API:
result = api_request("GET", f"apps/{APP_ID}/inAppPurchasesV2")
for iap in result["data"]:
state = iap["attributes"]["state"]
product_id = iap["attributes"]["productId"]
print(f"{product_id}: {state}")
# APPROVED = OK, READY_TO_SUBMIT = not included in review
Lesson learned: Verify IAP submission by checking whether
statechanges toAPPROVEDafter app approval. If it remainsREADY_TO_SUBMIT, the IAPs were not included. Link them in the browser and resubmit.
Reference: For IAP product registration, see Complete Guide to Adding a Tip Jar Feature Using the App Store Connect API.
Summary
The API operations required for an update submission:
| Operation | Endpoint | Method |
|---|---|---|
| Check build status | builds?filter[app]={id} | GET |
| Associate build | appStoreVersions/{id}/relationships/build | PATCH |
| Encryption compliance | builds/{id} | PATCH |
| Set whatsNew | appStoreVersionLocalizations/{id} | PATCH |
| Create submission | reviewSubmissions | POST |
| Add submission item | reviewSubmissionItems | POST |
| Confirm submission | reviewSubmissions/{id} | PATCH |
| Release | appStoreVersionReleaseRequests | POST |
| Territory availability | v2/appAvailabilities | POST |
Compared to the initial release, the required operations are minimal. From build upload to release, everything can be done from the command line without opening a browser. This makes it straightforward to integrate into a CI/CD pipeline.
Related Articles
- Complete Guide to Submitting an iOS App for Review Using Only the App Store Connect API — Full procedure for the initial release
- Complete Guide to Adding a Tip Jar Feature Using the App Store Connect API — IAP product registration and configuration