Documentation
App Store revenue
App Store Server Notifications V2: one URL, no secret, and what to do when your own backend already holds Apple's only slot.
How App Store revenue reaches us
Apple sends App Store Server Notifications V2 straight to a URL, and signs each one itself. We verify the signature against Apple's certificate chain, check that the bundle id is this app's, and read the price, currency, storefront and buyer from the signed transaction inside. There is no secret to paste and nothing to configure beyond the URL -- purchases, renewals, offers and refunds all arrive this way.
1. Set the notification URL
- App Store Connect → your app → App Information → App Store Server Notifications. Choose Version 2.
- Set both the Production Server URL and the Sandbox Server URL to the address on the app's Revenue tab (*Or take App Store revenue directly*). Sandbox is where every test purchase happens; leaving it blank is the commonest reason a test never arrives.
- Make sure the bundle id on the App details tab matches App Store Connect exactly. Notifications for any other bundle are refused.
https://api.roassensor.com/api/tracking/webhooks/appstore/YOUR_PUBLIC_KEYApple offers no "send test" button; the first sandbox purchase is the test. Sandbox purchases arrive flagged is_test, are attributed normally, and stay out of reported revenue.
2. If your own backend already receives Apple's notifications
Apple allows one notification URL per app, and a backend that manages entitlements from these notifications cannot give it up. So point Apple at us and we forward every notification to you, untouched:
- Revenue tab → *Forward App Store notifications* → paste your existing endpoint → Save.
- Press Test. It reports *reachable* on any HTTP answer, 4xx included -- a handler that parses
signedPayloadrightly rejects a test body. Only a real forward sets the last-status badge. - Then change the URL in App Store Connect to ours.
The body goes out byte for byte with only a Content-Type header, so your receiver verifies Apple's signature over exactly the bytes Apple produced and cannot tell it was relayed. Every verified notification type is forwarded, including the ones we do not book (EXPIRED, DID_CHANGE_RENEWAL_STATUS, …), because a subset would never expire anyone.
Delivery is durable: a 5xx, a timeout or a 429 from your side is retried with backoff for hours, because once we hold Apple's slot, Apple's own retries end at us. A 4xx parks the notification -- the same bytes will not become acceptable -- and the Revenue tab shows a Retry for it. Your URL must be https and public; private, loopback and link-local targets are refused.
3. The buyer, and the app's part
Apple's notification names the transaction, not the buyer. The app supplies the link by passing Roas.appAccountToken() as StoreKit's appAccountToken on every purchase -- a UUID derived from the visitor id, which the server reverses. Each SDK page shows the line. Without it sales book as unattributed.
What books, and what does not
| Apple notification | Treatment |
|---|---|
SUBSCRIBED, DID_RENEW, ONE_TIME_CHARGE, OFFER_REDEEMED | Books revenue; each renewal is its own conversion |
REFUND | Books a negative conversion reversing the original |
EXPIRED, DID_CHANGE_RENEWAL_STATUS, GRACE_PERIOD_EXPIRED, DID_FAIL_TO_RENEW, price increases, … | Acknowledged (and forwarded, if forwarding is on), books nothing |
A free trial or introductory offer books what was charged -- zero for a trial -- with the offer phase recorded, so the first paid renewal arrives as its own conversion at full price. The storefront country is kept on the conversion, so a sale in one region is reported against the spend that bought it.
4. Optional: confirm purchases the instant they happen
Apple's notification is fast but not instant, and the dashboard is empty until it lands. With an In-App Purchase key on your account, the SDK's verifyPurchase(transactionId:) lets us ask Apple about a transaction the moment the app sees it. We only ever *ask*: the amount always comes from Apple's own signed copy, never from the app, and the later notification for the same sale is recognised, not double-counted.
- App Store Connect → Users and Access → Integrations → In-App Purchase → Generate key. Note the Key ID and the Issuer ID shown above the key list, and download the
.p8once (Apple never shows it again). - Revenue tab → In-App Purchase key → Add: name, Key ID, Issuer ID, and the
.p8contents. One key covers every app on your Apple team; select it on each app.
When nothing books: the checklist
| Check | If it is wrong |
|---|---|
| Is the Sandbox URL set, not only Production? | Test purchases are sandbox purchases. Set both. |
| Is the notification version V2? | V1 payloads are not accepted. |
| Does the bundle id on App details match App Store Connect? | Notifications for another bundle are refused. |
| Is Apple's URL pointing at your own backend? | Either point it at us and forward (§2), or nothing reaches us. |
| Was the purchase made with a Sandbox Apple ID? | Settings → App Store → Sandbox Account on the device, or a TestFlight build. |
verifyPurchase answers 422? | No In-App Purchase key on the account, or the transaction is from a .storekit file. |

