1. Go to this page and download the library: Download kkxdev/laravel-apple-iap library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
kkxdev / laravel-apple-iap example snippets
use Kkxdev\AppleIap\Facades\AppleIap;
// routes/api.php
use Illuminate\Http\Request;
use Kkxdev\AppleIap\Facades\AppleIap;
Route::post('/webhooks/apple', function (Request $request) {
AppleIap::processServerNotification($request->input('signedPayload'));
return response()->noContent();
})->middleware('apple-iap.verify-notification');
use Kkxdev\AppleIap\Events\SubscriptionPurchased;
use Kkxdev\AppleIap\Events\SubscriptionRenewed;
use Kkxdev\AppleIap\Events\SubscriptionExpired;
use Kkxdev\AppleIap\Events\SubscriptionCancelled;
use Kkxdev\AppleIap\Events\RefundIssued;
protected $listen = [
SubscriptionPurchased::class => [HandleSubscriptionPurchased::class],
SubscriptionRenewed::class => [HandleSubscriptionRenewed::class],
SubscriptionExpired::class => [HandleSubscriptionExpired::class],
SubscriptionCancelled::class => [HandleSubscriptionCancelled::class],
RefundIssued::class => [HandleRefundIssued::class],
];
namespace App\Listeners;
use Kkxdev\AppleIap\Events\SubscriptionRenewed;
class HandleSubscriptionRenewed
{
public function handle(SubscriptionRenewed $event): void
{
$tx = $event->transaction; // JwsTransaction
$renewal = $event->renewalInfo; // JwsRenewalInfo|null
$notification = $event->notification; // ServerNotification
// Update your database — the package does not touch it
\App\Models\Subscription::where(
'apple_original_transaction_id',
$tx->originalTransactionId
)->update([
'product_id' => $tx->productId,
'expires_at' => $tx->expiresDateAsDateTime(),
'auto_renews' => $renewal?->willAutoRenew(),
'environment' => $tx->environment,
]);
}
}
use Kkxdev\AppleIap\DTO\ServerApi\TransactionHistoryRequest;
use Kkxdev\AppleIap\Facades\AppleIap;
$history = AppleIap::getTransactionHistory($originalTransactionId);
foreach ($history->transactions as $tx) {
echo $tx->productId;
echo $tx->expiresDateAsDateTime()?->format('Y-m-d');
}
// With filters
$request = new TransactionHistoryRequest(
productTypes: ['Auto-Renewable Subscription'],
sort: 'DESCENDING',
);
$history = AppleIap::getTransactionHistory($originalTransactionId, $request);
// Paginate — hasMore indicates additional pages
while ($history->hasMore) {
$next = new TransactionHistoryRequest(revision: $history->revision);
$history = AppleIap::getTransactionHistory($originalTransactionId, $next);
}
$statuses = AppleIap::getAllSubscriptionStatuses($originalTransactionId);
if ($statuses->hasActiveSubscription()) {
// At least one subscription is active
}
foreach ($statuses->data as $group) {
foreach ($group->subscriptions as $sub) {
echo match(true) {
$sub->isActive() => 'Active',
$sub->isInGracePeriod() => 'Grace period',
$sub->isInBillingRetry()=> 'Billing retry',
$sub->isExpired() => 'Expired',
$sub->isRevoked() => 'Revoked',
default => 'Unknown',
};
}
}
$result = AppleIap::lookUpOrderId($orderId);
$refunds = AppleIap::getRefundHistory($originalTransactionId);
foreach ($refunds->transactions as $tx) {
echo "Refunded: {$tx->productId} on {$tx->purchaseDateAsDateTime()->format('Y-m-d')}";
}
use Kkxdev\AppleIap\DTO\ServerApi\ExtendRenewalDateRequest;
$request = new ExtendRenewalDateRequest(
extendByDays: 30,
extendReasonCode: 1, // 1 = customer satisfaction issue
requestIdentifier: 'unique-id-for-idempotency',
productId: 'com.example.app.premium',
);
$result = AppleIap::extendSubscriptionRenewalDate($originalTransactionId, $request);
if ($result->success) {
echo "New expiry: " . (new DateTime())->setTimestamp($result->effectiveDate / 1000)->format('Y-m-d');
}
$token = AppleIap::sendTestNotification();
// Check delivery status
$status = AppleIap::getTestNotificationStatus($token);
use Kkxdev\AppleIap\Facades\AppleIap;
use Kkxdev\AppleIap\Exceptions\JwsVerificationException;
try {
$transaction = AppleIap::decodeTransaction($jwsTransactionFromApp);
echo $transaction->transactionId;
echo $transaction->originalTransactionId;
echo $transaction->productId;
echo $transaction->type; // "Auto-Renewable Subscription", "Consumable", etc.
echo $transaction->environment; // "Production" or "Sandbox"
// Always verify the bundle ID matches your app to prevent cross-app replay attacks.
if (!$transaction->matchesBundleId()) {
abort(400, 'Bundle ID mismatch.');
}
if (!$transaction->isExpired()) {
// Grant entitlement
}
} catch (JwsVerificationException $e) {
// Token did not pass Apple CA chain verification — reject it
}
use Kkxdev\AppleIap\Facades\AppleIap;
use Kkxdev\AppleIap\Exceptions\AppleIapException;
$signature = AppleIap::generatePromotionalOfferSignature(
productIdentifier: 'com.example.app.pro.monthly',
offerIdentifier: 'monthly_winback_50_off', // the code you set in App Store Connect
applicationUsername: $user->apple_account_token ?? '',
);
// Return this to the iOS app:
return response()->json($signature->toArray());
use Illuminate\Http\Request;
use Kkxdev\AppleIap\Facades\AppleIap;
class PromotionalOfferController extends Controller
{
public function generate(Request $request)
{
$request->validate([
'product_id' => 'id), 403, 'Not eligible for this offer.');
$signature = AppleIap::generatePromotionalOfferSignature(
productIdentifier: $request->product_id,
offerIdentifier: $request->offer_id,
applicationUsername: $user->apple_account_token ?? '',
);
return response()->json($signature->toArray());
}
private function isEligible($user, string $productId): bool
{
// Only offer to users who have previously subscribed.
return $user->subscriptions()
->where('apple_product_id', $productId)
->whereNotNull('expired_at')
->exists();
}
}
use Kkxdev\AppleIap\Facades\AppleIap;
use Kkxdev\AppleIap\Exceptions\ReceiptValidationException;
try {
$response = AppleIap::validateReceipt($base64EncodedReceiptData);
if ($response->isValid()) {
foreach ($response->latestReceiptInfo as $purchase) {
echo $purchase->productId;
echo $purchase->expiresDateAsDateTime()?->format('Y-m-d');
if (!$purchase->isExpired() && !$purchase->isCancelled()) {
// Active purchase
}
}
// Find most recent purchase for a specific product
$latest = $response->latestPurchaseFor('com.example.app.premium');
}
} catch (ReceiptValidationException $e) {
echo "Status {$e->getStatusCode()}: {$e->getMessage()}";
}
use Kkxdev\AppleIap\Contracts\AppStoreServerApiInterface;
use Kkxdev\AppleIap\Contracts\JwsVerifierInterface;
use Kkxdev\AppleIap\Contracts\NotificationVerifierInterface;
use Kkxdev\AppleIap\Contracts\ReceiptValidatorInterface;
class SubscriptionService
{
public function __construct(
private AppStoreServerApiInterface $serverApi,
private JwsVerifierInterface $jwsVerifier,
) {}
}
use Kkxdev\AppleIap\Exceptions\AppleIapException;
use Kkxdev\AppleIap\Exceptions\CircuitBreakerOpenException;
use Kkxdev\AppleIap\Exceptions\NetworkException;
try {
$statuses = AppleIap::getAllSubscriptionStatuses($originalTransactionId);
} catch (CircuitBreakerOpenException $e) {
// Apple API is temporarily unavailable — return cached state
return Cache::get("subscription:{$userId}");
} catch (NetworkException $e) {
// Transient network failure
Log::warning('Apple IAP network failure', ['error' => $e->getMessage()]);
} catch (AppleIapException $e) {
// Any other package exception
Log::error('Apple IAP error', ['error' => $e->getMessage()]);
}
use Illuminate\Support\Facades\Event;
use Kkxdev\AppleIap\Events\SubscriptionRenewed;
Event::fake();
// ... trigger notification processing ...
Event::assertDispatched(SubscriptionRenewed::class, function ($event) use ($originalTransactionId) {
return $event->transaction->originalTransactionId === $originalTransactionId;
});
use Kkxdev\AppleIap\Contracts\NotificationVerifierInterface;
use Kkxdev\AppleIap\DTO\Notification\ServerNotification;
$mock = $this->mock(NotificationVerifierInterface::class);
$mock->shouldReceive('verify')
->once()
->andReturn($this->makeTestNotification());
use Kkxdev\AppleIap\DTO\Enums\ProductType;
ProductType::AUTO_RENEWABLE_SUBSCRIPTION // 'Auto-Renewable Subscription'
ProductType::NON_CONSUMABLE // 'Non-Consumable'
ProductType::CONSUMABLE // 'Consumable'
ProductType::NON_RENEWING_SUBSCRIPTION // 'Non-Renewing Subscription'
ProductType::isSubscription($type); // true for auto-renewable and non-renewing