stubkit docs

Concepts

User mapping

Apple, Google Play, and Stripe each issue their own opaque purchase identifiers — Apple gives you an originalTransactionId, Google a purchaseToken, Stripe a customer/subscription ID. None of these match the user IDs in your own database. Stubkit looks up entitlements by your user_id, so the purchase record needs to carry that ID through the store webhook.

Each store has a single field for this. Set it the moment you start a purchase and you never have to think about user mapping again.

Apple — appAccountToken

Pass a UUID that maps to your user when creating the StoreKit purchase. Apple includes it in every Server Notification V2 we process, so the entitlement is keyed on the right user from the first receipt onward.

// Swift / StoreKit 2
let userUuid = UUID(uuidString: currentUser.id)!
let result = try await product.purchase(options: [.appAccountToken(userUuid)])

Without it, we fall back to Apple's originalTransactionId. The subscription is still recorded — but /v1/entitlement/:app_id/:your_user_id will return empty until you call POST /v1/admin/user-links to manually connect the two IDs. The webhook will emit a structuredapple.missing_app_account_token warning so you can catch missing tokens during integration.

Google Play — obfuscatedAccountId

Set obfuscatedAccountId on BillingFlowParams before launching the billing flow. It propagates to the Google Play Developer API and we read it via the cross-check call.

// Kotlin / Play Billing Library
val params = BillingFlowParams.newBuilder()
  .setProductDetailsParamsList(productList)
  .setObfuscatedAccountId(currentUser.id)
  .build()
billingClient.launchBillingFlow(activity, params)

Without it, we fall back to Google's purchaseToken — same caveat as Apple. Watch the consumer logs for google.missing_obfuscated_account_id if your entitlement reads come back empty after a known purchase.

Stripe — client_reference_id

Pass client_reference_id when creating the Stripe Checkout Session. We persist it directly as the user_id on the resulting subscription.

// any backend
const session = await stripe.checkout.sessions.create({
  mode: 'subscription',
  line_items: [{ price: priceId, quantity: 1 }],
  client_reference_id: currentUser.id, // ← required
  success_url, cancel_url,
});

What if a purchase was missing the user id?

Two options once you discover it:

  1. Backfill via user-links. Call POST /v1/admin/user-links with { user_id_a: '<your user id>', user_id_b: '<store fallback id>' }. The entitlement endpoint follows the link automatically, so reads start returning the right answer immediately.
  2. Re-issue the purchase. If the user is still active and you can prompt them, restart the flow with the proper token. We deduplicate on the store-side ID so no double charge.

Why we do not auto-link

We deliberately do not guess which of your users a stray purchase belongs to — guessing wrong silently grants premium access to the wrong account, which is a much worse failure mode than an explicit empty entitlement response. Always prefer setting the right token on the client.