mcpField
R
rc-webhooks

rc-webhooks

github.com/revenuecat開発76671 回インストール1 日前に更新
リポジトリ

rc-webhooks について

rc-webhooksは、サーバーでRevenueCatのWebhookを処理するためのスキルです。署名検証、イベントの重複排除、イベント種別に応じた権限更新を扱います。

主な機能

  1. 01Webhook署名の検証
  2. 02event.idによる重複排除
  3. 03イベント種別ごとの処理
  4. 04権限の付与と取り消し

活用シーン

  1. 01RevenueCatのWebhookエンドポイント構築
  2. 02イベントの重複処理防止
  3. 03サブスクリプション変更に伴う権限更新

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/revenuecat/ai-toolkit --skill rc-webhooks

インストール後、Claude Code で/rc-webhooksを入力して読み込まれたか確認します。

詳細

RevenueCat Webhooks

1 つのエンドポイントを設定します。RevenueCat は、すべてのストアについて正規化された JSON イベントスキーマを 1 つ送信します。サーバー側で行うことは、署名を検証し、event.id で重複を排除し、イベントタイプごとに振り分けることです。

Phase 1: Discover

コードに手を加える前に、何を接続するのか確認します。

  • POST と JSON 本文を受け付ける、サーバー側の HTTPS エンドポイントを所有しています。
  • RevenueCat ダッシュボードの「Integrations」から「Webhooks」に進み、Webhook シークレットを取得しています。
  • 処理済みイベント ID と、app_user_id ごとのエンタイトルメント状態を記録する永続ストレージがあります。
  • RevenueCat はすでにプロダクトをエンタイトルメントにマッピングしています。そのため、event.type で分岐し、event.entitlement_ids を読み取ります。バックエンドでプロダクトとエンタイトルメントの対応表を管理する必要はありません。

すべてのイベントは、次の外側の構造を持ちます。

{
  "api_version": "1.0",
  "event": {
    "id": "evt_01HABCXYZ0000000000000001",
    "type": "INITIAL_PURCHASE",
    "app_user_id": "user_12345",
    "product_id": "premium_monthly",
    "period_type": "NORMAL",
    "purchased_at_ms": 1700000000000,
    "expiration_at_ms": 1702592000000,
    "store": "PLAY_STORE",
    "environment": "PRODUCTION",
    "entitlement_ids": ["pro_access"],
    "transaction_id": "GPA.1234-5678-9012-34567"
  }
}

Phase 2: Plan

ハンドラーを書く前に、イベントタイプごとの適切なアクションを選びます。

イベントタイプ意味ハンドラーのアクション
INITIAL_PURCHASEこのユーザーとプロダクトに対する最初の有料トランザクションです。entitlement_ids のエンタイトルメントを付与します。
RENEWALEXPIRATION 後の再サブスクリプションを含む、サブスクリプションの更新です。entitlement_ids のエンタイトルメントを付与または延長します。
CANCELLATIONユーザーが自動更新をオフにしました。アクセスは expiration_at_ms まで継続します。expiration_at_ms に取り消すようスケジュールします。すぐに取り消してはいけません。
UNCANCELLATIONユーザーが有効期限前に自動更新を再びオンにしました。スケジュール済みの取り消しをキャンセルします。エンタイトルメントは有効なままにします。
EXPIRATIONサブスクリプションが実際に終了しました。エンタイトルメントをすぐに取り消します。
BILLING_ISSUE支払いに失敗しました。ユーザーは猶予期間中、またはアカウント保留中の可能性があります。アカウントにフラグを設定します。まだ取り消してはいけません。復旧に失敗した場合、RevenueCat は EXPIRATION を送信します。
PRODUCT_CHANGEユーザーがプランを変更しました(アップグレード、ダウングレード、または同一階層内の変更)。記録されているプロダクトを更新します。エンタイトルメントの状態は entitlement_ids に従います。
SUBSCRIBER_ALIAS2 つのアプリユーザー ID が 1 つの ID に統合されました。エイリアスされた ID のローカルレコードを統合します。
TRANSFERトランザクションが、あるアプリユーザー ID から別の ID に移動しました。エンタイトルメントを古い ID から新しい ID に移します。

この表には、次の重要な判断が反映されています。

  • CANCELLATION はアクセス状態の変更ではありません。これはユーザーの意図を示すシグナルです。すぐに取り消すのは、ユーザーがまだ所有している有料アクセスを削除するバグです。
  • EXPIRATION がアクセス状態の変更です。この時点で取り消します。
  • EXPIRATION 後に再サブスクリプションすると、INITIAL_PURCHASE ではなく RENEWAL が発生します。RENEWAL の分岐は、エンタイトルメントが現在取り消されているユーザーに対しても安全に実行できる必要があります。つまり、延長するだけでなく、付与する必要があります。
  • BILLING_ISSUE は取り消しではありません。BILLING_ISSUE で取り消すと、Google Play の猶予期間中、またはアカウント保留中のユーザーまでアクセスできなくなります。

Phase 3: Execute

検証、重複排除、振り分けを行うハンドラーを接続します。

署名を検証して解析する
post("/revenuecat/webhook") {
    val body = call.receiveText()
    val signature = call.request.headers["X-RevenueCat-Signature"]
    if (!verifySignature(body, signature, webhookSecret)) {
        call.respond(HttpStatusCode.Unauthorized); return@post
    }
    val event = Json.decodeFromString<RevenueCatEnvelope>(body).event
    handleEvent(event)
    call.respond(HttpStatusCode.OK)
}

イベントが永続化されたらすぐに 2xx を返します。処理に時間がかかる場合は、キューに登録してから応答してください。ハンドラーの応答が遅いと再試行が発生し、同じイベントが重複して配信されます。

event.id で重複を排除する

RevenueCat は同じイベントを再配信することがあります。event.id は冪等性キーです。

suspend fun handleEvent(event: RcEvent) {
    if (processedEvents.insertIfAbsent(event.id)) {
        dispatch(event)
    }
    // Already processed: fall through, responder still returns 200.
}

insertIfAbsent はストア内でアトミックである必要があります(event_id に一意インデックスを設定し、重複キーエラーを無視する挿入処理を行う方法が有効です)。イベント ID の挿入と同じトランザクション内で、後続の書き込みをすべて行ってください。そうすることで、ハンドラーの途中でクラッシュしても、イベントが処理済みと記録されたのに適用されていない状態を防げます。

タイプごとに振り分ける
suspend fun dispatch(e: RcEvent) = when (e.type) {
    "INITIAL_PURCHASE", "RENEWAL", "UNCANCELLATION" ->
        db.grantEntitlements(e.appUserId, e.entitlementIds, e.expirationAtMs)
    "CANCELLATION" ->
        db.scheduleRevocation(e.appUserId, e.entitlementIds, e.expirationAtMs)
    "EXPIRATION" ->
        db.revokeEntitlements(e.appUserId, e.entitlementIds)
    "BILLING_ISSUE" ->
        db.flagBillingIssue(e.appUserId)
    "PRODUCT_CHANGE" ->
        db.updateProduct(e.appUserId, e.productId, e.entitlementIds)
    "SUBSCRIBER_ALIAS", "TRANSFER" ->
        db.mergeIdentity(e)
    else -> Unit
}

ハンドブックに準拠するための注意事項:

  • RENEWAL での grantEntitlements は、冪等かつ加算的である必要があります。これにより、EXPIRATION 後の再サブスクリプションでアクセスが復旧します。
  • scheduleRevocation は、(app_user_id, entitlement_id) をキーとして、expiration_at_ms に実行される保留中のジョブを保存します。ジョブの実行前に UNCANCELLATION が届いた場合は、そのジョブをキャンセルします。先に EXPIRATION が届いた場合は、EXPIRATION ハンドラーで取り消し、保留中のジョブを削除します。
CANCELLATION ペイロード(アクセスは継続)
{
  "api_version": "1.0",
  "event": {
    "id": "evt_01HABCXYZ0000000000000010",
    "type": "CANCELLATION",
    "app_user_id": "user_12345",
    "product_id": "premium_monthly",
    "purchased_at_ms": 1700000000000,
    "expiration_at_ms": 1702592000000,
    "entitlement_ids": ["pro_access"],
    "store": "PLAY_STORE",
    "environment": "PRODUCTION"
  }
}

ユーザーは 1702592000000 まで pro_access を保持します。そのタイムスタンプで権限を取り消すようスケジュールしてください。

EXPIRATION ペイロード(即時に取り消し)
{
  "api_version": "1.0",
  "event": {
    "id": "evt_01HABCXYZ0000000000000011",
    "type": "EXPIRATION",
    "app_user_id": "user_12345",
    "product_id": "premium_monthly",
    "expiration_at_ms": 1702592000000,
    "entitlement_ids": ["pro_access"],
    "store": "PLAY_STORE",
    "environment": "PRODUCTION"
  }
}

これを処理したらすぐに、user_12345 の pro_access を取り消してください。

期限切れ後の RENEWAL(再登録)

期限切れになったユーザーが再登録すると、RevenueCat は INITIAL_PURCHASE ではなく RENEWAL を送信します。

{
  "api_version": "1.0",
  "event": {
    "id": "evt_01HABCXYZ0000000000000012",
    "type": "RENEWAL",
    "app_user_id": "user_12345",
    "product_id": "premium_monthly",
    "purchased_at_ms": 1705270400000,
    "expiration_at_ms": 1707862400000,
    "entitlement_ids": ["pro_access"],
    "store": "PLAY_STORE",
    "environment": "PRODUCTION"
  }
}

RENEWAL の分岐では、エンタイトルメントがすでに存在すると想定せず、付与する必要があります。既存の有効期限を延長するだけでは、再登録したユーザーは引き続きアクセスできません。

検証チェックリスト

  • 署名検証では、X-RevenueCat-Signature がない、または値が誤っているリクエストを拒否します。
  • 同じ event.id を持つイベントが再送された場合、何もせず、引き続き 200 を返します。
  • CANCELLATION ではアクセスを取り消しません。ユーザーは expiration_at_ms までエンタイトルメントを保持します。
  • EXPIRATION では、entitlement_ids 内の ID に対するアクセスを取り消します。
  • EXPIRATION の後に RENEWAL が届いた場合、同じ app_user_id のアクセスを復元します。
  • BILLING_ISSUE では、アクセスを取り消さずにアカウントにフラグを設定します。
  • 後続の処理が非同期の場合でも、ハンドラーは再試行期間内に 2xx を返します。

参考資料

コメント