Four calls. The network layer is ours to keep working.
PassbackKit opens the funnel, asks for the subscription, handles the return and takes the code. Your app never builds a request, parses a route or decides what a timeout means. Inside the dashboard the same page has your names filled in, with the plain-HTTP equivalents for Android.
PassbackKit for Swift
A Swift package with one object on it. Add it from the repository you are given at sign-up, make one instance with your origin and API key, and keep it for the life of the app. Four calls on it, and the exact lines are on the settings screen once you have signed in:
- Open. One call opens a funnel by name, only where the storefront allows a link out; the package asks first and answers from its last answer when offline.
- Check. One call on launch, and when the app comes to the foreground, returns the subscription for this install or this address.
- Handle. Hand every URL the app is opened with to the package. It exchanges the one that is ours and ignores the rest.
- Claim. Pass the six digits a customer types after installing, and get the same answer.
Every call returns the same Entitlement value. active is the answer. status, currentPeriodEnd, cancelAtPeriodEnd and plan are context. A person with no subscription is found: false, not an error. A request that could not be made at all comes back with unavailableReason set, so your app can tell “no” from “unknown” and never show a paywall to a paying customer on a bad connection.
The package owns the network layer: which requests are made, in what order, what is retried, how a token is exchanged exactly once, and what an expired or spent one means. None of that is your code to write or keep working when it changes.
The return trip
The default path needs nothing declared in Xcode. When the package first runs it makes an anonymous identifier for the install and keeps it in the Keychain, so it survives a reinstall and never leaves the device. It is not an Apple identifier and it is not tied to the person.
open(funnel:) carries that identifier to the funnel. When the customer pays, the purchase is recorded against it on the way back from Stripe, before any webhook has arrived. The confirmation screen tells the customer to switch back to your app. On the next launch, check() asks about the install and gets the subscription. The customer has not signed in, created an account or typed anything.
If your app already has accounts, check(email:) asks by the address they paid with instead, and returns the same value.
How a purchase is verified
Three ways to establish that the person holding the phone is the person who paid. The package chooses; your app only ever sees the one answer.
- By install. The identifier the funnel was opened with, kept in the Keychain, matched to the purchase recorded on the way back. Used on every launch. Needs nothing from the customer.
- By address. The email the customer paid with, for apps that have their own sign-in. Used on every launch after they sign in.
- By credential. A single-use token on the deep link, a restore link in the receipt, or the six-digit code. Each is exchanged once, expires (five minutes, seven days, one day), and is stored on our side only as a SHA-256 digest, so a copy of the database is not a set of working keys. A credential that has already been used or has expired is refused with a distinct reason, and
check()still answers by install.
Your API key identifies your app on every request and is scoped to it. Keys are made in Settings, shown once, stored as digests, and revoked there. Nothing is cached between the server and the device: a cancellation is visible on the next call.
Make the return instant (optional)
With a URL scheme, the confirmation screen opens your app itself rather than asking the customer to switch back. Three steps, none of them network code:
- Pick a word, lowercase, and save it in the App panel of your dashboard.
- In Xcode: Target → Info → URL Types → add one, identifier your bundle id, URL Schemes the same word.
- Hand every URL the app is opened with to
passback.handle(url). It exchanges the one that is ours and returnsnilfor anything else, so it can sit first in your handler without a check.
Add the Associated Domains capability with your Passback origin and the restore link in every receipt opens the app the same way, as a universal link. Until the scheme is in the build on the phone, tapping the link does nothing — no error, no prompt — which is why the install path is the default and this is the upgrade.
Paid before installing
A customer who pays on the web with no app on the phone sees six digits on the confirmation and gets them in the receipt. Give your paywall an “enter your code” field and pass what they type to claim(code:). Single use, good for a day, only valid inside your app, locked after ten wrong guesses. The answer is the same value as every other call.
Already on RevenueCat or Adapty
Give the package your id for the person — RevenueCat's app user id or Adapty's customer user id — and skip check() altogether.
One extra argument when the package is created, and no other change in the app.
With that id and a key from your RevenueCat or Adapty dashboard entered in Settings, every web purchase is written to the customer's entitlement or access level server to server when the payment settles, and kept in step with renewals, failed payments and cancellations. Your existing entitlement check starts saying yes. Nothing else in the app changes.
Android
There is no Android package yet. The same four steps are a few lines of Kotlin, and the dashboard's guide shows them with your funnel address and key filled in once you have signed in. In outline:
- Make an identifier once per install and keep it in app-private storage.
- Open your funnel address in a Custom Tab, with that identifier attached the way the guide shows.
- On resume, ask for the subscription state with your key and read
active. The answer has the same shape as on iOS. - Optionally register an intent filter for the instant return, and hand the incoming link to the same call.
Webhooks
You do not handle any. Passback listens to your connected Stripe account and keeps the subscription current; your app asks the package, the package asks us, and we read the row. When a payment fails you get an email and an alert in the dashboard, and the customer keeps access to the end of the period they paid for.
Something missing? Ask. The dashboard's version of this page has a test that runs the whole loop without Stripe.