Skip to content

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 ​

  1. In the console, open Project settings, Stores, Safari. Paste the App Store Connect key (key ID, issuer ID and the .p8 content), 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.
  2. 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.
  3. Add a ## Safari section to STORE.md and sync the listing. From an agent, extension_submit with action: "sync-listing" and browsers: ["safari"] pushes it to App Store Connect after an owner approves the change.
  4. 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 ​

LegState
Safari as a build targetOffered 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 payloadBuilt 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 assetsMinted 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 recordCreated 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 listingPushed 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 packagingDone on a macOS runner in a keychain that exists only for the run.
Upload to App Store ConnectDone. The installer uploads, TestFlight processes it, and the submission records the build number Apple holds.
App ReviewSent 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.
ReleaseIts 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 ## SafariWhat 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 notesThe notes App Review reads
### Release notesWhat's new for the version
### Age ratingBullets naming an attribute with NONE, INFREQUENT_OR_MILD, FREQUENT_OR_INTENSE, yes or no
- Uses non-exempt encryption: yesLeaves 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:

ValueMeaning
Short versionCFBundleShortVersionString, taken from the build's manifest version
Bundle versionCFBundleVersion, 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:

ModeWhat a submit does
draftUploads to App Store Connect and stops at the TestFlight build. The default.
reviewUploads 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-runSends 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 ​

ValueSecretNotes
App Store Connect key IDSTORE_SAFARI_ASC_KEY_IDIdentifies the .p8 key
Issuer IDSTORE_SAFARI_ASC_ISSUER_IDThe API issuer from App Store Connect
Key, base64STORE_SAFARI_ASC_KEY_BASE64The .p8 key content
App certificate .p12STORE_SAFARI_APP_CERT_P12_BASE64Signs the wrapper app, base64
App certificate passwordSTORE_SAFARI_APP_CERT_PASSWORDUnlocks that .p12
Installer certificate .p12STORE_SAFARI_INSTALLER_CERT_P12_BASE64Signs the .pkg, base64
Installer certificate passwordSTORE_SAFARI_INSTALLER_CERT_PASSWORDUnlocks that .p12
Provisioning profileSTORE_SAFARI_PROVISIONING_PROFILE_BASE64For the wrapper app, base64
Appex provisioning profileSTORE_SAFARI_APPEX_PROVISIONING_PROFILE_BASE64For 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 ​

ActWhereApproval
Mint signing assetsThe Safari card in the consoleOwner, on the card
Sync listingextension_submit sync-listing, or the listing routeOwner, per store change
SubmitThe submissions route, the console, extension_submitOwner, 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 submissionOwner; the sentence names the release type and the hold
Cancel (remove from review)The cancel route on the submissionOwner
ReleaseThe release route on an approved submissionOwner; irreversible once Apple releases

Readiness checks ​

CheckWhat it does
store.safari_app_recordWarns when no App Store id is saved
store.safari_app_record_resolvedBlocks when no app record matches the bundle ID
store.safari_bundle_identifiersBlocks when the bundle ID is not a valid reverse-DNS identifier
store.safari_signing_assetsBlocks when the certificates or profiles are missing or expired; warns inside thirty days of expiry
store.safari_app_review_lockLocks the publish act while a version of the app is in App Review
store.safari_build_number_floorBlocks when the next bundle version would pass Apple's ceiling
store.safari_release_typeWarns 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 ​

CodeStatusMeaning
MACOS_RUNNER_NOT_INCLUDED403A Safari build or submission on the free plan
SAFARI_SUBMISSIONS_EXHAUSTED403The sixteenth App Store submission of the month
REVIEW_INTENT_NOT_SUPPORTED400submitForReview sent to a store other than Safari
REVIEW_READINESS_UNMET409A run asked for review with a readiness check unmet
SAFARI_REVIEW_MODE_REQUIRED409A version approval tried to carry the publish line while the store is in draft mode
RELEASE_STORE_NOT_SUPPORTED400A release act on a store other than Safari
SAFARI_BUILD_NUMBER_INVALID400A release named a build number that is not a whole number above zero
SAFARI_NOT_PENDING_RELEASE409A release act while Apple holds no approved version for the developer to release
SAFARI_RELEASE_NOT_MANUAL409A release act on a version Apple releases itself
RELEASE_IN_FLIGHT409A 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 review submission mode yet; set it from an agent with extension_submit and configure-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 .p12 files 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 approval under ## Safari and 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 ​