Your app
iPhone: StoreKit 2 with Swift
StoreKit 2 already gives you a signed transaction. Send it to Vend16, finish it once we’ve answered, and unlock from our answer. No server of your own, and no library.
1Get your public key
Create a free account, verify your email, and the setup steps ask for your app’s name, iOS bundle ID and Android package name. You get a public key (pk_…, it goes in the app) and a secret key (sk_…, only for a server, if you have one). Both are shown once, so copy them.
2Add it to your app
No library to install. In Xcode, add the In-App Purchase capability (target, Signing and Capabilities, + Capability). Then a small client:
import Foundation
import StoreKit
enum Vend16 {
static let key = "pk_your_public_key" // public key, safe in the app
static let base = URL(string: "https://vend16.com")!
enum Outcome { case finish, finishAndUnlockNothing, tryLater }
/// Sends the signed transaction to Vend16, which checks it with Apple.
static func report(_ jws: String, userId: String) async -> Outcome {
var req = URLRequest(url: base.appendingPathComponent("v1/receipts"))
req.httpMethod = "POST"
req.setValue("Bearer \(key)", forHTTPHeaderField: "Authorization")
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try? JSONSerialization.data(withJSONObject: [
"platform": "ios", "token": jws, "appUserId": userId,
])
guard let reply = try? await URLSession.shared.data(for: req),
let code = (reply.1 as? HTTPURLResponse)?.statusCode else { return .tryLater }
let data = reply.0
if code == 422 { return .finishAndUnlockNothing }
guard code == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let status = json["status"] as? String else { return .tryLater }
return ["recorded", "duplicate", "revoked"].contains(status) ? .finish : .tryLater
}
/// What this user has paid for, as Vend16 sees it.
static func activeProducts(userId: String) async throws -> Set<String> {
var req = URLRequest(url: base.appendingPathComponent("v1/subscribers/\(userId)"))
req.setValue("Bearer \(key)", forHTTPHeaderField: "Authorization")
let (data, _) = try await URLSession.shared.data(for: req)
struct Subscriber: Decodable { let active_product_ids: [String] }
return Set(try JSONDecoder().decode(Subscriber.self, from: data).active_product_ids)
}
}Buy, listen for updates, finish only after Vend16 answers, and restore:
@MainActor final class Store: ObservableObject {
@Published var active: Set<String> = []
let userId: String
private var updates: Task<Void, Never>?
init(userId: String) {
self.userId = userId
// Renewals, Ask to Buy approvals and purchases interrupted last time.
updates = Task { [weak self] in
for await result in Transaction.updates { await self?.handle(result) }
}
}
func buy(_ product: Product) async throws {
let result = try await product.purchase()
if case .success(let verification) = result { await handle(verification) }
}
func handle(_ verification: VerificationResult<Transaction>) async {
// Vend16 checks Apple's signature itself, so send the JWS as it came.
let outcome = await Vend16.report(verification.jwsRepresentation, userId: userId)
if outcome != .tryLater { await verification.unsafePayloadValue.finish() }
await refresh()
}
func refresh() async {
if let ids = try? await Vend16.activeProducts(userId: userId) { active = ids }
}
// A Restore button: App Store sync, then everything goes through Vend16 again.
func restore() async {
try? await AppStore.sync()
for await result in Transaction.currentEntitlements { await handle(result) }
}
}3Keep the money safe
- Tell the store a purchase is finished only after Vend16 answers
recorded,duplicateorrevoked. If the answer ispending, or Vend16 can’t be reached (a502or no network), leave it unfinished. The store hands it back on the next launch and you try again. - If Vend16 answers
422 invalid_receipt, finish it and unlock nothing. The store says it isn’t a real purchase. - Unlock from
active_product_idsinGET /v1/subscribers/:appUserId, never from a flag on the phone. Check it at launch and after every purchase.
Use a stable user ID from your own sign-in (or a UUID you keep on the device), never an email address.
4Connect the stores
Your code is done. The stores need to know about your products and about Vend16:
Stuck? Email hello@spruik.co and a real person replies.