アプリを十本ほど並行で運用していた頃、季節のキャンペーン文言を全アプリの説明文に入れる作業に、半日を溶かしたことがあります。App Store Connect の画面を開き、アプリを選び、各言語のタブを切り替え、説明文の決まった位置に一文を足して保存する。これを言語数だけ、アプリ数だけ繰り返します。退屈なだけでなく、必ずどこかで貼り忘れや誤字が混じります。
Rork のような生成ツールでアプリを量産できるようになると、この「公開後の運用」のほうがボトルネックになります。作るのは速くなったのに、育てる作業は手作業のままだからです。今回は、App Store Connect API を使ってストア情報をコードとして管理し、手作業の更新を月一の仕組みに変える設計を共有します。
手作業の更新が破綻する境界
アプリが二、三本なら手作業で十分です。問題は本数が増えたときに、作業量が本数と言語数の掛け算で増えることです。五本のアプリを日英二言語で運用していれば、一回の文言変更で十箇所の編集が発生します。
私の実感では、五本を超えたあたりから手作業は割に合わなくなります。割に合わないのは時間だけではありません。手作業はミスを生み、ミスは審査差し戻しや表示崩れになって、結局もっと時間を奪います。だからこの仕組み化は、効率化というより事故防止の投資だと考えています。
App Store Connect API の最初の壁は認証
このAPIで最初につまずくのは、ほぼ全員が認証です。App Store Connect API は JWT(JSON Web Token)で認証しますが、この JWT の作り方に細かい決まりがあり、一つ外すと 401 が返ってきて理由が分かりません。
押さえるべき点は三つです。トークンの有効期限は最大 20 分で、それを超える値を入れると即座に弾かれます。aud(オーディエンス)は appstoreconnect-v1 固定です。そして発行者 ID とキー ID を取り違えると通りません。
import jwt from "jsonwebtoken" ;
import fs from "node:fs" ;
// .p8 秘密鍵は App Store Connect の「Users and Access > Keys」で発行
const privateKey = fs. readFileSync (process.env. ASC_KEY_PATH , "utf8" );
export function makeToken () {
const now = Math. floor (Date. now () / 1000 );
return jwt. sign (
{
iss: process.env. ASC_ISSUER_ID , // Issuer ID(チーム共通の UUID)
iat: now,
exp: now + 19 * 60 , // 19分。20分上限に余裕を持たせる
aud: "appstoreconnect-v1" ,
},
privateKey,
{
algorithm: "ES256" , // RS256 ではない。必ず ES256
header: { kid: process.env. ASC_KEY_ID , typ: "JWT" },
}
);
}
exp を 19 分にしているのは、生成からリクエスト到達までのわずかな時間で 20 分を超える事故を避けるためです。私自身、ここを 20 分ちょうどにして、ネットワークが遅い日にだけ間欠的に 401 が出る現象に半日悩まされました。アルゴリズムを ES256 にし忘れるのも定番の落とし穴です。.p8 鍵は楕円曲線鍵なので、RS256 を指定すると署名段階で失敗します。
メタデータを JSON で一元管理する
認証が通ったら、次は「何を真実とするか」を決めます。私はストア情報の正本をリポジトリの JSON に置き、API はそれを反映する出力先と位置づけています。画面で直接編集するのをやめ、編集はすべて JSON に対して行います。
{
"shared" : {
"marketingUrl" : "https://example.com" ,
"supportUrl" : "https://example.com/support"
},
"apps" : {
"1234567890" : {
"name" : "Calm Wallpapers" ,
"locales" : {
"ja" : { "subtitle" : "静かな壁紙" , "keywords" : "壁紙,癒し,シンプル" },
"en-US" : { "subtitle" : "Calm wallpapers" , "keywords" : "wallpaper,relax,minimal" }
}
}
}
}
この一元管理の利点は二つあります。一つは差分が Git の履歴に残ること。いつ誰が何を変えたかを後から追えます。もう一つは、共通項目を shared にまとめておけば、サポート URL の変更のような全アプリ共通の修正が一行で済むことです。手作業時代に毎回十箇所直していた変更が、一箇所の編集に変わります。
反映は差分だけにする
毎回すべての項目を送り直すと、APIのレート制限に当たりやすく、無駄も多くなります。私は現在の値を取得し、JSON と比較して、変わった項目だけを更新するようにしています。
async function syncLocale ( appId , locale , desired , token ) {
// 既存のローカライズ情報を取得
const current = await fetchLocalization (appId, locale, token);
const changed = {};
for ( const key of [ "subtitle" , "keywords" ]) {
if (current[key] !== desired[key]) changed[key] = desired[key];
}
if (Object. keys (changed). length === 0 ) {
console. log ( `skip ${ appId }/${ locale }: no diff` );
return { updated: false };
}
await patchLocalization (current.id, changed, token);
console. log ( `updated ${ appId }/${ locale }:` , Object. keys (changed). join ( ", " ));
return { updated: true , fields: Object. keys (changed) };
}
差分反映にすると、実行ログがそのまま「今日何を変えたか」の記録になります。skip だらけのログの中に updated が数行だけ並ぶので、意図しない変更にもすぐ気づけます。
全アプリを同時に壊さないための段階反映
一括反映の怖いところは、間違いも一括で広がることです。キーワードのJSONにタイプミスがあれば、それが全アプリへ一斉に反映されます。私はこれを防ぐために、反映を必ず三段階に分けています。
検証のみのドライラン。差分を出力するが API は呼ばない
一本だけの先行反映。代表アプリ一本に実際に反映して画面で確認する
残り全部への本反映
async function run ( mode ) {
const plan = await buildDiffPlan (); // 全アプリの差分計画
if (mode === "dry-run" ) {
plan. forEach ( p => console. log ( "DIFF" , p.appId, p.fields));
return ;
}
const targets = mode === "canary" ? plan. slice ( 0 , 1 ) : plan;
for ( const p of targets) {
await applyDiff (p);
await sleep ( 800 ); // レート制限を避けるための間隔
}
}
sleep(800) を挟むのは、App Store Connect API が短時間に多くのリクエストを送ると 429 を返すためです。十数本を一気に回すと確実に当たります。間隔を空けるだけで安定するので、急がない処理にしておくのが本番運用では安全です。私は夜間に流して翌朝ログを確認する運用にしています。
どこまで自動化し、どこを人が見るか
メタデータの反映は自動化できますが、何を書くかの判断まで自動化すべきではない、というのが私の立場です。説明文の訴求やキーワードの選定は、ダウンロード数に直結する意思決定で、ここは人が考えるべき部分です。
自動化が担うのは、決めた内容を正確に・漏れなく・記録を残して反映することだけにします。価格の変更のように影響が大きい操作は、ドライランの差分を一度自分の目で見てから本反映する手順を崩しません。仕組みは作業を速くするためのものであって、判断を肩代わりさせるものではない、と線を引いています。
運用データと組み合わせて効果を測る
仕組み化のもう一つの利点は、変更を記録として残せることです。いつどの文言に変えたかが Git に残るので、後からダウンロード数やコンバージョンの動きと突き合わせられます。私の場合、キーワードを season ごとに入れ替えた月とそうでない月で、特定アプリの自然流入が約 15% 変動したことを後から確認できました。手作業の時代は「いつ何を変えたか」が曖昧で、こうした検証そのものができませんでした。
収益面でも同じです。AdMob のリワード導線やサブスクのオンボーディング文言をストアの説明文と揃えておくと、流入から課金までの体験に一貫性が生まれます。説明文と実際のアプリ内体験がずれていると初日の離脱が増えるため、両者をひとつの JSON 源から管理することを強く推奨します。
月一の運用に落とす
最後に、これを習慣にする形を決めておくと続きます。私は月初に、その月のキャンペーン文言や価格をJSONにまとめて編集し、ドライラン、カナリア、本反映の順で流すことにしています。十本のアプリでも実作業は十数分で、しかも貼り忘れがありません。
手作業で半日かけていた頃と比べると、空いた時間をアプリそのものの改善や新作に回せるようになりました。運用の仕組み化は地味ですが、個人開発で本数を増やしていくなら、どこかで必ず効いてくる投資だと感じています。お読みいただきありがとうございました。