11.3 — Automated Pricing via App Store Connect API
Opening scenario
Your marketing director Slacks you on a Sunday evening: “Black Friday is in 14 hours. Drop all our annual tiers 40% from midnight Friday to midnight Monday, restore exactly after.” Pre-automation, this is an emergency Monday-morning meeting and a 90-click marathon in App Store Connect with non-zero chance of typos in the date pickers. Post-automation: you run ./scripts/run-sale.sh black-friday-2026, get a Slack confirmation in 30 seconds, sleep peacefully, and the price reverts itself on Tuesday morning while you’re still asleep.
The App Store Connect REST API turns pricing from a manual calendar event into infrastructure.
Context taxonomy
| API endpoint | Purpose | When you use it |
|---|---|---|
GET /v1/appPricePoints | List available price tiers per territory | Discovery — find the tier ID for “$4.99 in USA” |
GET /v1/apps/{id}/appPriceSchedules | Read current/scheduled pricing | Audit, backup before changes |
POST /v2/appPriceSchedules | Create new price schedule (immediate or scheduled) | Apply a sale, schedule a launch price |
GET /v1/subscriptionPricePoints | List subscription tier price points | Discovery for subscriptions |
GET /v1/subscriptions/{id}/prices | Current subscription pricing | Audit subscriptions |
POST /v1/subscriptionPrices | Set new subscription price (with optional preservation for existing subs) | Subscription repricing |
POST /v1/promotionalOffers | Win-back / loyalty offers | Retention automation |
POST /v1/territoryAvailabilities | Add/remove territories | Geographic expansion |
Concept → Why → How → Code
Concept. The App Store Connect API is a full REST surface over pricing operations. Authenticated via ES256 JWT signed with your .p8 private key.
Why. Time-zone-correct sales windows, multi-territory promotions, A/B price experiments, and audit trails are all manual operations in the UI that scale linearly with apps + territories. Automation flattens the cost to one-time scripting work.
Authentication: ASC JWT
# scripts/asc_jwt.py
import jwt # PyJWT
import time
import sys
from pathlib import Path
KEY_ID = "AAAA1111BB"
ISSUER_ID = "69a6de70-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
KEY_PATH = Path("AuthKey_AAAA1111BB.p8")
def make_token() -> str:
private_key = KEY_PATH.read_text()
headers = {"alg": "ES256", "kid": KEY_ID, "typ": "JWT"}
payload = {
"iss": ISSUER_ID,
"iat": int(time.time()),
"exp": int(time.time()) + 1200, # 20 min max
"aud": "appstoreconnect-v1",
}
return jwt.encode(payload, private_key, algorithm="ES256", headers=headers)
if __name__ == "__main__":
print(make_token())
TOKEN=$(python3 scripts/asc_jwt.py)
curl -H "Authorization: Bearer $TOKEN" https://api.appstoreconnect.apple.com/v1/apps
Discovering price points
# Find all USA price points for tier 8 (≈ $7.99)
TOKEN=$(python3 scripts/asc_jwt.py)
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.appstoreconnect.apple.com/v1/appPricePoints?filter[priceTier]=8&filter[territory]=USA" \
| jq '.data[0] | {id, customerPrice: .attributes.customerPrice, proceeds: .attributes.proceeds}'
# Example output:
# {
# "id": "eyJzIjoxNDQ3MDQ1MzcxLCJ0IjoiVVNBIiwicCI6IjgifQ", ← opaque tier point ID
# "customerPrice": "7.99",
# "proceeds": "5.59"
# }
The id is the opaque price-point identifier you pass to scheduling endpoints.
One-shot sale: schedule a price drop today, revert in 7 days
# scripts/run_sale.py
import os
import sys
import requests
from datetime import datetime, timedelta, timezone
from asc_jwt import make_token
APP_ID = "1234567890"
NORMAL_TIER = "10" # $9.99
SALE_TIER = "5" # $4.99
SALE_DAYS = 7
def price_point_id(token: str, tier: str, territory: str = "USA") -> str:
r = requests.get(
"https://api.appstoreconnect.apple.com/v1/appPricePoints",
headers={"Authorization": f"Bearer {token}"},
params={"filter[priceTier]": tier, "filter[territory]": territory},
)
r.raise_for_status()
return r.json()["data"][0]["id"]
def schedule_sale(token: str):
sale_point_id = price_point_id(token, SALE_TIER)
normal_point_id = price_point_id(token, NORMAL_TIER)
now = datetime.now(timezone.utc).replace(microsecond=0)
end_date = (now + timedelta(days=SALE_DAYS)).isoformat()
payload = {
"data": {
"type": "appPriceSchedules",
"relationships": {
"app": {"data": {"type": "apps", "id": APP_ID}},
"baseTerritory":{"data": {"type": "territories", "id": "USA"}},
"manualPrices": {"data": [
{"type": "appPrices", "id": "sale-price"},
{"type": "appPrices", "id": "restore-price"},
]},
},
},
"included": [
{
"type": "appPrices",
"id": "sale-price",
"attributes": {"startDate": None}, # null = immediate
"relationships": {
"appPricePoint": {"data": {"type": "appPricePoints", "id": sale_point_id}},
"territory": {"data": {"type": "territories", "id": "USA"}},
},
},
{
"type": "appPrices",
"id": "restore-price",
"attributes": {"startDate": end_date},
"relationships": {
"appPricePoint": {"data": {"type": "appPricePoints", "id": normal_point_id}},
"territory": {"data": {"type": "territories", "id": "USA"}},
},
},
],
}
r = requests.post(
"https://api.appstoreconnect.apple.com/v2/appPriceSchedules",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json=payload,
)
r.raise_for_status()
return r.json()
if __name__ == "__main__":
token = make_token()
result = schedule_sale(token)
print(f"Sale scheduled until {result['included'][1]['attributes']['startDate']}")
The schedule fires at Apple’s processing pace — usually within minutes. Restore is automatic on the date you specified.
Subscription repricing (preserving existing subscribers)
TOKEN=$(python3 scripts/asc_jwt.py)
SUB_ID=12345678 # subscription product ID
NEW_PRICE_POINT_ID=eyJzI... # discovered via /v1/subscriptionPricePoints
# Reduce price for new subs, preserve current subscribers at old price
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://api.appstoreconnect.apple.com/v1/subscriptionPrices" \
-d @- <<EOF
{
"data": {
"type": "subscriptionPrices",
"attributes": {
"startDate": null,
"preserveCurrentPrice": true
},
"relationships": {
"subscription": { "data": { "type": "subscriptions", "id": "$SUB_ID" } },
"subscriptionPricePoint":{ "data": { "type": "subscriptionPricePoints","id": "$NEW_PRICE_POINT_ID" } },
"territory": { "data": { "type": "territories", "id": "USA" } }
}
}
}
EOF
preserveCurrentPrice: true is critical for downward price moves — without it, existing subs benefit from the lower price (good for users, fine for retention, but reduces revenue from your highest LTV cohort). Set carefully.
For upward price moves, Apple enforces a 30-day notification + opt-in window. The API call schedules the change; Apple handles the user notification automatically.
Fastlane wrapper
If your team already uses Fastlane:
# fastlane/Fastfile
desc "Black Friday sale"
lane :black_friday do
api_key = app_store_connect_api_key(
key_id: ENV["ASC_KEY_ID"],
issuer_id: ENV["ASC_ISSUER_ID"],
key_content: ENV["ASC_KEY_BASE64"],
is_key_content_base64: true,
)
# Fastlane `deliver` action handles price tier setting,
# but for time-bounded sales the REST API directly is more flexible.
sh "python3 scripts/run_sale.py"
slack(message: "🛍️ Black Friday sale live — 50% off until Tuesday")
end
Scheduling via GitHub Actions cron
# .github/workflows/sale.yml
name: Scheduled Sale
on:
schedule:
- cron: '0 7 24 11 *' # Black Friday 2026 (Friday Nov 27, 07:00 UTC)
workflow_dispatch:
jobs:
start-sale:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.12' }
- run: pip install pyjwt requests
- env:
ASC_KEY_BASE64: ${{ secrets.ASC_KEY_BASE64 }}
run: echo "$ASC_KEY_BASE64" | base64 -d > AuthKey_AAAA1111BB.p8
- run: python3 scripts/run_sale.py
In the wild
- 1Password automates pricing across 50+ territories via the API — adjusting tiers quarterly as currencies move.
- Bear, Day One, Drafts all run synchronized Black Friday / Christmas sales via scripted price schedules — same script template, different app IDs.
- Calm and Headspace use the API for win-back promotional offers triggered by lapsed subscriber webhooks (App Store Server Notifications V2).
- App Store Connect’s own UI literally calls these endpoints — your script and Apple’s UI are peers. You see the exact same scheduled changes you’d see in the web UI.
Common misconceptions
- “Pricing changes apply instantly.” They apply once Apple’s pipeline processes the request — usually 5–30 minutes. Schedule a buffer.
- “You can A/B test pricing arbitrarily.” No. App Store Connect has dedicated Custom Product Pages and Price A/B Tests for controlled experiments. Raw price flipping is not an A/B test — it’s just sequential pricing.
- “Free trial length is part of price tier.” No — it’s a separate introductory offer attribute. Different API endpoint (
/v1/subscriptionIntroductoryOffers). - “The API is rate-limited so heavily it’s unusable.” It’s ~50 requests/min, plenty for pricing operations. Bulk territory changes do require batching.
- “Apple notifies users for every price change.” Only for upward subscription changes. Downward and one-time price changes happen silently to existing users.
Seasoned engineer’s take
TIP. Wrap every pricing change script in a dry-run mode that prints what would happen without making the API call. Run dry-run in code review, real mode only after approval.
WARNING. Test sale scripts against a sandbox app first. App Store Connect has no “undo” for a botched price schedule — you have to schedule a corrective change. Tooling errors visible to users are PR incidents.
The deeper insight: pricing automation isn’t just convenience — it’s how you build pricing as a product surface. With CI behind it, pricing experiments become PRs, audit trails become git logs, and incidents are bisectable. Pricing becomes infrastructure rather than a Sunday-evening fire drill.
Interview corner
Junior — “What does the App Store Connect API let you automate?” Pricing, metadata, screenshots, build uploads, TestFlight management, beta testers, sales reports, and subscription/IAP configuration — basically everything in the App Store Connect web UI.
Mid — “How would you script a one-week sale on an in-app purchase?” Two appPrices entries in a single appPriceSchedules payload: a sale-tier price with null startDate (immediate), and the original-tier price with startDate set to one week out. Run from CI on a cron.
Senior — “Design a pricing automation system for a 4-app product line across 30 territories.” Central config: YAML mapping app→territory→base tier with seasonal overrides. CLI tool reads config, diffs against current ASC state, produces a plan. Apply step requires --confirm flag. CI job runs daily in dry-run, emits a Slack alert if drift detected. Sales scheduled via PRs to the config repo; merging triggers the apply job. Audit log = git log.
Red flag — “Our designer manually changes prices in App Store Connect for promos.” That’s a high-risk pattern. Automate, add CI guardrails, and limit manual access to App Store Connect to a small admin group.
Lab preview
Lab 11.2 is exactly this — a Python script that reads current pricing, applies a 7-day sale, and restores afterward, all via the ASC REST API.