How do you authenticate to the App Store Server API, and what do you use it for?

28.4.4 · L5 · Interview

Predict first

What does the JWT contain, and is its lifetime acceptable?

import Foundation
import CryptoKit

func b64url(_ d: Data) -> String {
    d.base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "")
}
func unb64url(_ s: String) -> Data {
    var t = s.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
    while t.count % 4 != 0 { t += "=" }
    return Data(base64Encoded: t)!
}
func makeJWT(key: P256.Signing.PrivateKey, keyID: String, issuer: String, bundleID: String, now: Int) -> String {
    let h = try! JSONSerialization.data(withJSONObject: ["alg": "ES256", "kid": keyID, "typ": "JWT"], options: [.sortedKeys])
    let c = try! JSONSerialization.data(withJSONObject: ["iss": issuer, "iat": now, "exp": now + 1800, "aud": "appstoreconnect-v1", "bid": bundleID] as [String: Any], options: [.sortedKeys])
    let input = b64url(h) + "." + b64url(c)
    return input + "." + b64url(try! key.signature(for: Data(input.utf8)).rawRepresentation)
}
let key = P256.Signing.PrivateKey()
let jwt = makeJWT(key: key, keyID: "ABC123DEFG", issuer: "69a6de70-0000-0000-0000-000000000000", bundleID: "com.example.app", now: 1_800_000_000)
let p = jwt.split(separator: ".").map(String.init)
print(String(data: unb64url(p[0]), encoding: .utf8)!)
print(String(data: unb64url(p[1]), encoding: .utf8)!)
let ok = key.publicKey.isValidSignature(try! P256.Signing.ECDSASignature(rawRepresentation: unb64url(p[2])), for: Data((p[0] + "." + p[1]).utf8))
print("signature valid:", ok)
let claims = try! JSONSerialization.jsonObject(with: unb64url(p[1])) as! [String: Any]
print("lifetime minutes:", ((claims["exp"] as! Int) - (claims["iat"] as! Int)) / 60, "(must be at most 60)")
let tid = "2000000123456789"
for (env, host) in [("production", "api.storekit.itunes.apple.com"), ("sandbox", "api.storekit-sandbox.itunes.apple.com")] {
    print(env, URL(string: "https://\(host)/inApps/v1/history/\(tid)")!.absoluteString)
}
  • header kid ABC123DEFG, claims with aud appstoreconnect-v1, valid signature, 30 minutes
  • the lifetime is 90 minutes
  • the signature does not verify

Mental model. The API token is a visitor badge your server wears: it proves who you are for a short while, while the data you receive still has to be checked for Apple's seal.

The App Store Server API (REST, JSON) is authenticated by a JWT you sign with an In-App Purchase key from App Store Connect (a .p8 ES256 private key, with a key id and an issuer id). Header: alg ES256, kid (key id), typ JWT; claims: iss (issuer id), iat, exp (at most 60 minutes after issue), aud appstoreconnect-v1, and bid (bundle id). Send it as a Bearer token. The check signs one with CryptoKit and verifies it, prints header and claims, confirms a 30 minute lifetime, and shows the base URLs for the two environments (api.storekit.itunes.apple.com and api.storekit-sandbox.itunes.apple.com). No request was sent. Cache the token until shortly before expiry, keep the key in a secrets manager, never in the app, and rotate it.

Typical endpoints (check the current reference for exact paths and versions): transaction history for a transaction id, all subscription statuses (the source of truth when notifications are in doubt), transaction info, order-id lookup for customer support, refund history, notification history and test notifications, send consumption information (answering a refund request), and extend a subscription renewal date (compensating users after an outage). Use the id from the client (transaction.id or originalID) or from a notification as the entry point. Responses contain signed transaction and renewal JWS values that you verify as before, and the API is rate limited, so use notifications for state and the API for reconciliation and support. Apple's open source server libraries wrap JWT creation, requests and verification.

The token is a short-lived ES256 JWT; the URLs differ only by host.

Common traps

  • Shipping the .p8 key in the app. Anyone can then call the API as you; keep it server side.
  • Long-lived tokens. More than 60 minutes is rejected; mint new tokens regularly.

They might follow up with

  • How do you handle the sandbox? Use the sandbox host and transaction ids from sandbox purchases; ids do not cross environments.
  • When is polling justified? Rarely; for reconciliation or onboarding a user whose notification you missed.

Related questions

  • 28.4.3 How do App Store Server Notifications V2 behave, and how do you process them idempotently and out of order?
  • 28.4.1 How is a StoreKit JWS built, and what must a server check before trusting signedTransactionInfo?
  • 28.6.2 Design the billing and entitlement platform for an app with iOS, Android and web subscriptions and a backend…
  • 12.2.2 How do TLS and App Transport Security protect a request, and what is certificate pinning, and why is it…
  • 14.4.2 Why are secrets in the app binary not secret, and what do you do instead?

← How do App Store Server Notifications V2 behave, and how do you…   What replaced the receipt, and how do you grandfather paid users with… →