Skip to content

How to automate custom product pages with the App Store Connect API

Yes. The App Store Connect API has had endpoints for custom product pages since version 1.7, and for assigning search keywords to them since 4.0 and 4.1. A script creates the page with a version and a first localization in one request, adds localizations, uploads screenshots and previews, sets promotional text, assigns keyword ids, then submits the version for review. It is a few hundred lines of code and worth it for many apps; for one app and a few pages, a tool is simpler.

What do you need to know first?

Seven facts shape every script. The resources, the key, the token, and the rate limit.

The App Store Connect API for custom product pages, in brief
Page resourcesappCustomProductPages, appCustomProductPageVersions, appCustomProductPageLocalizations (API 1.7)1
Media resourcesappScreenshotSets, appScreenshots, appPreviewSets, appPreviews. Deprecated in 4.5.1 in favour of appAssetLibraryImages, appAssetLibraryVideos and appAssetLibraryPlacements; still documented, no removal date given13
Keyword resourcesappKeywords, listed by GET /v1/apps/{id}/searchKeywords (API 4.0) and assigned through the localization’s searchKeywords relationship (API 4.1)6
Review resourcesreviewSubmissions and reviewSubmissionItems, with an item relationship named appCustomProductPageVersion8
Key roleAccount Holder, Admin, App Manager or Marketing for creating and submitting pages; Apple applies user roles to keys. App Manager is the usual choice15
TokenA JWT signed with ES256, aud appstoreconnect-v1, at most 20 minutes from iat to exp, sent as a Bearer header10
Rate limitPer key, per rolling hour. Every response carries X-Rate-Limit: user-hour-lim:3500;user-hour-rem:500; in Apple’s example. Exceed it and you get 429 RATE_LIMIT_EXCEEDED9
Pages per app7011

What are the endpoints?

Eight groups of calls cover the whole job. All are under https://api.appstoreconnect.apple.com and take JSON:API bodies.

Endpoints for custom product pages, App Store Connect API 4.5
StepMethod and pathNotes
Create the pagePOST /v1/appCustomProductPagesname and the app relationship are required. Apple’s example creates the version and first localization inline in an included array2
List pagesGET /v1/apps/{id}/appCustomProductPagesSupports filter[visible] and include=appCustomProductPageVersions
Add a localizationPOST /v1/appCustomProductPageLocalizationslocale required, promotionalText optional, relationship appCustomProductPageVersion3
Set promotional textPATCH /v1/appCustomProductPageLocalizations/{id}promotionalText is the only writable attribute on update. 170 characters
Create a screenshot setPOST /v1/appScreenshotSetsscreenshotDisplayType plus the appCustomProductPageLocalization relationship. Deprecated in 4.5.1; see the asset library4
Reserve, upload, commit a screenshotPOST /v1/appScreenshots, PUT to each upload URL, PATCH /v1/appScreenshots/{id}Reserve with fileName and fileSize; commit with uploaded true and the MD5 sourceFileChecksum5
Order screenshotsPATCH /v1/appScreenshotSets/{id}/relationships/appScreenshotsReplaces the ordered list
App previewsPOST /v1/appPreviewSets, POST /v1/appPreviews, same reserve, upload, commitAttached to the localization the same way as screenshots
List the app’s keywordsGET /v1/apps/{id}/searchKeywordsfilter[locale], filter[platform], limit up to 200. Returns appKeywords with an id and no attributes7
Assign keywordsPOST /v1/appCustomProductPageLocalizations/{id}/relationships/searchKeywordsBody is data: [{ type: "appKeywords", id }]. 204 on success. DELETE with the same body removes. There is no PATCH6
Submit for reviewPOST /v1/reviewSubmissions, POST /v1/reviewSubmissionItems, PATCH /v1/reviewSubmissions/{id}The item’s relationship is appCustomProductPageVersion. Set submitted true to send, canceled true to withdraw8
Asset library (new)POST /v1/appAssetLibraryImages, POST /v1/appAssetLibraryPlacementsUpload once, then place the image on a custom product page localization. The replacement for screenshot sets since 4.5.113

In what order do you call them?

Page, localizations, media, text, keywords, review. Each step needs an id from the one before, so record every id Apple returns before you move on.

  1. Create the page, with its version and first localization inline.One POST to /v1/appCustomProductPages with name, the app relationship, and an included array holding one appCustomProductPageVersions object and one appCustomProductPageLocalizations object with its locale. Apple’s own example does it this way, and a secondary source reports the server rejects a page without them. The response gives you the page id, the version id and the localization id.
  2. Add a localization for each further locale.POST /v1/appCustomProductPageLocalizations with locale and the appCustomProductPageVersion relationship. Read the version’s existing localizations first so a retry does not make duplicates.
  3. Create a screenshot set per display type, on each localization.POST /v1/appScreenshotSets with screenshotDisplayType (APP_IPHONE_67 is the 6.9 inch set; there is no APP_IPHONE_69 value) and the appCustomProductPageLocalization relationship.
  4. Reserve each screenshot.POST /v1/appScreenshots with fileName, fileSize and the appScreenshotSet relationship. The response holds uploadOperations: one or more PUT URLs, each with an offset, a length and request headers.
  5. Upload the parts to the returned URLs.Send the bytes from offset for length to each URL with the headers given. The URLs are unauthenticated and time-limited; no JWT. Parts can go concurrently and in any order.
  6. Commit with the checksum.PATCH /v1/appScreenshots/{id} with uploaded true and sourceFileChecksum, the MD5 of the whole file. Then poll GET /v1/appScreenshots/{id} until assetDeliveryState.state is COMPLETE. FAILED is terminal: delete and reserve again.
  7. Add app previews the same way.POST /v1/appPreviewSets on the localization, then reserve, upload and commit each video through /v1/appPreviews. Up to three per device size.
  8. Set the promotional text.PATCH /v1/appCustomProductPageLocalizations/{id} with promotionalText, 170 characters at most, per locale.
  9. Assign keywords.GET /v1/apps/{id}/searchKeywords with filter[locale] to read the ids, then POST them to /v1/appCustomProductPageLocalizations/{id}/relationships/searchKeywords as { type: "appKeywords", id }. Only keywords from the latest approved app version are eligible.
  10. Submit for review.POST /v1/reviewSubmissions for the app, POST /v1/reviewSubmissionItems with the appCustomProductPageVersion relationship, then PATCH the submission with submitted true. Poll the version’s state: WAITING_FOR_REVIEW, IN_REVIEW, then APPROVED or REJECTED.

Apple also documents a template shortcut: on the create call you can pass appStoreVersionTemplate or customProductPageTemplate to copy an existing version or page, which saves the media steps when pages share most of their screenshots.2

What can’t be changed after creation?

Two things: a localization’s locale, and anything on a version while it is under review. On update, a page accepts only name and visible, a version only deepLink, and a localization only promotionalText; locale is fixed once the localization exists, so a wrong locale means delete and recreate.1 While a version is under review you cannot modify its screenshots, previews, promotional text or keywords; withdrawing the submission makes the page editable again.11 An approved page can be edited and resubmitted without changing its URL, and the new version replaces the old one when approved.12

What are the gotchas?

Five things trip up most first scripts.

  • Keyword ids are opaque. An appKeywords resource has an id and no attributes. Apple does not document whether the id is the keyword text or a generated value, so never build one: read the pool from the app’s searchKeywords list and pass those ids through unchanged.7
  • Each page needs a unique set of keywords. Apple tells you to select a unique set per page so the most relevant page is shown; whether the API enforces this or only recommends it is not documented.11 The search guide covers how to split keywords across pages.
  • 70 pages per app. Count existing pages before creating; a deleted page is gone for good and its link falls back to the default page.11
  • Screenshot sizes must match the display type exactly. A 6.9 inch set takes 1320 x 2868, 1290 x 2796 or 1260 x 2736 portrait pixels, or their landscape forms, with no alpha channel. The full list is in the requirements guide. If the bytes received differ from the reserved fileSize, the commit fails.5
  • Idempotency: record Apple’s ids before the next step. A page create that times out may have succeeded. List the app’s pages by name, the version’s localizations by locale, and the localization’s screenshot sets by display type before creating any of them, so a rerun picks up where it stopped instead of making a second copy.

Two more worth planning for: Apple allows one items-only submission under review per platform, so batch several pages as items in one submission rather than one submission each.12 And the screenshot set endpoints are now deprecated in favour of the asset library, which uploads an image once and places it on any localization; new scripts should read the migration page before choosing a path.14

What does the first call look like?

A signed token and one GET. This TypeScript uses the jose library for the ES256 signature and lists the app’s pages, printing the rate-limit header so you can see your budget.

import { SignJWT, importPKCS8 } from 'jose'

const key = await importPKCS8(process.env.ASC_PRIVATE_KEY!, 'ES256')
const token = await new SignJWT({ aud: 'appstoreconnect-v1' })
  .setProtectedHeader({ alg: 'ES256', kid: process.env.ASC_KEY_ID!, typ: 'JWT' })
  .setIssuer(process.env.ASC_ISSUER_ID!)
  .setIssuedAt()
  .setExpirationTime('19m')
  .sign(key)

const url = 'https://api.appstoreconnect.apple.com/v1/apps/APP_ID/appCustomProductPages'
const res = await fetch(`${url}?include=appCustomProductPageVersions`, {
  headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`)
const { data } = await res.json()
console.log(res.headers.get('x-rate-limit'))
for (const page of data) console.log(page.id, page.attributes.name, page.attributes.visible)

Reuse the token across calls until it expires; Apple recommends it. Omit the scope claim for write calls, and keep the lifetime under 20 minutes.10

When is a tool simpler?

When you have one app, or no pipeline that produces screenshots. The API uploads what you give it; the work before that, choosing which pages to make and designing their screenshots in every size, is the larger part, and a script does none of it. The script is also yours to maintain each time Apple moves an endpoint, as it did with the asset library in 4.5.1.

If you have several apps and an engineer, the script pays for itself. The tools comparison sets the API beside the other three options, and the complete guide covers what a page is and what Apple reviews.

Sources

  1. 1Apple, App Store Connect API: custom product pages and localizations
  2. 2Apple, Create a custom product page
  3. 3Apple, Create a custom product page localization
  4. 4Apple, Create an app screenshot set
  5. 5Apple, Uploading assets to App Store Connect
  6. 6Apple, Add a search keyword to a custom product page localization
  7. 7Apple, List search keywords for an app
  8. 8Apple, Create a review submission item
  9. 9Apple, Identifying rate limits
  10. 10Apple, Generating tokens for API requests
  11. 11Apple, Configure multiple product page versions
  12. 12Apple, Submit a custom product page
  13. 13Apple, App Store Connect API 4.5.1 release notes
  14. 14Apple, Migrating to the App Asset Library
  15. 15Apple, Role permissions

Questions people ask

Which App Store Connect API version added custom product pages?
The page, version and localization resources arrived in API 1.7. Keyword assignment came later: the app keyword list in 4.0 and the custom product page keyword endpoints in 4.1. The asset library that replaces the screenshot set endpoints arrived in 4.5.1.
Can I submit a custom product page without a new app version?
Yes, once the app has been approved. Create a review submission for the app, add an item whose appCustomProductPageVersion relationship points at your page version, and set submitted to true. If the app has never been approved, the page must go in the same submission as the first app version.
Does fastlane support custom product pages?
Not as a built-in action at the time of writing, which is why most teams write their own calls. fastlane’s deliver code is still a useful reference for which screenshot sizes map to which screenshotDisplayType value.
Can one script change a page that is live?
Yes. An approved page keeps its URL. Edit its version, resubmit, and the new version replaces the old one when Apple approves it. You cannot change screenshots, previews, promotional text or keywords while a version is under review; withdraw the submission first.
What role does the API key need?
Apple lists Account Holder, Admin, App Manager and Marketing for creating and submitting custom product pages in App Store Connect, and applies the same roles to API keys. App Manager is the usual choice. Note that App Manager keys cannot read App Analytics, which needs Admin to request reports.
Try it

See your first page.

Look up your app. In about a minute you see three pages planned for it and one designed on your own screenshots, before you sign up.