szohan/appstore-server-notifications
A Laravel package for handling Apple App Store Server Notifications. Receive webhook callbacks, decode signed notification payloads, and process subscription and transaction events in your PHP application.
Install and publish config
composer require szohan/appstore-server-notifications
php artisan vendor:publish --provider="Szohan\AppStoreServerNotifications\ServiceProvider"
The exact publish tag is worth checking in the README — packages this small often publish a single appstore.php config file. Look there first for the route path, environment toggle (sandbox vs. production), and your bundle ID.
Configure the essentials — minimally you need your Apple bundle ID and a flag for which Apple environment the endpoint is serving. Set these in .env rather than editing config directly, especially if you plan to run sandbox and production endpoints from the same codebase.
Find the route. The package is a "request handler," so the main value is a pre-wired POST endpoint (something like /appstore/notifications). Check routes/ in the package or php artisan route:list | grep -i app — you'll either need to register it yourself or it's auto-loaded. Confirm before wiring it into App Store Connect.
Wire it into App Store Connect. Under App Information → App Store Server Notifications, set the Sandbox URL to your staging endpoint and the Production URL to your live one. Start with sandbox — this is where you'll iterate.
First use case: receive a DID_RENEW or INITIAL_BUY notification, decode the payload, and log it. That's the smallest useful loop. Once you can see a decoded payload in storage/logs/laravel.log, you're ready to build real handlers.
Event-listener dispatch. Most packages in this category decode the JWS payload and dispatch an event (or expose a callback) keyed on notificationType. Rather than one giant switch, register dedicated listeners per type: SUBSCRIBED, DID_RENEW, EXPIRED, DID_FAIL_TO_RENEW, REFUND, GRACE_PERIOD_EXPIRED. Keep them small and let a shared subscription-state service do the actual mutation.
Persist first, process second. Always write the raw notification (with its notificationUUID) to an apple_notifications table before doing any work. This gives you an audit trail, a replay mechanism if you break a handler, and — critically — an idempotency key. Use notificationUUID as a unique index; Apple retries aggressively and you will see duplicates.
Reconcile, don't reimplement. Notifications are a hint that something changed, not a source of truth for entitlement. Use the transactionId / originalTransactionId in the payload to look up your local subscription record and reconcile. When in doubt, call the App Store Server API to fetch the current transaction state — never derive "is this user still subscribed?" purely from the notification type.
Queue the side effects. The endpoint handler should verify the payload, persist it, and return 200 — nothing more. Dispatch jobs for emails, entitlement flips, dunning emails, refund handling. This keeps the request fast (Apple times out) and lets you retry failures internally rather than relying on Apple's retry window.
Test with a real payload. App Store Connect has a "Request a Test Notification" button — use it. Also keep a fixture of a real signed payload in tests/ and feed it through your handler with a mocked verifier so you can unit-test listeners without hitting Apple.
signedPayload is a JWS signed by Apple. Verify against Apple's root certificates (AppleRootCA-G3.cer etc.). If the package does this for you, good — confirm it, because some lightweight handlers only base64-decode and call it a day. That's a security hole.signedPayload decodes to a JSON object with data.signedTransactionInfo and data.signedRenewalInfo, each of which is another JWS that needs independent decoding. If your handler assumes one flat payload, you'll get garbled data.200 as fast as possible. If your handler throws, wrap it — an uncaught exception bubbles up as a 500 and you'll get the same notification re-delivered for hours.notificationUUID is not optional.environment field tells you which Apple environment sent it. A sandbox notification landing on your production endpoint means misconfiguration — reject or log loudly.composer.json, read the package source before relying on it, and have a plan B. Apple's official apple/app-store-server-library-php is the safest fallback if the package stalls — it handles JWS verification and the notification schema authoritatively.signedPayload alongside the exception. Payloads are large; truncate in logs but keep the full version in a dead-letter table. You'll need it to reproduce.How can I help you explore Laravel packages today?