The App Store Connect API lets you create and price subscription products in code instead of clicking through the App Store Connect website. You authenticate with a JSON Web Token signed by a private key Apple gives you, create a subscription group, add subscriptions to it, attach a localized name and description, and set a price point for each territory you sell in. Everything the Subscriptions tab in the web UI does has a REST endpoint behind it.
This matters most when you sell in many countries. Setting one subscription's price by hand across 40 territories is an afternoon of clicking; doing it for a monthly and an annual tier is a lost day. The API turns that into a script you run once. This guide walks the whole flow end to end: the API key, the JWT, the subscription group, the subscription, its localizations, and per-territory pricing, including how to change a price later without raising it on people who already subscribed.
On this page
- Why script it instead of clicking
- Step 1: Create an API key
- Step 2: Authenticate with a JWT
- Step 3: Create a subscription group
- Step 4: Create the subscription
- Step 5: Add a localized name and description
- Step 6: Price it in every territory
- Changing a price without punishing existing subscribers
- Gotchas
- FAQ
Why script it instead of clicking
For a single subscription sold at one price, the App Store Connect website is fine. The API earns its keep the moment either number grows.
The first is territories. Apple sells in about 175 App Store regions. If you want purchasing-power-aware pricing rather than a flat currency conversion, that is 175 prices per subscription to set and later maintain, and the web UI sets them one territory at a time.
The second is products. A real subscription app usually ships at least a monthly and an annual tier, often a weekly trial-style tier too, and sometimes a second tier of features. Each is its own product with its own price ladder. The API lets you define all of it as data and apply it in one run, which also means it lives in version control instead of in someone's memory of what they clicked last quarter.
Step 1: Create an API key
API access uses a key you generate once in App Store Connect under Users and Access, Integrations, App Store Connect API. Create a key with the App Manager role (Admin also works) so it can write subscription data.
You get three things:
- Key ID, a short string like
2X9R4HXF34. - Issuer ID, a UUID shared by every key in your account.
- A
.p8private key file you download exactly once. Apple never shows it again, so store it somewhere safe.
Those three values are all you need to sign requests. Treat the .p8 like a password: it is a bearer credential that can change your app's pricing, so keep it out of your repo and out of client-side code.
Step 2: Authenticate with a JWT
Every request carries a short-lived JSON Web Token in the Authorization header. You build it from the three values above, sign it with the ES256 algorithm, and give it a lifetime of at most 20 minutes.
import fs from "node:fs";
import jwt from "jsonwebtoken";
const KEY_ID = process.env.ASC_KEY_ID;
const ISSUER_ID = process.env.ASC_ISSUER_ID;
const privateKey = fs.readFileSync("./AuthKey_2X9R4HXF34.p8", "utf8");
function makeToken() {
const now = Math.floor(Date.now() / 1000);
return jwt.sign(
{
iss: ISSUER_ID,
iat: now,
exp: now + 60 * 15, // 15 minutes, must be <= 20
aud: "appstoreconnect-v1",
},
privateKey,
{
algorithm: "ES256",
header: { alg: "ES256", kid: KEY_ID, typ: "JWT" },
}
);
}The two fields people get wrong are aud, which must be the literal string appstoreconnect-v1, and exp, which Apple rejects if it is more than 20 minutes out. Mint a fresh token per script run and you never think about it again.
Every call in the rest of this guide sends that token:
curl -H "Authorization: Bearer $TOKEN" \
https://api.appstoreconnect.apple.com/v1/appsThe base URL is always https://api.appstoreconnect.apple.com, and the API follows the JSON:API convention: every resource has a type, an id, an attributes object, and a relationships object linking it to other resources.
Step 3: Create a subscription group
Auto-renewable subscriptions live inside a subscription group. A group is a set of subscriptions a user picks between, where only one can be active at a time, monthly versus annual versus a higher tier. You need at least one group before you can create a subscription.
curl -X POST https://api.appstoreconnect.apple.com/v1/subscriptionGroups \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "subscriptionGroups",
"attributes": { "referenceName": "Pro" },
"relationships": {
"app": { "data": { "type": "apps", "id": "6740000000" } }
}
}
}'The referenceName is internal, only you see it. The app relationship id is your app's numeric App Store ID. The response includes the new group's id, which you pass into the next step.
Step 4: Create the subscription
Now the product itself. A subscription has a product ID, a name, a billing period, and a group level that orders it against its siblings.
curl -X POST https://api.appstoreconnect.apple.com/v1/subscriptions \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "subscriptions",
"attributes": {
"name": "Pro Monthly",
"productId": "com.example.app.pro.monthly",
"subscriptionPeriod": "ONE_MONTH",
"familySharable": false,
"groupLevel": 1
},
"relationships": {
"group": {
"data": { "type": "subscriptionGroups", "id": "GROUP_ID" }
}
}
}
}'A few of these attributes are load-bearing:
productIdis the identifier you reference in StoreKit and RevenueCat. It is permanent once the subscription is approved, so name it deliberately. Abundleid.tier.periodconvention reads well.subscriptionPeriodis one ofONE_WEEK,ONE_MONTH,TWO_MONTHS,THREE_MONTHS,SIX_MONTHS, orONE_YEAR.groupLevelranks tiers within the group. Level 1 is the top offering; a cheaper or lower tier gets a higher number. It drives which direction an upgrade or downgrade goes.
Repeat this call for each tier. A monthly and an annual product in the same group is just two POSTs with a shared GROUP_ID.
Step 5: Add a localized name and description
A subscription needs at least one localization before it can be submitted, the display name and description a customer sees on the product page and system subscription sheet.
curl -X POST https://api.appstoreconnect.apple.com/v1/subscriptionLocalizations \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "subscriptionLocalizations",
"attributes": {
"locale": "en-US",
"name": "Pro",
"description": "Unlock everything, billed monthly."
},
"relationships": {
"subscription": {
"data": { "type": "subscriptions", "id": "SUBSCRIPTION_ID" }
}
}
}
}'Add one call per locale you localize for. The name is what appears in the purchase sheet, so keep it short; the long pitch belongs in the paywall inside your app, not here.
Step 6: Price it in every territory
This is the step that makes the API worth using, and the one the web UI makes tedious.
You do not send a raw dollar amount. Apple exposes a fixed ladder of price points per subscription, each one a specific customer price in a specific territory, and you attach the price point you want. First, list the available price points, filtered to a territory, to find the one matching the price you intend to charge:
curl -H "Authorization: Bearer $TOKEN" \
"https://api.appstoreconnect.apple.com/v1/subscriptions/SUBSCRIPTION_ID/pricePoints?filter\[territory\]=USA&limit=200"Each returned price point has a customerPrice, the amount the user pays, and an opaque id. Pick the id for your base price, say 9.99 in the USA, then create a price that links the subscription to that price point:
curl -X POST https://api.appstoreconnect.apple.com/v1/subscriptionPrices \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "subscriptionPrices",
"attributes": { "startDate": null },
"relationships": {
"subscription": {
"data": { "type": "subscriptions", "id": "SUBSCRIPTION_ID" }
},
"subscriptionPricePoint": {
"data": { "type": "subscriptionPricePoints", "id": "PRICE_POINT_ID" }
}
}
}
}'A startDate of null means the price takes effect immediately. The territory is implied by the price point, so you never pass a country code here; the price point already belongs to one region.
To price the other 174 territories without picking each one by hand, use Apple's equalization endpoint. Given your base USA price point, it returns the equivalent price point in every other territory, converted at Apple's own rates:
curl -H "Authorization: Bearer $TOKEN" \
"https://api.appstoreconnect.apple.com/v1/subscriptionPricePoints/USA_PRICE_POINT_ID/equalizations?limit=200"Loop over that list, POST a subscriptionPrices for each, and one subscription is now priced worldwide from a single base number. If you want to depart from a flat conversion, for example charging less in markets with lower purchasing power, you swap the equalized price point for a nearby one on that territory's ladder before you POST. That is exactly the choice a good pricing strategy makes, and it is where most of the revenue-per-market judgment lives.
Changing a price without punishing existing subscribers
Raising a price later is the same subscriptionPrices call with a future startDate, but there is one attribute that decides whether it is a good experience or a churn event.
When you schedule a new price on a live subscription, Apple lets you choose whether existing subscribers move to the new price or keep the one they signed up at. Preserving the current price for existing subscribers means only new customers see the increase, and your loyal users are never surprised by a higher charge. It is almost always the right default for a price rise.
# Schedule a higher price for new subscribers only, starting next month
curl -X POST https://api.appstoreconnect.apple.com/v1/subscriptionPrices \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "subscriptionPrices",
"attributes": {
"startDate": "2026-08-01",
"preserveCurrentPrice": true
},
"relationships": {
"subscription": {
"data": { "type": "subscriptions", "id": "SUBSCRIPTION_ID" }
},
"subscriptionPricePoint": {
"data": { "type": "subscriptionPricePoints", "id": "NEW_PRICE_POINT_ID" }
}
}
}
}'Getting this right by hand across every territory is error-prone, which is exactly why it belongs in a script that applies the same rule everywhere.
Gotchas
A few things that trip people up the first time.
- Price points are per subscription, not global. The same 9.99 USA price point has a different
idon a different subscription. Always list price points from the subscription you are pricing, not from another one. - Pagination is real. Price point and equalization responses are large. Respect the
links.nextcursor and page through; do not assume the first 200 are all of them. - Rate limits exist. The API allows on the order of a few thousand requests per hour. Pricing every territory for several subscriptions can approach that, so batch sensibly and back off on a
429. - The token expires fast. A long pricing run can outlive a 15-minute token. Re-mint the JWT when you are close to the limit rather than at the very end.
- This is App Store Connect, not RevenueCat. Creating the product here does not create the matching entitlement, offering, or packages in RevenueCat. That is a separate set of API calls against RevenueCat, and both sides have to agree for your paywall to work. See adding subscriptions to a SwiftUI app with RevenueCat for the client side.
FAQ
Do I need the App Store Connect API to sell subscriptions?
No. You can create and price everything by hand in the App Store Connect website. The API is a time-saver for the tedious, repetitive parts, mainly pricing across many territories and managing several products, not a requirement for shipping.
Can I set different prices per country with the API?
Yes, and it is the single best reason to use it. Each territory has its own ladder of price points, and you attach whichever one you want per region. Apple's equalization endpoint gives you a converted starting point for all of them from one base price, which you can then adjust market by market.
What is a subscription group and do I need one?
A subscription group is a set of auto-renewable subscriptions where only one can be active at a time, typically your monthly, annual, and any higher tier. Every auto-renewable subscription must belong to a group, so you create the group first, then add subscriptions to it.
How do I change a subscription's price without raising it on current subscribers?
Schedule the new price with a future start date and preserve the current price for existing subscribers, so only new customers pay the higher amount. It is a single attribute on the price you create, applied per territory.
Does creating a subscription in App Store Connect set it up in RevenueCat too?
No. The App Store Connect API and the RevenueCat API are separate. Creating the product in App Store Connect leaves the entitlement, offering, and packages in RevenueCat untouched; you have to create those through RevenueCat's own API and keep the product IDs in sync.
Why does the API use price points instead of letting me type a price?
Apple only allows a fixed set of prices per territory, the price points, so that every price maps cleanly to a tier and a local currency. You choose from the ladder rather than typing an arbitrary number, which is why the flow is list-then-attach rather than a single price field.
Spaceport does this whole flow for you. You set one base price and pick a pricing model, and Spaceport creates the subscription group, the products, and the localizations in App Store Connect, then prices them across 25 of the top markets through the App Store Connect API, and matches the entitlement, offering, and packages in RevenueCat through its API. No JWT to sign, no price points to page through, no keeping two dashboards in sync by hand. From an indie iOS dev, for indie iOS devs.