Safari
Safari extensions ship through the App Store inside a wrapper app, which the platform assembles, signs and uploads for you. Reach for this page when a project should reach App Store Connect, App Review and the App Store from a build on extension.dev, and when you need to know which steps Apple still leaves to you.
Chrome, Edge and Firefox follow a shorter path, see Publishing. Safari is the one store where a build also needs an app record, signing assets and a macOS runner, so this page carries those legs.
Quick start
- In the console, open Project settings, Stores, Safari. Paste the App Store Connect key (key ID, issuer ID and the
.p8content), save the bundle ID, and run Mint signing assets. The platform registers both bundle IDs, mints the two certificates and the two profiles, and stores the six secrets on the project. - Create the app record once in App Store Connect (New App, platform macOS). The card shows the exact values to type. Apple has no API for this step. Then connect the store on the card, which finds the record by bundle ID and saves the app's id and name on the project.
- Add a
## Safarisection toSTORE.mdand sync the listing. From an agent,extension_submitwithaction: "sync-listing"andbrowsers: ["safari"]pushes it to App Store Connect after an owner approves the change. - Submit a build. The default stops at a TestFlight draft; ask for review on the run:
POST /api/projects/{id}/stores/safari/submissions
{ "buildId": "<sha>", "submitForReview": true }The run signs the wrapper app on a macOS runner, uploads the installer to App Store Connect, waits for TestFlight to process it, and sends the App Store version holding that build to App Review. The version is held for a manual release unless STORE.md says otherwise, so Apple's approval puts nothing on sale on its own.
How it works
One paid path
Safari and the App Store are one paid feature, builds and submissions together. The build runs on Linux like the other three, but the submit job needs a macOS runner, which bills ten times a Linux minute. On the free plan a Safari build and an App Store submission are both refused 403 with MACOS_RUNNER_NOT_INCLUDED before anything is dispatched, and no allowance is spent.
A paid workspace carries 15 App Store submissions a month. The count clears when the month rolls. It is a sub-limit of the monthly build allowance, so one submission spends one from each, and the sixteenth in a month answers 403 with SAFARI_SUBMISSIONS_EXHAUSTED. Building and packaging a Safari extension on your own machine with the extension CLI is free. See extension.dev/pricing and promote gates.
What runs today
| Leg | State |
|---|---|
| Safari as a build target | Offered on the paid plan. Project settings, Build carries a Safari row beside Chrome, Edge and Firefox, closed while the workspace is on the free plan. |
| The Safari payload | Built on Linux beside the other three. The build emits the web extension payload only; the Mac app that wraps it is assembled at submit time. |
| Signing assets | Minted through the App Store Connect API from the console card: both bundle IDs registered, the Mac App Distribution and Mac Installer Distribution certificates, both Mac App Store profiles, six secrets written to the project. Pasting your own exports still works. |
| The app record | Created by you once on App Store Connect's site. The card gives the values; connecting the store records the app's id and name. |
| The listing | Pushed to App Store Connect by the listing sync: name, subtitle, description, keywords, URLs, category, review contact and notes, age rating, content rights, screenshots, free price and worldwide availability. Fields Apple refuses are recorded with Apple's words. |
| Signing and packaging | Done on a macOS runner in a keychain that exists only for the run. |
| Upload to App Store Connect | Done. The installer uploads, TestFlight processes it, and the submission records the build number Apple holds. |
| App Review | Sent by the run when you ask for review, or by the publish act on an uploaded build. The version holding the build is created when none does. |
| Release | Its own approval. A version Apple approved for manual release waits as approved until you release it from the submission or in App Store Connect. |
Safari produces no launcher. Executables covers Chrome, Edge and Firefox only.
Signing assets
The App Store Connect API authenticates with an ES256 JWT signed from a .p8 key. Signing the package needs two certificates and two provisioning profiles on top of that key.
Mint signing assets on the Safari card does that work with the saved key: it registers the app and extension bundle IDs with your team (an ID that already exists is kept), mints both certificates from signing requests the platform keeps, creates both Mac App Store profiles, and writes the six secrets to the project. The card then reads Minted with the expiry date. Nothing is ever revoked for you; a certificate you want gone, you revoke in your developer account.
The six manual fields remain for assets you export yourself. Export both .p12 files in the legacy PKCS12 format. Keychain Access exports that format by default. A .p12 written by OpenSSL 3 with its own defaults is refused when the runner imports it.
The app record
Apple's API cannot create an app. The Safari card tells you to create it once in App Store Connect with the bundle ID you saved, a name within Apple's 30 characters (App Store names are global and may be taken), the primary language and the SKU. Connecting the store then finds the record by bundle ID and stores its numeric id and name, which the card shows as App Store record. The App Privacy answers are the other step that stays yours in App Store Connect.
The listing
The listing sync reads your STORE.md and writes App Store Connect. The shared ## Listing section gives the name, description, URLs and category. The ## Safari section adds what Apple alone asks for:
Line or heading under ## Safari | What it feeds |
|---|---|
- App Store name: | The name within Apple's 30 characters that the preflight's safari.name check reads; a ## Listing name over 30 characters leaves the record's own name in place |
- Release: | manual (the default when absent) or after approval; see Release type |
- Review contact name:, - Review contact email:, - Review contact phone: | The App Review contact; Apple requires a phone number before review |
### Reviewer notes | The notes App Review reads |
### Release notes | What's new for the version |
### Age rating | Bullets naming an attribute with NONE, INFREQUENT_OR_MILD, FREQUENT_OR_INTENSE, yes or no |
- Uses non-exempt encryption: yes | Leaves the export compliance key out of the app, so Apple asks you |
The sync writes text first, then the category, the review details, the age rating and the content rights declaration, then the screenshots, the free price and worldwide availability. A field Apple refuses is recorded with Apple's words and the rest still lands; only a run that wrote nothing reports failed. The preflight reads that record, so a listing check passes only after a sync that reached App Store Connect.
Versions
Two version values go with an upload, and the platform derives both:
| Value | Meaning |
|---|---|
| Short version | CFBundleShortVersionString, taken from the build's manifest version |
| Bundle version | CFBundleVersion, the higher of the number the platform reserved and one above the highest build App Store Connect already holds for the app |
The submission records the bundle version that was uploaded, never the reservation, so the TestFlight row and the platform record name the same build. Apple stops at 65535; a run that would pass it uploads nothing.
Review
A Safari submission stops at a TestFlight draft by default. The store carries three submission modes:
| Mode | What a submit does |
|---|---|
draft | Uploads to App Store Connect and stops at the TestFlight build. The default. |
review | Uploads and sends the App Store version holding that build to App Review under the same approval. Turning it on is an audience change a workspace owner approves. |
dry-run | Sends nothing to Apple. |
A single run can also ask for review with submitForReview: true on the submission call. Sending submitForReview to any other store is refused with 400 REVIEW_INTENT_NOT_SUPPORTED. Asking for review makes the readiness checks binding: an unmet check answers 409 REVIEW_READINESS_UNMET and names every check that failed, and nothing is dispatched.
An uploaded build can be sent to review later with the publish act on the submission. Apple takes one review submission per app at a time, so while a version is in review the act is locked, and the approval sentence says which version holds the queue. Removing a version from review is the cancel act; Apple records it as DEVELOPER_REJECTED, the build stays on TestFlight, and the version can take a new build.
Apple review cannot be withdrawn by the platform once Apple answers. That is why the review intent is stated per run or by a saved mode an owner approved, never inferred.
Release type
App Store Connect gives a version a release type: MANUAL holds an approved version until you release it, AFTER_APPROVAL puts it on sale the moment Apple approves, and an approval cannot be taken back from here.
The platform defaults to manual release. A version the platform creates is MANUAL. Before a review submission, a version that reads AFTER_APPROVAL is set to MANUAL unless the synced listing carries - Release: after approval from STORE.md; the publish record names the change, or Apple's refusal of it in Apple's words. The readiness check store.safari_release_type warns, and never blocks, when the version Apple holds would publish on approval and STORE.md does not ask for that. The preflight, the approval sentence and extension_release_status all name the type Apple last answered and what approval does to it.
Release
A version Apple approved under manual release reads approved on the Releases page with Apple's word PENDING_DEVELOPER_RELEASE. The release act on the submission puts it on sale; it is its own approval, and the sentence says the version cannot be pulled back by the platform afterward. A version set to release after approval reads live as soon as Apple approves.
Watching
The submission states are the eight on Publishing. The store release row for the version moves uploaded, in-review, approved and live, or rejected and withdrawn, as Store releases describes.
Reference
Credentials
| Value | Secret | Notes |
|---|---|---|
| App Store Connect key ID | STORE_SAFARI_ASC_KEY_ID | Identifies the .p8 key |
| Issuer ID | STORE_SAFARI_ASC_ISSUER_ID | The API issuer from App Store Connect |
| Key, base64 | STORE_SAFARI_ASC_KEY_BASE64 | The .p8 key content |
App certificate .p12 | STORE_SAFARI_APP_CERT_P12_BASE64 | Signs the wrapper app, base64 |
| App certificate password | STORE_SAFARI_APP_CERT_PASSWORD | Unlocks that .p12 |
Installer certificate .p12 | STORE_SAFARI_INSTALLER_CERT_P12_BASE64 | Signs the .pkg, base64 |
| Installer certificate password | STORE_SAFARI_INSTALLER_CERT_PASSWORD | Unlocks that .p12 |
| Provisioning profile | STORE_SAFARI_PROVISIONING_PROFILE_BASE64 | For the wrapper app, base64 |
| Appex provisioning profile | STORE_SAFARI_APPEX_PROVISIONING_PROFILE_BASE64 | For the extension appex, base64 |
The first three are the key you paste. The mint writes the other six. The app's numeric Apple id resolves from the bundle ID when it is not given.
Acts
| Act | Where | Approval |
|---|---|---|
| Mint signing assets | The Safari card in the console | Owner, on the card |
| Sync listing | extension_submit sync-listing, or the listing route | Owner, per store change |
| Submit | The submissions route, the console, extension_submit | Owner, per build; in review mode the same approval carries the send to App Review |
| Publish (send to App Review) | The publish route on an uploaded submission | Owner; the sentence names the release type and the hold |
| Cancel (remove from review) | The cancel route on the submission | Owner |
| Release | The release route on an approved submission | Owner; irreversible once Apple releases |
Readiness checks
| Check | What it does |
|---|---|
store.safari_app_record | Warns when no App Store id is saved |
store.safari_app_record_resolved | Blocks when no app record matches the bundle ID |
store.safari_bundle_identifiers | Blocks when the bundle ID is not a valid reverse-DNS identifier |
store.safari_signing_assets | Blocks when the certificates or profiles are missing or expired; warns inside thirty days of expiry |
store.safari_app_review_lock | Locks the publish act while a version of the app is in App Review |
store.safari_build_number_floor | Blocks when the next bundle version would pass Apple's ceiling |
store.safari_release_type | Warns when approval would publish the version and STORE.md does not ask for that; never blocks |
listing.safari_* | Description, category, support URL, screenshots and their dimensions, and the App Store declarations, read from the synced record |
Refusals
| Code | Status | Meaning |
|---|---|---|
MACOS_RUNNER_NOT_INCLUDED | 403 | A Safari build or submission on the free plan |
SAFARI_SUBMISSIONS_EXHAUSTED | 403 | The sixteenth App Store submission of the month |
REVIEW_INTENT_NOT_SUPPORTED | 400 | submitForReview sent to a store other than Safari |
REVIEW_READINESS_UNMET | 409 | A run asked for review with a readiness check unmet |
SAFARI_REVIEW_MODE_REQUIRED | 409 | A version approval tried to carry the publish line while the store is in draft mode |
RELEASE_STORE_NOT_SUPPORTED | 400 | A release act on a store other than Safari |
SAFARI_BUILD_NUMBER_INVALID | 400 | A release named a build number that is not a whole number above zero |
SAFARI_NOT_PENDING_RELEASE | 409 | A release act while Apple holds no approved version for the developer to release |
SAFARI_RELEASE_NOT_MANUAL | 409 | A release act on a version Apple releases itself |
RELEASE_IN_FLIGHT | 409 | A release act while an earlier release run is still answering |
What stays manual
- Creating the app record and answering App Privacy in App Store Connect.
- Release notes on an app's first version: Apple takes them only for an update, so the sync records them as owed until the next version.
- An App Store name over 30 characters is left as the record has it; add
- App Store name:under## Safari. - Age rating questions the platform does not know, and a category that maps to no macOS category, are named in the sync record for you to answer in App Store Connect.
- The console has no switch for the
reviewsubmission mode yet; set it from an agent withextension_submitandconfigure-store, which asks a workspace owner to approve the audience change.
When it fails
- The Safari row in build settings will not turn on. The workspace is on the free plan. Raise the plan at extension.dev/pricing.
- The submission cannot find its artifact. The build that commit came from did not include Safari. Enable Safari in Project settings, Build and build the commit again. A free workspace that enables it anyway has the Safari leg dropped from the dispatch, and the other browsers still build.
- The listing sync says no app record exists. Create the app in App Store Connect with the bundle ID you saved, connect the store on the card again, and sync again.
- The listing sync records fields as owed. Read Apple's words beside each field in the preflight. A missing review contact phone, a name over 30 characters and first-version release notes are the usual three.
- The upload is refused as unsigned or badly signed. Run Mint signing assets again, or check that both
.p12files are in the legacy format. - The run asked for review and stopped at a draft. The run fails instead of recording a review round that did not happen. Read the step log on the submission.
- The publish act is locked. A version of this app is in App Review. Wait for Apple, or remove it from review with the cancel act.
- The preflight warns that approval publishes the version. App Store Connect holds the version as
AFTER_APPROVAL. The review act sets it to manual release; to publish on approval by choice, add- Release: after approvalunder## Safariand sync the listing. - Apple approved and nothing is on the App Store. The version is held for manual release and reads
approved. Release it from the submission, or in App Store Connect. - ES256 authentication fails. Key ID, issuer ID and the base64 key must all come from the same App Store Connect key. Connecting the store on the card names which call failed, the key or the record.
Submission outcomes are subject to App Store review and policy.
Next
- Store releases for the row a submission advances
- Publishing overview for the three ways in
- Executables to ship a launcher outside the App Store