Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Store Receipt Validator Laravel Package

aporat/store-receipt-validator

A Laravel/PHP package for validating Apple App Store receipts, including auto-renewable subscriptions and StoreKit purchases. It supports sandbox and production environments, provides a simple API, helps verify transactions, and adds secure receipt validation to your app's backend.

Deep Wiki
Context7

Getting Started

This is a framework-agnostic PHP library, not a Laravel package — there is no service provider or publishable config. Install it and wrap it yourself.

composer require aporat/store-receipt-validator

Pin the major version explicitly (^4.0 or whatever the current major is). The namespaces and validator constructors have changed across majors, so an unpinned upgrade can break your checkout flow silently.

Where to look first, in order:

  1. README.md — the per-platform examples are the only reliable source for the current constructor/setter names for your pinned version.
  2. src/iTunes/, src/GooglePlay/, src/Amazon/ — one directory per platform, each with a Validator and a Response class.
  3. src/iTunes/Response.php (and equivalents) — this is the DTO you will actually spend your time with: isValid(), getResultCode()/getStatus(), getPurchases(), getLatestReceiptInfo(), getTransactionId(), getOriginalTransactionId(), getExpiresDate().

First use case — validating an Apple non-consumable before granting an entitlement:

use ReceiptValidator\iTunes\Validator as iTunesValidator;

$validator = (new iTunesValidator())
    ->setReceiptData($request->string('receipt'))   // base64 blob from StoreKit
    ->setEndpoint(config('services.apple.endpoint')) // production | sandbox
    ->setSharedSecret(config('services.apple.shared_secret')); // auto-renewables only

$response = $validator->validate();

if (! $response->isValid()) {
    // 21006 = subscription expired, 21007 = sandbox receipt sent to prod, 21008 = inverse
    abort(422, 'Invalid receipt');
}

Google Play needs a configured Google_Client with a service-account JSON; Amazon needs a developer secret and a receipt ID. Both follow the same set*()->validate() shape.

Implementation Patterns

Wrap the library behind one application-level service. Do not sprinkle new Validator() through controllers. Define an interface like ReceiptVerifier::verify(Platform $p, string $payload): VerifiedPurchase and bind it in a service provider. This gives you a single seam for tests, for logging, and for a future migration to Apple's App Store Server API or Google's purchases.subscriptionsv2 endpoint.

Keep credentials in config/services.php, never in the client. Apple shared secret, Google service-account JSON path, Amazon developer secret — all env-driven. Note the Apple shared secret is per-app and only required for auto-renewable subscriptions; skip it for consumables/non-consumables.

The controller flow: form request validates the platform + payload → call the verifier → assert the returned productId and bundleId against a server-side allowlist (never trust what the client claims it bought) → DB::transaction(fn () => grantEntitlement(...)) → return the entitlement. The receipt is evidence, not the source of truth for what was purchased.

Idempotency is the core pattern. Clients retry, users double-tap, and Apple replays receipts. Store a purchases row keyed on a unique index over (platform, original_transaction_id, environment) and use firstOrCreate/upsert inside the transaction. Granting an entitlement twice because of a retried POST is the most common production bug with this kind of library.

Subscriptions: treat original_transaction_id as the stable subscription identity and transaction_id as the individual renewal. Use getLatestReceiptInfo() to roll forward through renewals, persist expires_date_ms as a UTC timestamp, and compare against now() at request time rather than trusting isValid() alone — a 21006 response still tells you when it lapsed. Model grace periods and is_upgraded/is_in_intro_offer_period explicitly if your tier logic depends on them.

Sandbox fallback: drive the endpoint from config, but for Apple add an explicit retry — if production returns 21007, re-submit once against the sandbox endpoint; if sandbox returns 21008, re-submit against production. Encapsulate this inside ReceiptVerifier so callers never see it.

Queue the work where it can be slow. Validation is an outbound HTTPS call to a third party. For webhook-driven or batch flows, dispatch a job with $tries = 3 and backoff() — but classify failures first: ConnectionException/5xx are retryable, "receipt invalid" is terminal and should fail fast without burning retries.

Testing: bind a fake ReceiptVerifier in your test container for feature tests, and keep a handful of real (anonymized) fixture receipts for integration tests against the library itself. Mock the HTTP layer (Guzzle MockHandler) rather than the library classes when you want to test your error mapping — 21007, 21006, Google 400s, and Amazon 400/496 each need a distinct branch in your code.

Pair it with server notifications. Validation is pull-based. Refunds, chargebacks, and cancellations will not appear in a validation response you already stored. If revenue integrity matters, run App Store Server Notifications V2 and Google Real-time Developer Notifications alongside this library; this package alone cannot keep entitlements accurate over time for subscriptions.

Gotchas and Tips

  • isValid() is not uniform across platforms. On iTunes it's derived from result code 0 (and treats 21006 as "valid but expired" in some versions); on Google it reflects the purchase state. Branch on the explicit result/status code and assert validity yourself rather than relying on one boolean.
  • 21007/21008 are the classic trap. A TestFlight or review build hits your production endpoint. Handle the fallback loop or your testers and App Review will report broken purchases.
  • Apple receipt blobs are base64 and can be large. With many auto-renewable renewals the receipt grows past what some proxies or post_max_size allow. Send it as a POST body field, not a URL parameter, and check body size limits.
  • Apple sends latest_receipt and latest_receipt_info. They are not the same; prefer the decoded transaction array and don't confuse the two when rolling subscriptions forward.
  • Google wants purchaseToken, not orderId. Tokens expire/rotate on some flows; persist the token from the client at purchase time. Also confirm the Android Publisher API is enabled and the service account has the right Play Console permissions — misconfiguration surfaces as an opaque 401/403, not a friendly error.
  • Google purchases must be acknowledged within three days (or consumed) or they auto-refund. This library validates; it does not necessarily acknowledge. Check your version's API and call acknowledge/consume explicitly.
  • Amazon's developerSecret is a server secret and Amazon's endpoint has sandbox vs live variants for a reason. Never let the client choose the endpoint.
  • Timezones and epoch units. Apple returns milliseconds since epoch (expires_date_ms); Carbon::createFromTimestampMs() or divide by 1000. Mixing seconds and ms produces silently wrong expiry dates that only show up months later.
  • Never validate on app launch. One validation per purchase/renewal event, cached. Validation calls are rate-limited upstream and slow; a startup check turns into a latency and quota problem.
  • Don't log raw receipts or tokens. They're replayable bearer credentials. Log the platform, result code, transaction ID, and product ID only.
  • Duplicate transaction IDs across environments are real — include environment in your unique index, or sandbox testers will collide with production rows.
  • API churn across majors. Class names, namespaces, and setter chains have moved between major releases; the README for your installed version is the spec, and pinning the major is cheap insurance.
  • Extension point: the library is deliberately thin, so subclassing validators isn't usually worth it. The useful extension point is your own ReceiptVerifier interface with per-platform adapters — that lets you swap in Apple's newer Server API or a queue-and-webhook architecture without touching controllers.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package