What does a .verified StoreKit result prove, what does it not prove, and when would you read unsafePayloadValue?
Predict first
Do the generic checked(_:) helper and the unsafe/raw accessors compile?
import StoreKit
struct NotVerified: Error { let reason: String }
func checked<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .unverified(_, let error):
switch error {
case .invalidSignature, .invalidCertificateChain, .revokedCertificate: throw NotVerified(reason: "signature or chain")
case .invalidDeviceVerification: throw NotVerified(reason: "device")
default: throw NotVerified(reason: "\(error)")
}
case .verified(let value): return value
}
}
func inspect(_ result: VerificationResult<Transaction>) {
let raw: String = result.jwsRepresentation
let unsafe: Transaction = result.unsafePayloadValue
print(raw.split(separator: ".").count, unsafe.id)
}
print("compiled")
- Yes, the helper switches on VerificationResult and VerificationError and the raw JWS and unsafe payload are readable
- No: VerificationResult has no jwsRepresentation
- No: VerificationError cannot be switched over
Mental model. A verified result is a banknote with an intact watermark: it proves who printed it, not whose pocket it came from or whether the shop has since cancelled it.
Every Transaction, RenewalInfo and AppTransaction arrives wrapped in a VerificationResult: .verified(value) or .unverified(value, error). .verified means StoreKit checked the JWS signature and certificate chain against Apple's root (and the device/app binding) on this device, so the payload was signed by the App Store and not modified in transit or on disk. The VerificationError says why a result failed (invalid signature, invalid certificate chain, revoked certificate, invalid device verification, invalid encoding, missing required properties). The check shows the idiom: one generic helper that throws for .unverified and returns the value otherwise, used for every result type so no call site forgets.
What it does not prove: that the purchase belongs to your backend user (use appAccountToken), that it is still valid today (a refund after signing is a new transaction update), or that your own device is trustworthy (a jailbroken device can lie to local code, which is why anything valuable needs server-side verification). Reading result.unsafePayloadValue or jwsRepresentation of an unverified result is legitimate for logging, analytics, or forwarding the raw JWS to your server, which then performs its own verification; never grant content from it. jwsRepresentation is a compact three-part string, so a server can verify it independently with the same chain rules as the next section. Make the policy explicit: when a result is unverified you may deliver nothing, or show a "we are verifying your purchase" state and retry, but do not silently fall back to granting.
This only confirms the public API shape of VerificationResult.
Common traps
- Using
unsafePayloadValuefor access decisions. The name is the warning; it skips the signature check. - Skipping verification because the purchase "just happened". Entitlements and updates carry the same wrapper; check all of them.
They might follow up with
- Does verification need the network? StoreKit can verify offline against cached certificates; revocation checks may need connectivity.
- How do I verify on the server? See the JWS and certificate-chain questions in 28.4.
Related questions
- 28.1.2 What can product.purchase() return or throw, and how should each outcome behave in the UI?
- 28.1.4 Why start a Transaction.updates listener at launch, and what do finish() and idempotent delivery guarantee?
- 28.4.1 How is a StoreKit JWS built, and what must a server check before trusting signedTransactionInfo?
- 17.4.2 How does StoreKit 2 work: products, purchases, transaction verification, entitlements, refunds, and what must…
← What can product.purchase() return or throw, and how should each… Why start a Transaction.updates listener at launch, and what do… →