Revenuecat webhook oluşturma adımlarını eventlerini ve typlerını araştırarak, gerekli migration tablolarını ve yapıyı oluştur.
<?php

declare(strict_types=1);

namespace App\Http\Controllers\API;

use App\Enums\SubscriptionEventTypeEnum;
use App\Http\Controllers\Controller;
use App\Models\Subscription;
use App\Models\User;
use App\Traits\ApiResponses;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

final class RevenueCatWebhookController extends Controller
{
    use ApiResponses;

    public function handle(Request $request)
    {
        Log::info('RevenueCat Webhook Received', $request->all());

        $payload = $request->all();
        $data = $payload['event'];
        $eventType = $data['type'];
        $revenuecatAppUserId = $data['app_user_id'] ?? null;


        $user = User::where('app_user_id', $revenuecatAppUserId)->first();

        if (! $user) {
            Log::warning("User with RevenueCat app user ID {$revenuecatAppUserId} not found.");
            return $this->error('User not found', 404);
        }

        switch ($eventType) {
            case 'INITIAL_PURCHASE':
                $this->handleInitialPurchase($user, $data);
                break;
            case 'NON_RENEWING_PURCHASE':
                // Handle non-renewing purchase
                break;
            case 'RENEWAL':
                $this->handleRenewal($user, $data);
                break;
            case 'CANCELLATION':
                $this->handleCancellation($user, $data);
                break;
            case 'BILLING_ISSUE':
                // Handle billing issue
                break;
            case 'EXPIRATION':
                $this->handleExpiration($user, $data);
                break;
            case 'PRODUCT_CHANGE':
                $this->handleProductChange($user, $data);
                break;
            case 'SUBSCRIPTION_EXTENDED':
                $this->handleSubscriptionExtended($user, $data);
                break;
            default:
                break;
        }

        return $this->success('Subscription updated successfully');
    }

    private function handleInitialPurchase(User $user, array $data)
    {

    }

    protected function handleRenewal($user, $data) {
    }

    protected function handleCancellation($user, $data) {
    }

    protected function handleExpiration($user, $data) {
    }

    protected function handleProductChange($user, $data) {
    }

    protected function handleSubscriptionExtended($user, $data) {

    }
}


Event Types and Fields
RevenueCat sends webhooks in response to events that occur in your app. Here these event types are defined, as well as the data contained in each webhook.

Event Types
Webhook Event Type	Description	App Store	Play Store	Amazon	Stripe	Promo
TEST	Test event issued through the RevenueCat dashboard.	✅	✅	❌	❌	❌
INITIAL_PURCHASE	A new subscription has been purchased.	✅	✅	✅	✅	❌
RENEWAL	An existing subscription has been renewed or a lapsed user has resubscribed.	✅	✅	✅	✅	❌
CANCELLATION	A subscription or non-renewing purchase has been cancelled or refunded. Note that in the event of refunds, a subscription's auto-renewal setting may still be active. See cancellation reasons for more details. Note that, for the case of subscription refunds, this event is only fired if the latest subscription period of a subscription is refunded, refunds for earlier subscription periods do not trigger this event.	✅	✅	✅	✅	✅
UNCANCELLATION	A non-expired cancelled subscription has been re-enabled.	✅	✅	✅	❌	❌
NON_RENEWING_PURCHASE	A customer has made a purchase that will not auto-renew.	✅	✅	✅	✅	✅
SUBSCRIPTION_PAUSED	The subscription has set to be paused at the end of the period. Please note: You should not revoke access when receiving a SUBSCRIPTION_PAUSED event, but only when receiving an EXPIRATION event (which will have the expiration reason SUBSCRIPTION_PAUSED).	❌	✅	❌	❌	❌
EXPIRATION	A subscription has expired and access should be removed. If you have Platform Server Notifications configured, this event will occur as soon as we are notified (within seconds to minutes) of the expiration. If you do not have notifications configured, delays may be approximately 1 hour.	✅	✅	✅	✅	✅
BILLING_ISSUE	There has been a problem trying to charge the subscriber. This does not mean the subscription has expired. Can be safely ignored if listening to CANCELLATION event + cancel_reason=BILLING_ERROR.	✅	✅	✅	✅	❌
PRODUCT_CHANGE	A subscriber has changed the product of their subscription. This does not mean the new subscription is in effect immediately. See Managing Subscriptions for more details on updates, downgrades, and crossgrades.	✅	✅	❌	✅	❌
TRANSFER	A transfer of transactions and entitlements was initiated between one App User ID(s) to another. Please note: The webhook will only be sent for the destination user despite us displaying this event in both customer histories because the event body is exactly the same for both users.	✅	✅	✅	✅	❌
SUBSCRIPTION_EXTENDED	An existing subscription has been extended (the expiration date of the current subscription period has been pushed back to the future). This event is fired when a Apple App Store or Google Play Store subscription is extended through the store's API. On the Google Play Store, this event can also sometimes fire when Google defers charging for a renewal by less than 24 hours (for unknown reasons). In this case, you will receive a SUBSCRIPTION_EXTENDED webhook, followed by either a RENEWAL or BILLING_ISSUE webhook within the next 24 hours.	✅	✅	✅	✅	❌
TEMPORARY_ENTITLEMENT_GRANT	RevenueCat was temporarily unable to validate a purchase with the respective store and has granted a short-term entitlement to the customer. This event is sent in exceptional situations (for example, a partial app store outage) and is used to avoid customers making a purchase but not getting access to their entitlement. The expiration date of the entitlement is always at most 24 hours in the future. Once the exceptional situation has been resolved and RevenueCat can validate the purchases, a regular INITIAL_PURCHASE event will be sent. If the purchase can't be validated, an EXPIRATION event with the same transaction_id is sent. Please note: because this event type is dispatched in cases of limited connectivity with the store servers, it contains less information than a regular purchase event. The following fields are guaranteed to be present: app_user_id, purchased_at_ms, expiration_at_ms, event_timestamp_ms, product_id, entitlement_ids, store, transaction_id (note: the transaction_id might be different from the store's transaction_id present in a subsequent INITIAL_PURCHASE event; depending on the store, the product_id might be different from the product_id present in the subsequent INITIAL_PURCHASE event).	✅	✅	✅	✅	❌
Events Format
Webhook events are serialized in JSON. The body of a POST request to your server will contain the serialized event, as well as the API version.

Code
{
  "api_version": "1.0",
  "event": {
    "aliases": [
      "yourCustomerAliasedID",
      "yourCustomerAliasedID"
    ],
    "app_id": "yourAppID",
    "app_user_id": "yourCustomerAppUserID",
    "commission_percentage": 0.3,
    "country_code": "US",
    "currency": "USD",
    "entitlement_id": "pro_cat",
    "entitlement_ids": [
      "pro_cat"
    ],
    "environment": "PRODUCTION",
    "event_timestamp_ms": 1591121855319,
    "expiration_at_ms": 1591726653000,
    "id": "UniqueIdentifierOfEvent",
    "is_family_share": false,
    "offer_code": "free_month",
    "original_app_user_id": "OriginalAppUserID",
    "original_transaction_id": "1530648507000",
    "period_type": "NORMAL",
    "presented_offering_id": "OfferingID",
    "price": 2.49,
    "price_in_purchased_currency": 2.49,
    "product_id": "onemonth_no_trial",
    "purchased_at_ms": 1591121853000,
    "store": "APP_STORE",
    "subscriber_attributes": {
      "$Favorite Cat": {
        "updated_at_ms": 1581121853000,
        "value": "Garfield"
      }
    },
    "takehome_percentage": 0.7,
    "tax_percentage": 0.3,
    "transaction_id": "170000869511114",
    "type": "INITIAL_PURCHASE"
  }
}

Common Fields
Field	Type	Description	Possible Values
type	String	Type of the event.	TEST, INITIAL_PURCHASE, NON_RENEWING_PURCHASE, RENEWAL, PRODUCT_CHANGE, CANCELLATION, BILLING_ISSUE, SUBSCRIBER_ALIAS, SUBSCRIPTION_PAUSED, UNCANCELLATION, TRANSFER, SUBSCRIPTION_EXTENDED
id	String	Unique identifier of the event.	
app_id	String	Unique identifier of the app the event is associated with. Corresponds to an app within a project. This value will soon be visible in the app's configuration page in project settings.	
event_timestamp_ms	Integer	The time that the event was generated. Does not necessarily coincide with when the action that triggered the event occurred (purchased, cancelled, etc).	
app_user_id	String	Last seen app user id of the subscriber.	
original_app_user_id	String	The first app user id used by the subscriber.	
aliases	Array	All app user ids ever used by the subscriber.	
subscriber_attributes	Array	All attributes set for the customer, including common reserved attributes such as $idfa, $gpsAdId, $campaign, $keyword, etc.	
📘
When looking up users from the webhook in your systems, make sure to search both the original_app_user_id and the aliases array.

📘
If we have to retry a webhook for any reason, the retry will have the same id and event_timestamp_ms of the first attempt.

Subscription Lifecycle Events Fields
Field	Type	Description	Possible Values
product_id	String	Product identifier of the subscription. Please note: For Google Play products set up in RevenueCat after February 2023, this identifier has the format <subscription_id>:<base_plan_id>.	
entitlement_ids	Array	Entitlement identifiers of the subscription. It can be NULL if the product_id is not mapped to any entitlements.	
entitlement_id	String	Deprecated. See entitlement_ids.	Deprecated. See entitlement_ids.
period_type	String	Period type of the transaction.	TRIAL (for free trials), INTRO (for introductory pricing), NORMAL (standard subscription), PROMOTIONAL (for subscriptions granted through RevenueCat), PREPAID (for Play Store prepaid transactions).
purchased_at_ms	Integer	Time when the transaction was purchased. Measured in milliseconds since Unix epoch.	
grace_period_expiration_at_ms	Integer	Only available for BILLING_ISSUE events. The time that the grace period for the subscription would expire. Measured in milliseconds since Unix epoch. Use this field to determine if the user is currently in a grace period. It can be NULL if the subscription does not have a grace period.	
expiration_at_ms	Integer	Expiration of the transaction. Measured in milliseconds since Unix epoch. Use this field to determine if a subscription is still active. It can be NULL for non-subscription purchases.	
auto_resume_at_ms	Integer	The time when an Android subscription would resume after being paused. Measured in milliseconds since Unix epoch. Only available for Play Store subscriptions and SUBSCRIPTION_PAUSED events.	
store	String	Store the subscription belongs to.	AMAZON, APP_STORE, MAC_APP_STORE, PLAY_STORE, PROMOTIONAL, STRIPE.
environment	String	Store environment.	SANDBOX, PRODUCTION.
is_trial_conversion	Boolean	Only available for RENEWAL events. Whether the previous transaction was a free trial or not.	true or false.
cancel_reason	String	Only available for CANCELLATION events. See Cancellation and Expiration Reasons.	UNSUBSCRIBE, BILLING_ERROR, DEVELOPER_INITIATED, PRICE_INCREASE, CUSTOMER_SUPPORT, UNKNOWN.
expiration_reason	String	Only available for EXPIRATION events. See Cancellation and Expiration Reasons.	UNSUBSCRIBE, BILLING_ERROR, DEVELOPER_INITIATED, PRICE_INCREASE, CUSTOMER_SUPPORT, UNKNOWN.
new_product_id	String	Product identifier of the new product the subscriber has switched to. Only available for App Store subscriptions and PRODUCT_CHANGE events.	
presented_offering_id	String	Not available for apps using legacy entitlements. The identifier for the offering that was presented to the user during their initial purchase. Can be NULL if the purchase was made using purchaseProduct instead of purchasePackage or if the purchase was made outside of your app or before you integrated RevenueCat.	
price	Double	The USD price of the transaction. Can be NULL if the price is unknown, and 0 for free trials. Can be negative for refunds.	
currency	String	The ISO 4217 currency code that the product was purchased in. Can be NULL if the currency is unknown.	USD, CAD, etc.
price_in_purchased_currency	Double	The price of the transaction in the currency the product was purchased in. Can be NULL if the price is unknown, and 0 for free trials. Can be negative for refunds.	
tax_percentage	Double	The estimated percentage of the transaction price that was deducted for taxes (varies by country and store). Can be NULL if the tax percentage is unknown.	
commission_percentage	Double	The estimated percentage of the transaction price that was deducted as a store commission / processing fee. Can be NULL if the commission percentage is unknown.	
takehome_percentage	Double	DEPRECATED: The estimated percentage of the transaction price that will be paid out to developers after commissions, but before VAT and DST taxes are taken into account. We recommend using tax_percentage and commission_percentage to calculate proceeds instead. Learn more here.	
subscriber_attributes	Map	Map of attribute names to attribute objects. For more details see the customer attributes guide.	
transaction_id	String	Transaction identifier from Apple/Amazon/Google/Stripe.	
original_transaction_id	String	transaction_id of the original transaction in the subscription from Apple/Amazon/Google/Stripe.	
is_family_share	Boolean	Indicates if the user made this purchase or if it was shared to them via Family Sharing. Always false for non-Apple purchases.	true or false.
transferred_from	String[]	This field is only available when type is set to TRANSFER. App User ID(s) that transactions and entitlements are being taken from, and granted to transferred_to.	
transferred_to	String[]	This field is only available when type is set to TRANSFER. App User ID(s) that are receiving the transactions and entitlements taken from transferred_from.	
country_code	String	The ISO 3166 country code that the product was purchased in. The two-letter country code (e.g., US, GB, CA) of the app user's location (this country code is derived from the last seen request from the SDK for the subscriber.)	US, CA, etc.
offer_code	String	Not available when type is set to SUBSCRIBER_ALIAS or TRANSFER. The offer code that the customer used to redeem the transaction. Available for App Store and Play Store. For App Store this property corresponds to the offer_code_ref_name. For Play Store this corresponds to the promotionCode. Can be null if no offer code was used for this product.	
renewal_number	Integer	The number of renewals that this subscription has already gone through. Always starts at 1. Trial conversions are counted as renewals. is_trial_conversion is used to signify whether a transaction was a trial conversion.	
📘
To get the RevenueCat event id from a Subscription Lifecycle webhook, simply make an API call to our GET /subscribersendpoint with the app_user_id after receiving the webhook and look for the latest purchase in the subscription/non-subscription object.

📘Determine trial and subscription duration
To get a trial or subscription's duration from a webhook, you can subtract purchased_at_ms from expiration_at_ms and you will get the duration of the trial in milliseconds.

Cancellation and Expiration Reasons
Reason	Description	App Store	Play Store	Amazon	Web	Promo
UNSUBSCRIBE	Subscriber cancelled voluntarily. This event fires when a user unsubscribes, not when the subscription expires.	✅	✅	✅	✅	❌
BILLING_ERROR	Apple, Amazon, or Google could not charge the subscriber using their payment method. The CANCELLATION event with cancellation reason BILLING_ERROR is fired as soon as the billing issue has been detected. The EXPIRATION event with expiration reason BILLING_ERROR is fired if the grace period (if set up) has ended without recovering the payment, and the customer should lose access to the subscription.	✅	✅	✅	❌	❌
DEVELOPER_INITIATED	Developer cancelled the subscription.	✅	✅	❌	❌	✅
PRICE_INCREASE	Subscriber did not agree to a price increase.	✅	❌	❌	❌	❌
CUSTOMER_SUPPORT	Customer received a refund from Apple support, a Play Store subscription was refunded through RevenueCat, an Amazon subscription was refunded through Amazon support, or a web (RevenueCat Billing or Stripe Billing) subscription was refunded. Note that this does not mean that a subscription's autorenewal preference has been deactivated since refunds can be given without cancelling a subscription. You should check the current subscription status to check if the subscription is still active.	✅	✅	✅	✅	❌
UNKNOWN	Apple did not provide the reason for the cancellation.	✅	❌	❌	❌	❌
SUBSCRIPTION_PAUSED	The subscription expired because it was paused (only EXPIRATION event).	❌	✅	❌

Sample Events
These are some representative samples of webhooks you might receive from RevenueCat. Keep in mind that webhooks can include additional fields to what's shown here.

Initial Purchase
Renewal
Cancellation
Uncancellation
Non Renewing Purchase
Subscription Paused
{
    "event": {
        "event_timestamp_ms": 1658726378679,
        "product_id": "com.subscription.weekly",
        "period_type": "NORMAL",
        "purchased_at_ms": 1658726374000,
        "expiration_at_ms": 1659331174000,
        "environment": "PRODUCTION",
        "entitlement_id": null,
        "entitlement_ids": [
            "pro"
        ],
        "presented_offering_id": null,
        "transaction_id": "123456789012345",
        "original_transaction_id": "123456789012345",
        "is_family_share": false,
        "country_code": "US",
        "app_user_id": "1234567890",
        "aliases": [
            "$RCAnonymousID:8069238d6049ce87cc529853916d624c"
        ],
        "original_app_user_id": "$RCAnonymousID:87c6049c58069238dce29853916d624c",
        "currency": "USD",
        "price": 4.99,
        "price_in_purchased_currency": 4.99,
        "subscriber_attributes": {
            "$email": {
                "updated_at_ms": 1662955084635,
                "value": "firstlast@gmail.com"
            }
        },
        "store": "APP_STORE",
        "takehome_percentage": 0.7,
        "offer_code": null,
        "type": "INITIAL_PURCHASE",
        "id": "12345678-1234-1234-1234-123456789012",
        "app_id": "1234567890"
    },
    "api_version": "1.0"
}


Billing Issue
Expiration
Transfer
Refund
Product Change
Subscription Extended
{
  "event" : {
    "event_timestamp_ms" : 1601337601013,
    "product_id" : "com.revenuecat.myapp.monthly",
    "period_type" : "NORMAL",
    "purchased_at_ms" : 1598640647000,
    "expiration_at_ms" : 1601319047000,
    "environment" : "PRODUCTION",
    "entitlement_id" : "pro",
    "entitlement_ids" : [
      "pro"
    ],
    "presented_offering_id" : "defaultoffering",
    "transaction_id" : "100000000000002",
    "original_transaction_id" : "100000000000000",
    "app_user_id" : "$RCAnonymousID:12345678-1234-1234-1234-123456789123",
    "aliases" : [
      "$RCAnonymousID:12345678-1234-1234-1234-123456789123"
    ],
    "offer_code": "summer_special",
    "original_app_user_id" : "$RCAnonymousID:12345678-1234-1234-1234-123456789123",
    "currency" : "USD",
    "price" : 0,
    "price_in_purchased_currency" : 0,
    "subscriber_attributes" : {
      "$idfa" : {
        "value" : "12345678-1234-1234-1234-12345678912x",
        "updated_at_ms" : 1578018408238
      },
      "$appsflyerId" : {
        "value" : "1234567891234-1234567",
        "updated_at_ms" : 1578018408238
      }
    },
    "store" : "APP_STORE",
    "takehome_percentage" : 0.7,
    "tax_percentage": null,
    "commission_percentage": null,
    "type" : "BILLING_ISSUE",
    "id" : "12345678-1234-1234-1234-12345678912"
  },
  "api_version" : "1.0"
}


Billing Issue
Expiration
Transfer
Refund
Product Change
Subscription Extended
{
  "event": {
    "event_timestamp_ms": 1697451462232,
    "product_id": "com.subscription.weekly",
    "period_type": "NORMAL",
    "purchased_at_ms": 1696846623000,
    "expiration_at_ms": 1697451423000,
    "environment": "PRODUCTION",
    "entitlement_id": null,
    "entitlement_ids": ["pro"],
    "presented_offering_id": null,
    "transaction_id": "123456789012345",
    "original_transaction_id": "123456789012345",
    "is_family_share": false,
    "country_code": "US",
    "app_user_id": "1234567890",
    "aliases": ["$RCAnonymousID:8069238d6049ce87cc529853916d624c"],
    "original_app_user_id": "$RCAnonymousID:8069238d6049ce87cc529853916d624c",
    "expiration_reason": "UNSUBSCRIBE",
    "currency": "USD",
    "price": 0.00,
    "price_in_purchased_currency": 0.00,
    "subscriber_attributes": {
        "$email": {
            "updated_at_ms": 1662955084635,
            "value": "firstlast@gmail.com"
        }
    },
    "store": "APP_STORE",
    "takehome_percentage": 0.7,
    "offer_code": null,
    "tax_percentage": 0.012,
    "commission_percentage": 0.3,
    "type": "EXPIRATION",
    "id": "12345678-1234-1234-1234-123456789012",
    "app_id": "1234567890"
  },
  "api_version": "1.0"
}

Refund
Product Change
Subscription Extended
{
  "event" : {
    "event_timestamp_ms" : 1601337615995,
    "product_id" : "com.revenuecat.myapp.monthly",
    "period_type" : "NORMAL",
    "purchased_at_ms" : 1601258901000,
    "expiration_at_ms" : 1601336705000,
    "environment" : "PRODUCTION",
    "entitlement_id" : "pro",
    "entitlement_ids" : [
      "pro"
    ],
    "presented_offering_id" : "defaultoffering",
    "transaction_id" : "100000000000000",
    "original_transaction_id" : "100000000000000",
    "app_user_id" : "$RCAnonymousID:12345678-1234-ABCD-1234-123456789123",
    "aliases" : [
      "$RCAnonymousID:12345678-1234-1234-1234-123456789123",
      "user_1234"
    ],
    "offer_code": null,
    "original_app_user_id" : "$RCAnonymousID:12345678-1234-1234-1234-123456789123",
    "cancel_reason" : "CUSTOMER_SUPPORT",
    "currency" : "USD",
    "price" : -9.99,
    "price_in_purchased_currency" : -9.99,
    "subscriber_attributes" : {
      "$idfa" : {
        "value" : "12345678-1234-1234-1234-12345678912x",
        "updated_at_ms" : 1578018408238
      },
      "$appsflyerId" : {
        "value" : "1234567891234-1234567",
        "updated_at_ms" : 1578018408238
      }
    },
    "store" : "APP_STORE",
    "takehome_percentage" : 0.7,
    "tax_percentage": 0.1109,
    "commission_percentage": 0.15,
    "type" : "CANCELLATION",
    "id" : "12345678-1234-1234-1234-12345678912"
  },
  "api_version" : "1.0"
}

Subscription Extended
{
    "event": {
      "event_timestamp_ms": 1697451462232,
      "product_id": "com.subscription.weekly",
      "period_type": "NORMAL",
      "purchased_at_ms": 1696846623000,
      "expiration_at_ms": 1697451423000,
      "environment": "PRODUCTION",
      "entitlement_id": null,
      "entitlement_ids": ["pro"],
      "presented_offering_id": null,
      "transaction_id": "123456789012345",
      "original_transaction_id": "123456789012345",
      "is_family_share": false,
      "country_code": "US",
      "app_user_id": "1234567890",
      "aliases": ["$RCAnonymousID:8069238d6049ce87cc529853916d624c"],
      "original_app_user_id": "$RCAnonymousID:8069238d6049ce87cc529853916d624c",
      "currency": "USD",
      "price": 0.00,
      "price_in_purchased_currency": 0.00,
      "subscriber_attributes": {
          "$email": {
              "updated_at_ms": 1662955084635,
              "value": "firstlast@gmail.com"
          }
      },
      "store": "APP_STORE",
      "takehome_percentage": 0.7,
      "offer_code": null,
      "tax_percentage": 0.012,
      "commission_percentage": 0.3,
      "type": "SUBSCRIPTION_EXTENDED",
      "id": "12345678-1234-1234-1234-123456789012",
      "app_id": "1234567890"
    },
    "api_version": "1.0"
  }