Skill: ivangdavila/app-store-connect (version 1.0.0)

--- name: App Store Connect slug: app-store-connect version: 1.0.0 homepage: https://clawic.com/skills/app-store-connect description: Manage iOS apps, TestFlight builds, submissions, and analytics via App Store Connect API. metadata: {"clawdbot":{"emoji":"🍎","requires":{"bins":[],"env":["ASC_ISSUER_ID","ASC_KEY_ID","ASC_PRIVATE_KEY_PATH"]},"os":["linux","darwin","win32"]}} --- ## When to Use User needs to manage iOS/macOS apps on App Store Connect. Agent handles API authentication, build management, TestFlight distribution, App Review submissions, and analytics retrieval. ## Quick Reference | Topic | File | |-------|------| | API Authentication | `api-auth.md` | | Common Workflows | `workflows.md` | ## Core Rules ### 1. JWT Authentication Required App Store Connect API uses JWT tokens signed with your private key. ```bash # Required environment variables: # ASC_ISSUER_ID - From App Store Connect > Users > Keys # ASC_KEY_ID - From the API key you created # ASC_PRIVATE_KEY_PATH - Path to your .p8 private key file ``` Generate JWT with ES256 algorithm, 20-minute expiration max. See `api-auth.md` for code examples. ### 2. API Versioning Always specify API version in requests. ```bash curl -H "Authorization: Bearer $JWT" \ "https://api.appstoreconnect.apple.com/v1/apps" ``` Current stable version: `v1`. Check Apple docs for v2 endpoints. ### 3. Build Processing States Builds go through states after upload: | State | Meaning | Action | |-------|---------|--------| | PROCESSING | Upload received, processing | Wait | | FAILED | Processing failed | Check logs | | INVALID | Validation failed | Fix issues, re-upload | | VALID | Ready for testing/submission | Proceed | Never submit a build that is not `VALID`. ### 4. TestFlight Distribution - **Internal Testing**: Up to 100 members, builds available immediately after processing - **External Testing**: Up to 10,000 testers, requires Beta App Review for first build of version - External groups need at least: app description, feedback email, privacy policy URL ### 5. App Review Submission Before submitting for review: - All required metadata complete (descriptions, keywords, screenshots) - App Preview videos under 30 seconds - Privacy policy URL valid and accessible - Contact information current Submission creates an `appStoreVersion` in `PENDING_DEVELOPER_RELEASE` or `WAITING_FOR_REVIEW`. ### 6. Rate Limits API has rate limits per hour. Handle 429 responses with exponential backoff. ```bash # Respect Retry-After header HTTP/1.1 429 Too Many Requests Retry-After: 60 ``` ### 7. Bundle ID Management Bundle IDs are permanent once created. Cannot be deleted or renamed. - Use reverse-domain notation: `com.company.appname` - Plan naming carefully before registration - Each bundle ID can only belong to one team ## Common Traps - **Expired JWT** - Tokens expire in 20 min max. Regenerate before long operations. - **Wrong key permissions** - API keys need specific roles (Admin, App Manager, etc.) - **Missing export compliance** - Apps with encryption need ECCN or exemption documentation - **Build version collision** - Each build needs unique version+build number combo - **Screenshot dimensions** - Must match exactly for each device type (no scaling) - **Phased release confusion** - Phased release is for App Store only, not TestFlight ## External Endpoints | Endpoint | Data Sent | Purpose | |----------|-----------|---------| | api.appstoreconnect.apple.com | App metadata, build info | App Store Connect API | No other data is sent externally. ## Security & Privacy **Data that leaves your machine:** - App metadata sent to Apple for App Store listing - Build information for processing - Analytics queries **Data that stays local:** - API private key (.p8) - never transmit - JWT tokens - generated locally - Downloaded reports **This skill does NOT:** - Store your .p8 key in plain text - Share credentials with third parties - Access apps outside your team ## Related Skills Install with `clawhub install <slug>` if user confirms: - `ios` — iOS development patterns - `swift` — Swift language reference - `xcode` — Xcode IDE workflows ## Feedback - If useful: `clawhub star app-store-connect` - Stay updated: `clawhub sync`

path: api-auth.md
content: # API Authentication — App Store Connect

## Prerequisites

1. **Apple Developer Account** with Admin or App Manager role
2. **API Key** created in App Store Connect > Users and Access > Keys
3. **Private Key** (.p8 file) downloaded when key was created

## Creating an API Key

1. Go to [App Store Connect](https://appstoreconnect.apple.com)
2. Users and Access > Keys > App Store Connect API
3. Click "+" to generate new key
4. Select role (Admin for full access, App Manager for app-specific)
5. Download .p8 file immediately (only available once)
6. Note the Key ID and Issuer ID

## JWT Token Generation

App Store Connect uses JWT (JSON Web Tokens) signed with ES256.

### Token Structure

```json
{
  "alg": "ES256",
  "kid": "YOUR_KEY_ID",
  "typ": "JWT"
}
```

### Payload

```json
{
  "iss": "YOUR_ISSUER_ID",
  "iat": 1234567890,
  "exp": 1234568190,
  "aud": "appstoreconnect-v1"
}
```

- `iat`: Issued at (current Unix timestamp)
- `exp`: Expiration (max 20 minutes from iat)
- `aud`: Always "appstoreconnect-v1"

### Generate with Ruby

```ruby
require 'jwt'
require 'openssl'

# Uses environment variables: ASC_ISSUER_ID, ASC_KEY_ID, ASC_PRIVATE_KEY_PATH
private_key = OpenSSL::PKey::EC.new(File.read(ENV['ASC_PRIVATE_KEY_PATH']))

payload = {
  iss: ENV['ASC_ISSUER_ID'],
  iat: Time.now.to_i,
  exp: Time.now.to_i + 1200, # 20 minutes
  aud: 'appstoreconnect-v1'
}

[REDACTED_SECRET], private_key, 'ES256', { kid: ENV['ASC_KEY_ID'] })
puts token
```

### Generate with Python

```python
import jwt
import time
import os

# Uses environment variables: ASC_ISSUER_ID, ASC_KEY_ID, ASC_PRIVATE_KEY_PATH
with open(os.environ['ASC_PRIVATE_KEY_PATH'], 'r') as f:
    private_key = f.read()

payload = {
    'iss': os.environ['ASC_ISSUER_ID'],
    'iat': int(time.time()),
    'exp': int(time.time()) + 1200,
    'aud': 'appstoreconnect-v1'
}

headers = {
    'kid': os.environ['ASC_KEY_ID'],
    'typ': 'JWT'
}

[REDACTED_SECRET], private_key, algorithm='ES256', headers=headers)
print(token)
```

### Generate with Node.js

```javascript
const jwt = require('jsonwebtoken');
const fs = require('fs');

// Uses environment variables: ASC_ISSUER_ID, ASC_KEY_ID, ASC_PRIVATE_KEY_PATH
const privateKey = fs.readFileSync(process.env.ASC_PRIVATE_KEY_PATH);

const [REDACTED_SECRET]
  iss: process.env.ASC_ISSUER_ID,
  iat: Math.floor(Date.now() / 1000),
  exp: Math.floor(Date.now() / 1000) + 1200,
  aud: 'appstoreconnect-v1'
}, privateKey, {
  algorithm: 'ES256',
  header: { kid: process.env.ASC_KEY_ID, typ: 'JWT' }
});

console.log(token);
```

## Making API Requests

```bash
JWT="your_generated_token"

# List all apps
curl -H "Authorization: Bearer $JWT" \
     "https://api.appstoreconnect.apple.com/v1/apps"

# Get specific app
curl -H "Authorization: Bearer $JWT" \
     "https://api.appstoreconnect.apple.com/v1/apps/YOUR_APP_ID"
```

## Role Permissions

| Role | Apps | Users | Finance | Reports |
|------|------|-------|---------|---------|
| Admin | Full | Full | Full | Full |
| App Manager | Full | None | None | App-level |
| Developer | Read | None | None | None |
| Marketing | Metadata | None | None | App-level |
| Sales | None | None | Read | Full |

## Token Best Practices

1. **Short expiration** - Generate new tokens frequently (every 15-20 min)
2. **Secure storage** - Never commit .p8 file to version control
3. **Environment variables** - Store credentials in env vars, not code
4. **Key rotation** - Rotate API keys periodically
5. **Minimal permissions** - Use role with least required access

## Common Errors

| Error | Cause | Solution |
|-------|-------|----------|
| 401 Unauthorized | Invalid/expired JWT | Regenerate token |
| 403 Forbidden | Insufficient role permissions | Use key with higher role |
| NOT_AUTHORIZED | Key revoked or team access removed | Check key status in ASC |


path: workflows.md
content: # Common Workflows — App Store Connect

## Upload and Submit a Build

### 1. Upload via Xcode or Transporter

```bash
# Using xcrun altool (deprecated but still works)
xcrun altool --upload-app -f MyApp.ipa \
  --type ios \
  --apiKey YOUR_KEY_ID \
  --apiIssuer YOUR_ISSUER_ID

# Using Transporter CLI
/Applications/Transporter.app/Contents/itms/bin/iTMSTransporter \
  -m upload \
  -jwt "$JWT" \
  -f MyApp.ipa
```

### 2. Wait for Processing

```bash
# Check build status
curl -H "Authorization: Bearer $JWT" \
  "https://api.appstoreconnect.apple.com/v1/builds?filter[app]=APP_ID&sort=-uploadedDate&limit=1"
```

Wait until `processingState` is `VALID`.

### 3. Submit for Review

```bash
# Create app store version submission
curl -X POST -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  "https://api.appstoreconnect.apple.com/v1/appStoreVersionSubmissions" \
  -d '{
    "data": {
      "type": "appStoreVersionSubmissions",
      "relationships": {
        "appStoreVersion": {
          "data": { "type": "appStoreVersions", "id": "VERSION_ID" }
        }
      }
    }
  }'
```

## TestFlight Distribution

### Add Build to Internal Testing

Internal testers get access automatically after build processes.

```bash
# Get internal beta groups
curl -H "Authorization: Bearer $JWT" \
  "https://api.appstoreconnect.apple.com/v1/apps/APP_ID/betaGroups?filter[isInternalGroup]=true"
```

### Add Build to External Testing

```bash
# Add build to external beta group
curl -X POST -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  "https://api.appstoreconnect.apple.com/v1/betaGroups/GROUP_ID/relationships/builds" \
  -d '{
    "data": [{ "type": "builds", "id": "BUILD_ID" }]
  }'
```

First build of each version requires Beta App Review (usually 24-48h).

### Add External Testers

```bash
# Create beta tester
curl -X POST -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  "https://api.appstoreconnect.apple.com/v1/betaTesters" \
  -d '{
    "data": {
      "type": "betaTesters",
      "attributes": {
        "email": "[REDACTED_SECRET]",
        "firstName": "Test",
        "lastName": "User"
      },
      "relationships": {
        "betaGroups": {
          "data": [{ "type": "betaGroups", "id": "GROUP_ID" }]
        }
      }
    }
  }'
```

## Update App Metadata

### Update Description

```bash
# Get app store version localization ID
curl -H "Authorization: Bearer $JWT" \
  "https://api.appstoreconnect.apple.com/v1/appStoreVersions/VERSION_ID/appStoreVersionLocalizations"

# Update description
curl -X PATCH -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  "https://api.appstoreconnect.apple.com/v1/appStoreVersionLocalizations/LOCALIZATION_ID" \
  -d '{
    "data": {
      "type": "appStoreVersionLocalizations",
      "id": "LOCALIZATION_ID",
      "attributes": {
        "description": "Your new description here"
      }
    }
  }'
```

### Update Keywords

```bash
curl -X PATCH -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  "https://api.appstoreconnect.apple.com/v1/appStoreVersionLocalizations/LOCALIZATION_ID" \
  -d '{
    "data": {
      "type": "appStoreVersionLocalizations",
      "id": "LOCALIZATION_ID",
      "attributes": {
        "keywords": "keyword1,keyword2,keyword3"
      }
    }
  }'
```

## Download Sales Reports

```bash
# Get sales report
curl -H "Authorization: Bearer $JWT" \
  -H "Accept: application/a]gzip" \
  "https://api.appstoreconnect.apple.com/v1/salesReports?\
filter[reportType]=SALES&\
filter[reportSubType]=SUMMARY&\
filter[frequency]=DAILY&\
filter[vendorNumber]=YOUR_VENDOR_NUMBER&\
filter[reportDate]=2024-01-15" \
  --output report.csv.gz

gunzip report.csv.gz
```

## Check App Review Status

```bash
# Get app store version
curl -H "Authorization: Bearer $JWT" \
  "https://api.appstoreconnect.apple.com/v1/apps/APP_ID/appStoreVersions?filter[appStoreState]=PENDING_DEVELOPER_RELEASE,WAITING_FOR_REVIEW,IN_REVIEW"
```

### App Store States

| State | Meaning |
|-------|---------|
| PREPARE_FOR_SUBMISSION | Editable, not submitted |
| WAITING_FOR_REVIEW | Submitted, in queue |
| IN_REVIEW | Being reviewed |
| PENDING_DEVELOPER_RELEASE | Approved, waiting for manual release |
| READY_FOR_SALE | Live on App Store |
| REJECTED | Review failed |

## Create New App

```bash
# Register new bundle ID first
curl -X POST -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  "https://api.appstoreconnect.apple.com/v1/bundleIds" \
  -d '{
    "data": {
      "type": "bundleIds",
      "attributes": {
        "identifier": "com.company.newapp",
        "name": "New App Bundle",
        "platform": "IOS"
      }
    }
  }'

# Create app
curl -X POST -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  "https://api.appstoreconnect.apple.com/v1/apps" \
  -d '{
    "data": {
      "type": "apps",
      "attributes": {
        "name": "My New App",
        "primaryLocale": "en-US",
        "sku": "MYAPP001"
      },
      "relationships": {
        "bundleId": {
          "data": { "type": "bundleIds", "id": "BUNDLE_ID" }
        }
      }
    }
  }'
```

## Release Management

### Enable Phased Release

```bash
curl -X PATCH -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  "https://api.appstoreconnect.apple.com/v1/appStoreVersionPhasedReleases/PHASED_RELEASE_ID" \
  -d '{
    "data": {
      "type": "appStoreVersionPhasedReleases",
      "id": "PHASED_RELEASE_ID",
      "attributes": {
        "phasedReleaseState": "ACTIVE"
      }
    }
  }'
```

### Manual Release After Approval

```bash
curl -X PATCH -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  "https://api.appstoreconnect.apple.com/v1/appStoreVersions/VERSION_ID" \
  -d '{
    "data": {
      "type": "appStoreVersions",
      "id": "VERSION_ID",
      "attributes": {
        "releaseType": "MANUAL"
      }
    }
  }'
```


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)