Using App Store Connect API for Automated Localization
Technical guide to using the App Store Connect API for programmatic metadata localization. Automate your ASO workflow across all 40 locales.
Why Use the App Store Connect API?
Managing metadata for 40 locales through the App Store Connect web interface is tedious. Each locale requires individual editing, and a single update cycle could mean hours of copy-pasting.
The App Store Connect API changes this. You can programmatically update metadata across all locales in seconds.
API Overview
Authentication
App Store Connect uses JWT (JSON Web Token) authentication with ES256 signing.
You'll Need:
- API Key (from App Store Connect → Users and Access → Keys)
- Issuer ID
- Key ID
- Private Key (.p8 file)
JWT Structure:
`json
{
"alg": "ES256",
"kid": "YOUR_KEY_ID",
"typ": "JWT"
}
{
"iss": "YOUR_ISSUER_ID",
"iat":
"exp":
"aud": "appstoreconnect-v1"
}
`
JWT tokens expire after 20 minutes. Generate them as needed.
Base URL
All requests go to:
`
https://api.appstoreconnect.apple.com/v1
`
Key Endpoints for Localization
| Endpoint | Purpose |
|---|---|
| `/apps` | List your apps |
| `/apps/{id}/appStoreVersions` | Get versions |
| `/appStoreVersions/{id}/appStoreVersionLocalizations` | Get localizations |
| `/appStoreVersionLocalizations/{id}` | Update localization |
| `/appInfoLocalizations/{id}` | Update app-level info |
Understanding the Data Model
App-Level vs. Version-Level
App-Level Fields (persist across versions):
- App Name
- Subtitle
- Privacy Policy URL
Version-Level Fields (per release):
- Description
- Keywords
- Promotional Text
- What's New
You'll work with different endpoints depending on which fields you're updating.
Localization IDs
Each locale has a unique localization ID. When you fetch localizations, you'll get responses like:
`json
{
"data": [
{
"type": "appStoreVersionLocalizations",
"id": "abc123",
"attributes": {
"locale": "en-US",
"description": "...",
"keywords": "..."
}
},
{
"type": "appStoreVersionLocalizations",
"id": "def456",
"attributes": {
"locale": "de-DE",
"description": "...",
"keywords": "..."
}
}
]
}
`
Use these IDs for updates.
Common Workflows
Fetching Current Metadata
Step 1: Get Your App
`
GET /v1/apps?filter[bundleId]=com.yourcompany.yourapp
`
Step 2: Get Editable Version
`
GET /v1/apps/{appId}/appStoreVersions?filter[appStoreState]=PREPARE_FOR_SUBMISSION
`
Step 3: Get All Localizations
`
GET /v1/appStoreVersions/{versionId}/appStoreVersionLocalizations
`
Updating Metadata
Update a Single Locale:
`
PATCH /v1/appStoreVersionLocalizations/{localizationId}
Content-Type: application/json
{
"data": {
"type": "appStoreVersionLocalizations",
"id": "{localizationId}",
"attributes": {
"description": "Your new description...",
"keywords": "keyword1,keyword2,keyword3",
"promotionalText": "New promotional text",
"whatsNew": "What's new in this version"
}
}
}
`
Creating New Localizations
If a locale doesn't exist yet:
`
POST /v1/appStoreVersionLocalizations
Content-Type: application/json
{
"data": {
"type": "appStoreVersionLocalizations",
"attributes": {
"locale": "ja",
"description": "Japanese description...",
"keywords": "キーワード1,キーワード2"
},
"relationships": {
"appStoreVersion": {
"data": {
"type": "appStoreVersions",
"id": "{versionId}"
}
}
}
}
}
`
Building a Localization Script
Here's a practical workflow for automated localization:
Step 1: Prepare Your Data
Structure your localized content in a format like:
`json
{
"en-US": {
"description": "English description...",
"keywords": "keyword1,keyword2",
"promotionalText": "..."
},
"de-DE": {
"description": "German description...",
"keywords": "schlüsselwort1,schlüsselwort2",
"promotionalText": "..."
},
"ja": {
"description": "Japanese description...",
"keywords": "キーワード1,キーワード2",
"promotionalText": "..."
}
}
`
Step 2: Fetch Existing Localizations
Map locale codes to localization IDs:
`javascript
const localizationMap = {};
const localizations = await fetchLocalizations(versionId);
for (const loc of localizations) {
localizationMap[loc.attributes.locale] = loc.id;
}
`
Step 3: Update or Create
For each locale in your data:
`javascript
for (const [locale, content] of Object.entries(localizedContent)) {
if (localizationMap[locale]) {
// Update existing
await updateLocalization(localizationMap[locale], content);
} else {
// Create new
await createLocalization(versionId, locale, content);
}
}
`
Step 4: Verify
After updates, fetch localizations again and verify content matches what you sent.
Error Handling
Common Errors
| Status | Meaning | Solution |
|---|---|---|
| 401 | Authentication failed | Check JWT token |
| 403 | Permission denied | Verify API key permissions |
| 404 | Resource not found | Check IDs are correct |
| 409 | Conflict | Version not editable or locale exists |
| 422 | Validation failed | Check field lengths and format |
| 429 | Rate limited | Slow down requests |
Rate Limits
Apple enforces rate limits. Best practices:
- Add delays between requests (200-500ms)
- Use exponential backoff on 429 errors
- Batch operations where possible
Working with App-Level Fields
App Name and Subtitle are app-level, not version-level.
Get App Info Localizations:
`
GET /v1/apps/{appId}/appInfos
GET /v1/appInfos/{appInfoId}/appInfoLocalizations
`
Update App Info:
`
PATCH /v1/appInfoLocalizations/{localizationId}
{
"data": {
"type": "appInfoLocalizations",
"id": "{localizationId}",
"attributes": {
"name": "Your App Name",
"subtitle": "Your Subtitle"
}
}
}
`
Screenshot Upload via API
Screenshot upload is more complex:
Step 1: Create Screenshot Set (if needed)
`
POST /v1/appScreenshotSets
`
Step 2: Reserve Screenshot Asset
`
POST /v1/appScreenshots
{
"data": {
"type": "appScreenshots",
"attributes": {
"fileName": "screenshot.png",
"fileSize": 1234567
},
"relationships": {
"appScreenshotSet": {...}
}
}
}
`
Step 3: Upload Binary
The response includes upload operations with URLs and headers. Upload your file in chunks.
Step 4: Commit Upload
`
PATCH /v1/appScreenshots/{id}
{
"data": {
"type": "appScreenshots",
"id": "{id}",
"attributes": {
"uploaded": true
}
}
}
`
This process requires handling multipart uploads and checksums.
Tools and Libraries
Official Resources
- [App Store Connect API Documentation](https://developer.apple.com/documentation/appstoreconnectapi)
- [Apple's Sample Code](https://developer.apple.com/sample-code/)
Community Libraries
Various open-source libraries wrap the API:
- **Node.js:** app-store-connect-api
- **Python:** app-store-connect-api
- **Ruby:** spaceship (via fastlane)
Fastlane Alternative
Fastlane's deliver action abstracts much of this complexity:
`
fastlane deliver --metadata_path ./metadata
`
Great for CI/CD pipelines, but requires local setup.
When to Build vs. Buy
Build Your Own Integration When:
- You need custom workflow automation
- You're integrating with existing systems
- You want complete control
Use Existing Tools When:
- You need to move fast
- API complexity is a barrier
- Maintenance overhead matters
Many developers find that dedicated localization tools save more time than they cost.
Security Best Practices
Protect Your Credentials
- Never commit .p8 files to git
- Use environment variables for keys
- Rotate API keys periodically
Use Minimal Permissions
Create API keys with only the permissions needed:
- App Manager for metadata updates
- Developer for read-only access
Audit Access
Review who has API access regularly. Remove keys that aren't in use.
Summary
The App Store Connect API enables powerful automation for localization workflows. Key takeaways:
- **Authentication uses JWT** with ES256 signing
- **App-level and version-level fields** use different endpoints
- **Each locale has a unique ID** you'll use for updates
- **Rate limiting exists**—build in delays
- **Screenshot upload is complex**—consider tools for this
For developers managing multiple apps across many locales, API automation is transformative. Start with metadata updates, then expand to screenshots as you get comfortable.