App Store Connectを毎日開いて売上を確認し、レビューに返信し、キーワードのランキングをチェックする——個人開発者なら誰もが通る道ですが、これらの作業に費やす時間が積み重なると、本来集中すべき開発・改善の時間が大幅に削られます。
App Store Connect APIは2018年に公開されたものの、JWT認証の手順が独特で、公式ドキュメントを読んでもどこから始めればいいかわかりにくいのが正直なところです。ここではAntigravityをAIエージェントとして活用しながら、この障壁を一気に突破します。認証の実装から始まり、売上データの自動取得、レビューの感情分析、ASOトラッキング、TestFlightの管理まで、個人開発者が実際に使える自動化パターンをすべてコードと共に解説します。
App Store Connect APIの全体像と個人開発者にとっての価値
App Store Connect APIはRESTベースのAPIで、App Store Connectのほぼすべての機能をプログラムから操作できます。主要なエンドポイントは以下のカテゴリに分かれています。
Sales and Trends : 日次・週次・月次の売上レポート、サブスクリプション解約率、国別収益の詳細。App Store Connectの管理画面では過去365日分までしか見られませんが、APIを使えばすべての履歴データを取得できます。
Customer Reviews : アプリへのレビューの取得と返信。レビューの感情を時系列で追うことで、アップデートの影響を定量的に測れます。
App Metadata : アプリのタイトル・説明文・スクリーンショット・プレビュー動画の管理。多言語対応の自動化に使えます。
TestFlight : ベータグループの管理、ビルドの配布制御、フィードバックの収集。
Reporting : ダウンロード数・アクティブデバイス数・クラッシュ率など、パフォーマンス指標の取得。
これらを組み合わせると、毎朝自動でレポートが届き、ネガティブレビューを即座に検知し、ASOのキーワードランキングを追跡するシステムを、Antigravityの支援を借りてゼロから構築できます。
JWT認証の実装 — 最初の壁を乗り越える
App Store Connect APIの最大のハードルがJWT(JSON Web Token)認証です。OAuthのアクセストークンとは異なり、秘密鍵を使って毎回トークンを生成する必要があります。
Step 1: App Store Connectでの鍵の生成
App Store Connect(appstoreconnect.apple.com )にログインし、「ユーザーとアクセス」→「統合」→「App Store Connect API」から新しいAPIキーを作成します。権限は用途に応じて設定しますが、読み取りが中心なら「財務」+「販売とトレンド」+「顧客レビュー」を選択します。
生成後、以下の3つの情報を控えます。
Issuer ID : XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX 形式
Key ID : XXXXXXXXXX 形式(10文字)
.p8秘密鍵ファイル : AuthKey_XXXXXXXXXX.p8(一度しかダウンロードできません)
.p8ファイルは絶対に紛失しないよう、パスワードマネージャーか暗号化ストレージに保管してください。
Step 2: JWTトークン生成の実装
Antigravityに次のプロンプトを渡してください。
App Store Connect APIのJWT認証トークンを生成するTypeScriptのクラスを実装して。
- issuer_id、key_id、.p8秘密鍵のパス を受け取る
- ES256アルゴリズムでJWT署名
- 有効期限は20分(Appleの最大値)
- トークンの自動更新機能を含める
- エラーハンドリングも完備すること
出力されたコードをベースに、以下のような実装になります。
import * as jwt from 'jsonwebtoken' ;
import * as fs from 'fs' ;
class AppStoreConnectAuth {
private readonly issuerId : string ;
private readonly keyId : string ;
private readonly privateKey : string ;
private cachedToken : string | null = null ;
private tokenExpiry : number = 0 ;
constructor ( issuerId : string , keyId : string , privateKeyPath : string ) {
this .issuerId = issuerId;
this .keyId = keyId;
// .p8ファイルを読み込む(改行コードを正規化)
this .privateKey = fs. readFileSync (privateKeyPath, 'utf8' ). replace ( / \r\n / g , ' \n ' );
}
generateToken () : string {
const now = Math. floor (Date. now () / 1000 );
// キャッシュされたトークンがまだ有効なら再利用(有効期限の2分前まで)
if ( this .cachedToken && this .tokenExpiry > now + 120 ) {
return this .cachedToken;
}
const payload = {
iss: this .issuerId,
iat: now,
exp: now + 1200 , // 20分(Appleの最大値)
aud: 'appstoreconnect-v1' ,
};
const header = {
alg: 'ES256' ,
kid: this .keyId,
typ: 'JWT' ,
};
try {
this .cachedToken = jwt. sign (payload, this .privateKey, {
algorithm: 'ES256' ,
header,
});
this .tokenExpiry = now + 1200 ;
return this .cachedToken;
} catch (error) {
// 秘密鍵の形式エラーが最も多い。.p8ファイルのBEGIN/END行が含まれているか確認する
throw new Error ( `JWT生成失敗: ${ error instanceof Error ? error . message : String ( error ) }` );
}
}
// APIリクエスト用のAuthorizationヘッダーを返す
getAuthHeader () : { Authorization : string } {
return { Authorization: `Bearer ${ this . generateToken () }` };
}
}
// 使用例
const auth = new AppStoreConnectAuth (
process.env. ASC_ISSUER_ID \ ! ,
process.env. ASC_KEY_ID \ ! ,
'./AuthKey_XXXXXXXXXX.p8'
);
// 環境変数として設定(.envファイル)
// ASC_ISSUER_ID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
// ASC_KEY_ID=XXXXXXXXXX
// APP_ID=あなたのアプリのApple ID(数字10桁)
よくある間違い1: 秘密鍵の改行コード問題
.p8ファイルをWindowsで扱ったりコピー&ペーストすると、改行コードが\r\nになってjsonwebtokenがエラーを返します。.replace(/\r\n/g, '\n')で必ず正規化してください。
よくある間違い2: aud(オーディエンス)の指定ミス
audは必ず"appstoreconnect-v1"(文字列)を指定します。配列形式や値の変更はすべて401エラーになります。
売上レポートの自動取得と分析
JWT認証ができたら、最初に実装すべきは売上レポートの自動取得です。
日次売上レポートの取得
import axios from 'axios' ;
import * as zlib from 'zlib' ;
import { promisify } from 'util' ;
const gunzip = promisify (zlib.gunzip);
const BASE_URL = 'https://api.appstoreconnect.apple.com/v1' ;
async function fetchDailySalesReport (
auth : AppStoreConnectAuth ,
vendorNumber : string , // App Store ConnectのベンダーID
date : string // 'YYYY-MM-DD' 形式(昨日の日付を指定)
) : Promise < SalesRecord []> {
const params = new URLSearchParams ({
'filter[frequency]' : 'DAILY' ,
'filter[reportDate]' : date,
'filter[reportType]' : 'SALES' ,
'filter[vendorNumber]' : vendorNumber,
'filter[version]' : '1_0' ,
});
try {
const response = await axios. get (
`${ BASE_URL }/salesReports?${ params . toString () }` ,
{
headers: {
... auth. getAuthHeader (),
'Accept' : 'application/a-gzip' , // レポートはgzip圧縮で返る
},
responseType: 'arraybuffer' ,
}
);
// gzip解凍してTSV(タブ区切り)をパース
const decompressed = await gunzip (Buffer. from (response.data));
const tsv = decompressed. toString ( 'utf8' );
return parseSalesTsv (tsv);
} catch (error) {
if (axios. isAxiosError (error) && error.response?.status === 404 ) {
// その日のデータがまだ生成されていない(通常は翌日の午前2〜4時JST頃に生成)
return [];
}
throw error;
}
}
interface SalesRecord {
appId : string ;
title : string ;
units : number ;
developerProceeds : number ;
currency : string ;
storefront : string ; // 国コード (JP, US, GB など)
productType : string ; // 1=paid download, 7=free download, F1=subscription
}
function parseSalesTsv ( tsv : string ) : SalesRecord [] {
const lines = tsv. split ( ' \n ' ). filter ( line => line. trim ());
const headers = lines[ 0 ]. split ( ' \t ' );
return lines. slice ( 1 ). map ( line => {
const values = line. split ( ' \t ' );
const record : Record < string , string > = {};
headers. forEach (( h , i ) => { record[h] = values[i] || '' ; });
return {
appId: record[ 'Apple Identifier' ],
title: record[ 'Title' ],
units: parseInt (record[ 'Units' ] || '0' , 10 ),
developerProceeds: parseFloat (record[ 'Developer Proceeds' ] || '0' ),
currency: record[ 'Currency of Proceeds' ],
storefront: record[ 'Storefront' ],
productType: record[ 'Product Type Identifier' ],
};
});
}
// 期待する出力例:
// [
// { appId: '123456789', title: 'MyApp Pro', units: 15, developerProceeds: 10.50, currency: 'JPY', storefront: 'JP', productType: '1' },
// { appId: '123456789', title: 'MyApp Pro', units: 3, developerProceeds: 2.10, currency: 'USD', storefront: 'US', productType: '1' },
// ]
週次・月次レポートの集計と可視化
日次レポートを30日分取得して収益推移を可視化するAntigravityへのプロンプトは以下の通りです。
上記のfetchDailySalesReportを使って、過去30日間の日次売上データを並列取得し、
以下を計算して返す関数を実装して:
- 日別総収益(JPY換算)
- アプリ別収益シェア
- 国別収益トップ5
- 前週比の変化率
Promise.allSettledを使って失敗した日付はスキップする設計にすること
よくある間違い3: 売上レポートのデータ生成タイミング
昨日の日付を指定しても404が返ることがあります。App Store Connectのレポートは日本時間の午前2〜5時頃に生成されるため、夜中に自動実行する場合は生成完了を待つリトライロジックが必要です。
async function fetchWithRetry (
fetchFn : () => Promise < SalesRecord []>,
maxRetries = 3 ,
delayMs = 30000 // 30秒待機
) : Promise < SalesRecord []> {
for ( let attempt = 0 ; attempt < maxRetries; attempt ++ ) {
const result = await fetchFn ();
if (result. length > 0 ) return result;
if (attempt < maxRetries - 1 ) {
console. log ( `データ未生成 — ${ delayMs / 1000 }秒後に再試行 (${ attempt + 1 }/${ maxRetries })` );
await new Promise ( resolve => setTimeout (resolve, delayMs));
}
}
return []; // 最終的にデータがない場合は空配列で続行
}
レビューの自動取得と感情分析
レビューは個人開発者にとって最も重要なフィードバックです。APIで自動取得し、ネガティブなレビューを即座に検知して対応できるシステムを構築します。
レビューの全件取得
App Store Connect APIのレビューエンドポイントはページネーションが必要です。
interface Review {
id : string ;
authorName : string ;
rating : number ; // 1〜5
title : string ;
body : string ;
createdDate : string ;
territory : string ; // 'JPN', 'USA' など
responded : boolean ;
}
async function fetchAllReviews (
auth : AppStoreConnectAuth ,
appId : string ,
afterDate ?: Date
) : Promise < Review []> {
const allReviews : Review [] = [];
let nextUrl : string | null =
`${ BASE_URL }/apps/${ appId }/customerReviews?limit=200&sort=-createdDate&fields[customerReviews]=authorName,rating,title,body,createdDate,territory` ;
while (nextUrl) {
const response = await axios. get (nextUrl, { headers: auth. getAuthHeader () });
const { data , links } = response.data;
for ( const item of data) {
const review : Review = {
id: item.id,
... item.attributes,
responded: \ ! \ ! item.relationships?.response?.data,
};
// 指定日付以前のレビューは取得不要なら早期終了
if (afterDate && new Date (review.createdDate) < afterDate) {
return allReviews;
}
allReviews. push (review);
}
// 次ページのURLが存在する場合は続行、なければ終了
nextUrl = links?.next || null ;
}
return allReviews;
}
低評価レビューの即時Slack通知
★1〜★2のレビューを検知したらSlackに即通知するシステムです。
import { WebClient } from '@slack/web-api' ;
const slack = new WebClient (process.env. SLACK_BOT_TOKEN );
async function notifyNegativeReviews (
auth : AppStoreConnectAuth ,
appId : string ,
appName : string
) : Promise < void > {
// 過去24時間以内のレビューを取得
const since = new Date (Date. now () - 24 * 60 * 60 * 1000 );
const reviews = await fetchAllReviews (auth, appId, since);
const negativeReviews = reviews. filter ( r => r.rating <= 2 );
if (negativeReviews. length === 0 ) {
console. log ( '過去24時間の低評価レビューなし' );
return ;
}
for ( const review of negativeReviews) {
const stars = '★' . repeat (review.rating) + '☆' . repeat ( 5 - review.rating);
await slack.chat. postMessage ({
channel: process.env. SLACK_CHANNEL_ID \ ! ,
blocks: [
{
type: 'section' ,
text: {
type: 'mrkdwn' ,
text: `🚨 *${ appName }* に低評価レビューがあります \n ${ stars } by ${ review . authorName } (${ review . territory })` ,
},
},
{
type: 'section' ,
text: {
type: 'mrkdwn' ,
text: `*${ review . title }* \n ${ review . body }` ,
},
},
{
type: 'actions' ,
elements: [
{
type: 'button' ,
text: { type: 'plain_text' , text: 'App Store Connectで返信' },
url: `https://appstoreconnect.apple.com/apps/${ appId }/appstore/ratings` ,
},
],
},
],
});
}
}
// 期待する動作: ★1〜2のレビューが投稿から24時間以内にSlackに通知される
レビューへの返信
App Store Connect APIでレビューへの返信も自動化できます。ただし、AIが自動生成した返信を無審査でそのまま投稿するのはリスクがあるため、Slackの「承認」ボタンを押してから投稿する半自動フローを推奨します。
async function replyToReview (
auth : AppStoreConnectAuth ,
reviewId : string ,
responseBody : string
) : Promise < void > {
// 既存の返信があるか確認(上書き不可のため)
const checkResponse = await axios. get (
`${ BASE_URL }/customerReviewResponses/${ reviewId }` ,
{ headers: auth. getAuthHeader (), validateStatus : () => true }
);
if (checkResponse.status === 200 ) {
console. log ( `レビュー ${ reviewId } にはすでに返信済みです` );
return ;
}
await axios. post (
`${ BASE_URL }/customerReviewResponses` ,
{
data: {
type: 'customerReviewResponses' ,
attributes: { responseBody },
relationships: {
review: { data: { type: 'customerReviews' , id: reviewId } },
},
},
},
{ headers: { ... auth. getAuthHeader (), 'Content-Type' : 'application/json' } }
);
console. log ( `✅ レビュー ${ reviewId } への返信を投稿しました` );
}
ASOトラッキングの自動化
ASOキーワードのランキングを毎日追跡するには、外部のASO APIサービス(AppFollow、Sensor Tower等)と組み合わせるか、App Store Connect APIのメトリクスエンドポイントを使います。ここではApp Store Connect標準のアナリティクスAPIを活用します。
// インプレッション・閲覧数・転換率の取得
async function fetchDiscoveryMetrics (
auth : AppStoreConnectAuth ,
appId : string ,
startDate : string , // 'YYYY-MM-DD'
endDate : string
) : Promise < void > {
const params = new URLSearchParams ({
granularity: 'DAY' ,
groupBy: 'sourceType' ,
'filter[apps]' : appId,
'filter[measures]' : 'impressionsTotal,pageViewCount,conversionRate,units' ,
'filter[frequency]' : 'DAY' ,
'filter[dateTime][gte]' : startDate,
'filter[dateTime][lte]' : endDate,
});
const response = await axios. get (
`https://api.appstoreconnect.apple.com/v1/analyticsReportRequests` ,
{ headers: auth. getAuthHeader () }
);
// レポートリクエストは非同期で処理される
// 詳細はAnalytics Reports APIのフローに従う
console. log ( 'アナリティクスレポートリクエスト送信:' , response.status);
}
よくある間違い4: Analytics Reports APIの非同期フロー
Analytics Reports APIは古いSales and Trends APIと異なり、リクエスト→ポーリング→ダウンロードの3ステップを踏みます。Antigravityに「App Store Connect Analytics Reports APIの非同期フローを実装して」と指示すると、このフロー全体をコード化してくれます。
毎朝の自動収益レポートを構築する
これまでの実装を組み合わせて、毎朝Slackに届く収益サマリーを自動生成するシステムを構築します。
// 毎朝7:00 JSTにGitHub Actionsまたは自前サーバーで実行
async function generateMorningReport () : Promise < void > {
const auth = new AppStoreConnectAuth (
process.env. ASC_ISSUER_ID \ ! ,
process.env. ASC_KEY_ID \ ! ,
process.env. ASC_PRIVATE_KEY_PATH \ !
);
const vendorNumber = process.env. ASC_VENDOR_NUMBER \ ! ;
const appId = process.env. APP_ID \ ! ;
const yesterday = new Date (Date. now () - 24 * 60 * 60 * 1000 )
. toISOString (). split ( 'T' )[ 0 ];
try {
// 並列で売上・レビューを取得
const [ salesData , recentReviews ] = await Promise . all ([
fetchWithRetry (() => fetchDailySalesReport (auth, vendorNumber, yesterday)),
fetchAllReviews (auth, appId, new Date (Date. now () - 7 * 24 * 60 * 60 * 1000 )),
]);
// 売上集計
const totalRevenue = salesData. reduce (
( sum , r ) => sum + r.developerProceeds, 0
);
const totalDownloads = salesData
. filter ( r => r.productType === '7' ) // 無料ダウンロード
. reduce (( sum , r ) => sum + r.units, 0 );
// レビューの評価平均(過去7日)
const avgRating = recentReviews. length > 0
? recentReviews. reduce (( sum , r ) => sum + r.rating, 0 ) / recentReviews. length
: 0 ;
// Slackに投稿
const stars = '★' . repeat (Math. round (avgRating)) + '☆' . repeat ( 5 - Math. round (avgRating));
await slack.chat. postMessage ({
channel: process.env. SLACK_CHANNEL_ID \ ! ,
text: `📊 *${ yesterday } 収益レポート*
💰 開発者収益: ¥${ Math . round ( totalRevenue * 150 ). toLocaleString () } (約)
📥 ダウンロード数: ${ totalDownloads . toLocaleString () }件
⭐ 平均評価(過去7日): ${ stars } (${ avgRating . toFixed ( 1 ) })
💬 新着レビュー: ${ recentReviews . filter ( r => new Date ( r . createdDate ) > new Date ( Date . now () - 24 * 60 * 60 * 1000 )). length }件` ,
});
// 低評価レビューの通知
await notifyNegativeReviews (auth, appId, 'MyApp' );
} catch (error) {
console. error ( 'レポート生成エラー:' , error);
// エラーもSlackに通知(無音でスキップしない)
await slack.chat. postMessage ({
channel: process.env. SLACK_CHANNEL_ID \ ! ,
text: `⚠️ 収益レポート生成中にエラーが発生しました: ${ String ( error ) }` ,
});
}
}
GitHub Actionsで定期実行する
上記のスクリプトをGitHub Actionsで毎朝自動実行する設定です。APIキーなどの機密情報はGitHub Secretsに保存します。
# .github/workflows/daily-report.yml
name : Daily App Store Report
on :
schedule :
- cron : '0 22 * * *' # 毎日UTC 22:00 = JST 7:00
workflow_dispatch : # 手動実行も可能
jobs :
report :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- name : Setup Node.js
uses : actions/setup-node@v4
with :
node-version : '22'
cache : 'npm'
- name : Install dependencies
run : npm ci
- name : Create private key file
run : echo "${{ secrets.ASC_PRIVATE_KEY }}" > ./AuthKey.p8
- name : Run daily report
env :
ASC_ISSUER_ID : ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_ID : ${{ secrets.ASC_KEY_ID }}
ASC_PRIVATE_KEY_PATH : ./AuthKey.p8
ASC_VENDOR_NUMBER : ${{ secrets.ASC_VENDOR_NUMBER }}
APP_ID : ${{ secrets.APP_ID }}
SLACK_BOT_TOKEN : ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_CHANNEL_ID : ${{ secrets.SLACK_CHANNEL_ID }}
run : npx tsx src/report.ts
- name : Cleanup private key
if : always() # 成功・失敗問わず秘密鍵を削除
run : rm -f ./AuthKey.p8
GitHub Actionsを使うことで、サーバーを持たない個人開発者でも月2,000分(無料枠)の中で十分動かせます。1日1回の実行なら年間365回 × 約2分 = 730分で収まります。
よくある間違い5: GitHub Secretsへの.p8ファイルの登録方法
-----BEGIN PRIVATE KEY-----から-----END PRIVATE KEY-----まで、改行を含む全内容をそのままGitHub Secretsに貼り付けます。ファイルを直接アップロードする方法はないため、上記のようにechoでファイルを復元するステップが必要です。
複数アプリの一括管理
複数のアプリを運営している場合、アプリごとに処理を並列化するパターンです。
const APP_CONFIG = [
{ appId: '111111111' , name: 'アプリA' , vendorNumber: process.env. VENDOR_NUMBER \ ! },
{ appId: '222222222' , name: 'アプリB' , vendorNumber: process.env. VENDOR_NUMBER \ ! },
{ appId: '333333333' , name: 'アプリC' , vendorNumber: process.env. VENDOR_NUMBER \ ! },
];
async function generateAllReports () : Promise < void > {
const auth = new AppStoreConnectAuth (
process.env. ASC_ISSUER_ID \ ! ,
process.env. ASC_KEY_ID \ ! ,
process.env. ASC_PRIVATE_KEY_PATH \ !
);
// 全アプリを並列処理(JWTトークンは共通の auth インスタンスで自動管理)
const results = await Promise . allSettled (
APP_CONFIG . map ( app =>
notifyNegativeReviews (auth, app.appId, app.name)
)
);
// 失敗したアプリのみログ出力(全体の処理は止めない)
results. forEach (( result , i ) => {
if (result.status === 'rejected' ) {
console. error ( `${ APP_CONFIG [ i ]. name } の処理失敗:` , result.reason);
}
});
}
実用的な応用例: 月次レポートのPDF自動生成
Antigravityを使えば、取得したデータからPDFレポートを生成する機能も追加できます。毎月1日に先月分の収益・レビュー・主要指標をまとめたPDFをDropboxに保存し、重要数値だけSlackに通知するフローです。
import puppeteer from 'puppeteer' ;
async function generateMonthlyPdf (
reportData : MonthlyReportData ,
outputPath : string
) : Promise < void > {
const html = buildReportHtml (reportData); // HTMLテンプレートを生成
const browser = await puppeteer. launch ({ headless: true });
const page = await browser. newPage ();
await page. setContent (html, { waitUntil: 'networkidle0' });
await page. pdf ({
path: outputPath,
format: 'A4' ,
printBackground: true ,
margin: { top: '20mm' , right: '15mm' , bottom: '20mm' , left: '15mm' },
});
await browser. close ();
console. log ( `✅ 月次レポートPDFを生成しました: ${ outputPath }` );
}
「Antigravityで月次レポートをまとめたHTMLを返すbuildReportHtml関数を実装して。AppleのデザインガイドラインにあるSF Proフォントに近い書体を使い、グラフはChart.jsで描画する」と指示すれば、ビジュアルの整ったレポートテンプレートが出力されます。
このシステムを本番で運用する際の注意点
レートリミットへの対応 : App Store Connect APIにはレートリミットがあります。大量のリクエストを送ると429エラーが返ります。各リクエスト間に100〜200msのdelayを入れ、429が返ったら指数バックオフで再試行するのが安全です。
APIキーのローテーション : .p8秘密鍵は一度だけ発行できます。紛失した場合は新しいキーを発行し、古いキーを即座に失効させてください。定期的にGitHub Secretsのキーを更新するリマインダーをカレンダーに設定することを推奨します。
データの保存 : 取得した売上データはローカルまたはGoogle SheetsやNotion APIに保存して蓄積しておくと、長期トレンドの分析に使えます。App Store Connect側での保存期間は有限です。
個人開発者として運営するアプリの数が増えてきた場合には、Antigravity × AdMob × Firebase Analytics でデータドリブン収益最適化で紹介している収益ダッシュボードと組み合わせると、広告収益とアプリ内課金の両方を一元管理できます。また、TestFlightの自動配信についてはAntigravity × Fastlane 完全攻略ガイド も参考にしてください。
私自身がこの仕組みに頼るようになった経緯
私自身、個人開発で複数のアプリを長く運営してきました。以前は毎朝コーヒーを淹れながらApp Store Connectを開き、前日の売上を眺め、新着レビューに目を通すのが日課でした。数字が良ければ嬉しく、悪ければその理由を画面の中に探す——その時間が、気づけば一日の最初の集中力を静かに削っていました。
転機は、確認作業そのものをやめてみたことでした。毎朝のサマリーをSlackへ自動で届けるようにし、低評価のレビューだけが手元に飛んでくる仕組みに切り替えたのです。すると、ダッシュボードを「見に行く」必要がなくなりました。異変があるときだけ通知が来るので、平穏な日は開発に没頭できます。
数字を追いかける時間が減ったことで、かえって数字との向き合い方が落ち着いたように感じています。毎日の細かな変動に一喜一憂するのではなく、週単位・月単位の傾向で判断できるようになりました。自動化の本当の価値は、作業時間の短縮そのものよりも、こうして取り戻せる心の余白にあるのかもしれません。
もし同じように毎朝の確認作業に時間を奪われていると感じている方がいらっしゃれば、まずはJWT認証と日次売上レポートの2つだけでも自動化してみてください。最初の通知がSlackに届いた朝の、肩の力が抜ける感覚を、ぜひ味わっていただきたいと思っています。
App Store Connect APIを一度セットアップすれば、これまで毎日30分〜1時間かけていたデータ確認とレビュー対応の大半が自動化されます。浮いた時間をアプリの機能改善やマーケティング戦略の検討に使える——これが個人開発者にとって最大の恩恵です。まずはJWT認証と日次売上レポートの取得から試してみてください。そこさえ動けば、本記事の残りのピースはすぐに組み合わせられます。