Full Guide: Apple StoreKit 2 Receipt Verification in Dart (As of July 29, 2025)
This method should not be used inside your mobile Flutter app. It’s for learning or backend prototyping. You must use a server-side implementation (e.g., Python, Node.js, Go) for production.
What This Code Does
- Generates a JWT (JSON Web Token) signed with your Apple private key.
- Uses the JWT to fetch transaction info from Apple’s StoreKit 2 API.
- Decodes Apple’s signed response and verifies its digital signature.
- Checks if the subscription is still active (based on expiry time).
STEP 1: Get Apple API Credentials
To access StoreKit’s transaction verification API, you need credentials from App Store Connect:
Where to get credentials:
- Log in to: https://appstoreconnect.apple.com
- Go to: Users and Access → Keys
- Under the In-App Purchase section, click “+”
- Once created, you will get:
- Private Key (.p8 file) — download and store securely
- Key ID
- Issuer ID
These credentials are used to generate a JWT that authorizes your server to query Apple.
STEP 2: Add Dart Dependencies
To your pubspec.yaml, add:
dependencies:
http: ^0.13.6
jose: ^2.0.0
http: To make API requests to Applejose: To create and verify JWTs (supports ES256 signing)
STEP 3: Prepare Key and Metadata
const keyId = 'YOUR_KEY_ID';
const issuerId = 'YOUR_ISSUER_ID';
const bundleId = 'com.your.app';
const transactionId = 'YOUR_TRANSACTION_ID'; // From Apple receipt
const privateKeyPem = '''-----BEGIN PRIVATE KEY-----
YOUR_P8_PRIVATE_KEY_CONTENT_HERE
-----END PRIVATE KEY-----''';
keyIdandissuerId: From App Store ConnectbundleId: Your app’s bundle identifiertransactionId: From StoreKit purchase receipt JSONprivateKeyPem: Paste your downloaded.p8private key here (PEM format)
STEP 4: Generate the JWT
final iat = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final exp = iat + 1200; // JWT expires in 20 minutes
final claims = {
'iss': issuerId,
'iat': iat,
'exp': exp,
'aud': 'appstoreconnect-v1',
'bid': bundleId,
};
final jwtBuilder = JsonWebSignatureBuilder()
..jsonContent = claims
..addRecipient(JsonWebKey.fromPem(privateKeyPem, keyId: keyId), algorithm: 'ES256');
final jwt = jwtBuilder.build().toCompactSerialization();
What this does:
- JWT claims follow Apple’s requirement.
- We use ES256 to sign the JWT with our private key.
- This JWT is later sent to Apple for authorization.
STEP 5: Fetch Transaction Info from Apple
final response = await http.get(
Uri.parse('https://api.storekit-sandbox.itunes.apple.com/inApps/v1/transactions/$transactionId'),
headers: {'Authorization': 'Bearer $jwt'},
);
🔍 Key points:
- This is a sandbox endpoint — for testing only.
- Apple returns JSON with a
signedTransactionInfofield.
Check response:
if (response.statusCode != 200) {
print('❌ Failed to fetch transaction info');
return;
}
STEP 6: Decode and Verify Apple’s Signed Response
Apple responds with a JWS (JSON Web Signature), which we must verify using Apple’s embedded certificate.
final signedTransaction = jsonDecode(response.body)['signedTransactionInfo'];
final jws = JsonWebSignature.fromCompactSerialization(signedTransaction);
Extract the public cert from the JWS header:
final parts = signedTransaction.split('.');
final headerJson = json.decode(
utf8.decode(base64Url.decode(base64Url.normalize(parts[0]))),
);
final x5c = headerJson['x5c'][0];
Convert it to PEM:
String x5cToPem(String x5c) {
return '-----BEGIN CERTIFICATE-----\\n' +
x5c.replaceAllMapped(RegExp(r'.{1,64}'), (m) => m.group(0)! + '\\n') +
'-----END CERTIFICATE-----';
}
final certPem = x5cToPem(x5c);
final appleKey = JsonWebKey.fromPem(certPem);
Verify:
final verified = await jws.verify(JsonWebKeyStore()..addKey(appleKey));
if (!verified) {
print('❌ Signature verification failed.');
return;
}
STEP 7: Parse Payload and Check Subscription Status
Once verified, decode the payload:
final payload = json.decode(
utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))),
);
Check expiry:
final expires = payload['expiresDate'];
final now = DateTime.now().millisecondsSinceEpoch;
if (expires != null && now < expires) {
print('✅ Subscription is ACTIVE');
} else {
print('⚠️ Subscription is EXPIRED');
}
Why You Must Move This To a Backend Server
Running this Dart code in a mobile app is unsafe, because:
- Private keys can be extracted via reverse engineering.
- Attackers could impersonate your app and abuse Apple APIs.
- Apple may revoke your credentials for unsafe usage.
Recommended Server Options:
- Python (Flask/FastAPI using
jwt,cryptography,requests) - Node.js (
jose,jsonwebtoken,axios) - Go, Rust, PHP, etc.
Summary Table
| Step | Action | Secure? |
|---|---|---|
| 1 | Fetch Apple credentials | ✅ |
| 2 | Prepare Dart project | ✅ |
| 3 | Sign JWT using Apple private key | ❌ (must be server-side) |
| 4 | Call Apple transaction API | ❌ (must be server-side) |
| 5 | Verify Apple’s signature using certificate | ✅ if done on server |
| 6 | Decode and check expiration | ✅ |
