What the command actually does
eas submit is a delivery mechanism, nothing more. It takes a finished .ipa or .aab, authenticates against the store, and uploads it. It does not compile, it does not fill in your store listing, and — the part that surprises people — it does not submit anything for review. A successful eas submitmeans “the binary is now sitting in App Store Connect.” You still open the console and press the buttons.
# The 90% case: build, then send the newest one eas build --platform ios --profile production eas submit --platform ios --latest # Or chain them eas build --platform all --profile production --auto-submit # Send a specific older build eas submit --platform android --id 8f3c1e42-... # Send a binary you built elsewhere eas submit --platform android --path ./app-release.aab
The flags
| Flag | What it does | When it matters |
|---|---|---|
--platform ios | android | all | Which store to submit to | Required in --non-interactive |
--latest | Submit the most recent finished build | The everyday flag |
--id <build-id> | Submit one specific EAS build | Use when re-submitting an older build |
--path <file> | Submit a local .ipa / .aab | For binaries not built on EAS |
--url <url> | Submit a remotely hosted binary | Rare; useful for external CI |
--profile <name> | Pick a submit profile from eas.json | Defaults to "production" |
--non-interactive | Never prompt; fail instead | Mandatory in CI |
--wait / --no-wait | Block until the store finishes processing | Waiting surfaces errors in the same log |
--verbose | Full transport output | First thing to add when a submit fails opaquely |
--waitis the underrated one. Without it the command exits as soon as the upload finishes, and Apple’s processing errors arrive by email twenty minutes later instead of in your terminal. In CI, waiting is what makes a red build actually red.
iOS credentials: use the API key
You can authenticate with your Apple ID, and for a first manual submission that’s fine — EAS will prompt and store it. But an Apple ID needs two-factor, which means it can never work unattended. An App Store Connect API key can, and it’s revocable without touching your account password. Generate one in App Store Connect → Users and Access → Integrations → App Store Connect API, role App Manager. The .p8 file downloads exactly once.
// eas.json
{
"build": {
"production": { "distribution": "store", "autoIncrement": true }
},
"submit": {
"production": {
"ios": {
"appleId": "you@example.com",
"ascAppId": "6478123456", // numeric App Store Connect app ID
"appleTeamId": "AB12CD34EF",
"ascApiKeyPath": "./secrets/AuthKey_ABC123.p8",
"ascApiKeyIssuerId": "69a6de70-...",
"ascApiKeyId": "ABC123"
},
"android": {
"serviceAccountKeyPath": "./secrets/play-service-account.json",
"track": "internal", // internal | alpha | beta | production
"releaseStatus": "draft"
}
}
}
}Do not commit those two secret files. Add secrets/ to .gitignore and in CI supply them as base64 environment variables written to disk at the start of the job, or reference EXPO_ASC_API_KEY_PATH and GOOGLE_SERVICE_ACCOUNT_KEY_PATH. A leaked .p8 is publish access to your App Store account.
Android: the first release must be manual
This one costs everybody an evening. Google Play refuses API uploads for a package name that has never had a release. So the sequence for a brand-new Android app is:
eas build --platform android --profile production, then download the AAB.- Create the app in Play Console and upload that AAB by hand to the internal testing track. Roll it out.
- Create a service account (Google Cloud → IAM), grant it access in Play Console under Users and permissions with at least Release to testing tracks, and download the JSON.
- Every release after that:
eas submit --platform android --latest.
The permission grant propagates slowly — a 403 immediately after granting access often resolves itself in ten minutes. Retry once before you go re-reading IAM docs.
Running it unattended
# .github/workflows/release.yml (excerpt)
- run: npm ci
- run: npx eas-cli@latest build --platform all --profile production \
--non-interactive --no-wait
- run: npx eas-cli@latest submit --platform all --profile production \
--latest --non-interactive --wait
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}Three rules for CI: authenticate with EXPO_TOKEN (a robot access token from expo.dev, not your password), always pass --non-interactive so a missing credential fails loudly instead of hanging on a prompt, and set autoIncrement: true in the build profile so you never lose a run to a duplicate build number.
Error decoder
| Error | Real cause | Fix |
|---|---|---|
| ITMS-90283 / invalid provisioning profile | Built with a dev or ad-hoc profile | Rebuild with distribution: "store" |
| ITMS-90189 / redundant binary upload | That version + build number already exists | Bump buildNumber, or use autoIncrement in eas.json |
| ITMS-91053 missing API declaration | A dependency touches a required-reason API | Add PrivacyInfo.xcprivacy entries, then rebuild |
| Google: "APK/AAB not found" | No manual first release for this package name | Upload the first AAB by hand in Play Console |
| Google: 403 from the service account | Missing release permission or unlinked API access | Grant "Release to testing tracks" and re-link in Play Console |
| Apple: "two-factor required" | Apple ID auth used in a non-interactive run | Switch to an App Store Connect API key |
The pattern across almost all of them: a failed submit is rarely fixable by resubmitting. If the binary is wrong — wrong signature, duplicate build number, missing privacy manifest — you need a new build, not another upload. Change the config, rebuild, then submit.
Getting to the point where this is your only problem
Everything above assumes you have an Expo app worth submitting. ShipNative generates that part — a real React Native app from a description, previewable on your phone, exported as a standard Expo project with an eas.jsonalready in it. The submission commands on this page are the same ones you’ll run. Before you get here, walk the submission checklist — most failed submits are caught there.