I shipped two iOS apps last weekend. Apple's review system approved both within 24h.
Neither showed up on the App Store for 36 hours after approval.
Not "delayed propagation". Not "iTunes Lookup cache". The apps were approved, signed, ready, and completely invisible to every potential user on Earth.
The cause: appAvailabilities was never POST'd. Zero territories assigned. The apps were live in a parallel universe nobody could see.
This post is the lesson I'd have paid money for two weeks ago.
What the symptoms looked like
Two apps — HabitHash and FocusFlow Lite — both moved through the normal sequence:
- CI on macos-15 ran
fastlane match+xcodebuild archive -
.ipauploaded to TestFlight cleanly - ASC API submitted for review (
POST /v1/reviewSubmissionsetc.) - Apple reviewer approved
- ASC state moved to
READY_FOR_SALE
Standard stuff. I tagged them as LIVE in my dashboard. Tweeted about the launch.
Then I checked https://itunes.apple.com/lookup?bundleId=com.jiejuefuyou.habithash.
{ "resultCount": 0, "results": [] }
Zero results. For an app that ASC swore was READY_FOR_SALE.
I checked App Store search on a US iPhone. App not found. JP iPhone — same. Tried the direct apps.apple.com/us/app/id<trackId> URL — Apple's web shell rendered "App Not Available in Your Country or Region".
This is the bit that breaks your brain: the country selector was empty.
Why this happens
Apple's submission pipeline has two completely separate readiness gates:
-
Content gate — metadata, screenshots, IAP review notes, App Privacy, build VALID. This is what
appStoreVersionstracks. Reviewer approval moves this toREADY_FOR_SALE. -
Distribution gate — which territories can sell the app. This is what
appAvailabilitiestracks. Default = empty.
For LIVE app updates, you inherit territories from the previous version. So if you shipped v1.0 with 174 territories and you push v1.1, you keep your 174.
For a brand new app's first version, there is no previous version to inherit from. Territory selection is a separate, deliberate action. If you never do it, the app is approved but unsellable.
The ASC web UI front-loads territory selection in the Pricing wizard the first time you create a price tier. If you create the app via API (which I do, because Windows + Fastlane + headless), the wizard never fires.
So the territory list stays at [].
The fix is an API call, not a web wizard
You don't need the ASC web. You don't need a Mac. You need one POST.
ASC API V2 supports POST /v2/appAvailabilities with all 175 territories in a single request. The catch is the id syntax in the included block — it uses an inline-creation pattern with literal ${LOCAL_ID} curly braces. This is not documented in the API reference index in the way you'd expect — it's mentioned once in a sub-resource page and absent from most blog posts.
Here's the actual working request body:
import json
import jwt
import requests
import time
# Standard ASC JWT minting (key_id, issuer_id, p8 file)
def mint_token():
with open("AuthKey_XXXX.p8") as f:
key = f.read()
payload = {
"iss": "<ISSUER_ID>",
"exp": int(time.time()) + 600,
"aud": "appstoreconnect-v1",
}
return jwt.encode(payload, key, algorithm="ES256", headers={"kid": "<KEY_ID>"})
# All 175 ITU territory codes (subset shown)
TERRITORIES = ["USA", "JPN", "GBR", "DEU", "FRA", "CAN", "AUS", "CHN", "KOR", "IND"] # … 175 total
def restore_all_territories(app_id: str):
token = mint_token()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
included = [
{
"type": "territoryAvailabilities",
"id": f"${{{code}}}", # literal: ${USA}
"attributes": {"available": True},
"relationships": {
"territory": {"data": {"type": "territories", "id": code}},
},
}
for code in TERRITORIES
]
body = {
"data": {
"type": "appAvailabilities",
"attributes": {"availableInNewTerritories": True},
"relationships": {
"app": {"data": {"type": "apps", "id": app_id}},
"territoryAvailabilities": {
"data": [{"type": "territoryAvailabilities", "id": f"${{{c}}}"} for c in TERRITORIES],
},
},
},
"included": included,
}
r = requests.post(
"https://api.appstoreconnect.apple.com/v2/appAvailabilities",
headers=headers,
data=json.dumps(body),
)
print(r.status_code, r.text[:500])
Notes that took me a full afternoon to figure out:
-
id: "USA"returns 409ENTITY_ERROR.INCLUDED.INVALID_ID. Theincludedresources must use the${...}inline-creation placeholder. -
id: "$USA"(single dollar, no braces) also fails. The full literal${USA}is what the API parser accepts. - The response body's
data.relationships.territoryAvailabilities.datareturns[]even on a successful 201. This is misleading. Verify byGET /v2/appAvailabilities/{app_id}/territoryAvailabilities?limit=200after the POST — you will see 175/175 attached. -
availableInNewTerritories: Trueis what lets Apple auto-add future territories Apple opens (they add 2-3 a year). - Propagation to public
iTunes Lookuplags 5-30 minutes after the POST succeeds. The ASC API state is immediately correct; the public CDN catches up slower.
Why this isn't on Stack Overflow
The Apple-side approval state and the territory state are decoupled in a way that's invisible from the standard API summary. If your app was first created via the ASC web, you'd never hit this — the Pricing wizard handles it for you on day one. The trap is specific to fully API-driven workflows (which is the only sane option from Windows).
Searching for "approved but not on app store" turns up cache flush threads, time-zone confusion, and people misreading state transitions. Searching for "appAvailabilities API" turns up a docs page that uses ${LOCAL_ID} in an example but doesn't flag it as load-bearing.
What this means if you're building from Windows
If you're using fastlane + ASC API + GitHub Actions macos-15 from a Windows dev box (yes, this is a real workflow — I've shipped 8 apps this way in 45 days), bake a territory POST into your release pipeline for every new app's first version. Not v1.0.x — every brand new bundle ID.
Build a script. Wire it after the first appStoreVersion is approved. Verify with the GET I mentioned above. Don't trust the "the app is live" tweet you've already drafted until iTunes Lookup returns a resultCount > 0.
This sits in my ship_done_verify.py now as condition 4 (alongside build-VALID, ASV-in-ship-state, IAP preflight). The lesson cost me 36 hours of confused "but Apple said it's approved?" debugging.
May it cost you zero.
Sources / references
- Apple App Store Connect API reference:
/v2/appAvailabilities(the inline-creation pattern is shown in the schema example but not flagged as required) - Apple territory ITU-3 country codes: https://www.itu.int/dms_pub/itu-t/oth/02/01/T02010000730003PDFE.pdf
- Reusable script source (open-sourced):
dashboard/asc_4apps_resubmit_2026_05_20.py(full 175-territory list + retry logic)
If you've shipped via the ASC API from a non-Mac dev box, I'd love to compare notes. Reply or DM @Snakesun_H on X.
Hao Sun
Building 8 iOS apps in 45 days from a Windows machine. Hireable. Substack: autoappnotes.substack.com
Top comments (0)