Apple StoreKit 2 Receipt Verification (As of July 29, 2025) – Zaigo Infotech Software Solutions

Let’s craft brilliance together!

Request a free consultation and get a no-obligation quote for your project within one working day.

Company-Logo

Error: Contact form not found.

Apple StoreKit 2 Receipt Verification (As of July 29, 2025)

Flutter

Mobile App Android

Mobile App Ios

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

  1. Generates a JWT (JSON Web Token) signed with your Apple private key.
  2. Uses the JWT to fetch transaction info from Apple’s StoreKit 2 API.
  3. Decodes Apple’s signed response and verifies its digital signature.
  4. 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:

  1. Log in to: https://appstoreconnect.apple.com
  2. Go to: Users and Access → Keys
  3. Under the In-App Purchase section, click “+”
  4. 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 Apple
  • jose: 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-----''';
  • keyId and issuerId: From App Store Connect
  • bundleId: Your app’s bundle identifier
  • transactionId: From StoreKit purchase receipt JSON
  • privateKeyPem: Paste your downloaded .p8 private 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 signedTransactionInfo field.

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

 

Can't find what you are looking for?

Post your query now, and we will get in touch with you soon!

    Want to start a project?

    Our team is ready to implement your ideas. Contact us now to discuss your roadmap!

    GET IN TOUCH

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    INDIA

    9thfloor, (9A & 9B) Sapna Trade Centre, 135,
    Old 109, Poonamallee High Rd, Egmore,
    Chennai, Tamil Nadu 600084

    +91 9884783216

    marketing@zaigoinfotech.com

    USA

    170 Post Rd #211, Fairfield,
    CT 06824,
    USA

    +1 904-672-8617

    sales@zaigoinfotech.com