What does a .verified StoreKit result prove, what does it not prove, and when would you read unsafePayloadValue?

28.1.3 · L3 · Interview

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 unsafePayloadValue for 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… →