Skip to main content
Skip to navigation
Unlisted page
This page is unlisted. Search engines will not index it, and only users having a direct link can access it.

Implementing Checkpoints

Register Checkpoints with the RevenueCat SDK and handle what they return.

Ask RicoChatGPTClaude

Call checkpoint() at each moment where RevenueCat might show something. Your Targeting rules decide what happens there. Call it directly where the moment happens, not from a wrapper.

Before you start​

Ensure that:

  • Your project is enrolled in the Targeting by Checkpoint beta.
  • You use RevenueCatUI with the RevenueCat iOS SDK 5.96.0+ or Android SDK 10.28.1+.
  • RevenueCat completes your purchases. Observer Mode (purchasesAreCompletedBy set to your app) isn't supported yet.
  • You opt in to the beta API: @_spi(InviteOnlyCheckpointsApi) on iOS, @OptIn(InviteOnlyCheckpointsAPI::class) on Android.

Register a Checkpoint​

A Checkpoint is a named moment in your app that your customer has reached, such as: the end of onboarding, a button to access a gated feature, an informational screen, a usage limit, etc.

Checkpoint identifiers aren't case-sensitive. They must start with an ASCII letter, use only ASCII letters, numbers, _, and -, and be no more than 255 characters long.

@_spi(InviteOnlyCheckpointsApi) import RevenueCat
@_spi(InviteOnlyCheckpointsApi) import RevenueCatUI

Purchases.shared.checkpoint("onboarding_completed") { result in
// Runs when the customer moves past the Checkpoint.
// `result.flow` is nil if the Checkpoint isn't configured or no rule matched.
showHomeScreen()
}

The first time a Checkpoint is reached in your app, it will be added to your list of available Checkpoints in the dashboard.

Register Checkpoints ahead of time

Add Checkpoints anywhere you might want to serve a Flow or Paywall later. A Checkpoint with no rules shows nothing: Customers pass straight through, and the Checkpoint continuation runs right away. You can turn it on from the dashboard at any time, without an app update.

Handle the result​

The Checkpoint continuation (the closure or lambda you pass to checkpoint()) runs once at most, when the customer moves past the Checkpoint. When it runs, it receives a CheckpointResult. Its optional flow describes the completed Flow, if there was one.

OutcomeMeaning
result.flow is nil / nullNothing was shown because the Checkpoint isn't configured or no rule matched.
result.flow contains a FlowResult with EntitlementsThe customer moved past the Checkpoint after gaining one or more Entitlements.
result.flow contains a FlowResult with no EntitlementsThe customer moved past the Checkpoint, but no new Entitlements were reported.
The Checkpoint continuation doesn't runThe customer didn't move past the Checkpoint because they navigated back, or an error prevented completion.

Put the code that moves the customer forward inside the Checkpoint continuation. If the customer navigates back, they stay where they were.

Errors the SDK encounters while serving a Checkpoint use its default error presenter, or you can provide your own. Checkpoints fail closed: If an error prevents the Flow from completing, the Checkpoint continuation doesn't run and the customer stays where they were.

Offerings shown through your own paywall presenter return the same results. When your presenter completes with continued or closed, the SDK refreshes CustomerInfo to find the Entitlements obtained. The Checkpoint continuation doesn't run after navigatedBack. Your custom paywall remains responsible for errors inside its own UI.

Pass custom variables​

📘Coming soon

You can pass custom variables to checkpoint() today, but you can't yet create targeting rules that use them in the dashboard. Support is coming soon.

Custom variables give the Checkpoint context. They're the same custom variables your Paywalls use, passed at the Checkpoint instead of when presenting the Paywall. The Paywall can display them as text. Once rules can target them (coming soon), use them to move app-side conditions, like pages remaining, into the rule instead of deciding in code whether to call checkpoint().

@_spi(InviteOnlyCheckpointsApi) import RevenueCat
@_spi(InviteOnlyCheckpointsApi) import RevenueCatUI

Purchases.shared.checkpoint(
"onboarding_completed",
customVariables: ["goal": .string("lose_weight")]
) { result in
let unlockedPro = result.flow?.obtainedEntitlements
.contains { $0.entitlementInfo.identifier == "pro" } ?? false
showHomeScreen(pro: unlockedPro)
}

Skip customers who already have access​

Checkpoints don't skip entitled customers automatically, since they can also be used to serve upgrade flows, non-purchasing flows, etc.

If you want customers with an active entitlement to not be served a paywall, add an entitlement condition to the rule's audience to ensure they won't match the rule. If a customer does not match a rule at a Checkpoint, they'll proceed past the Checkpoint uninterrupted.

Require an Entitlement to continue​

Every Checkpoint is a soft gate today: Customers can close the Flow and the Checkpoint continuation still runs. Hard Paywalls are coming soon. Until then, check the Entitlement yourself in the continuation, and only continue if the customer has it.

@_spi(InviteOnlyCheckpointsApi) import RevenueCat
@_spi(InviteOnlyCheckpointsApi) import RevenueCatUI

Purchases.shared.checkpoint("access_locked_content") { _ in
Purchases.shared.getCustomerInfo { customerInfo, _ in
if customerInfo?.entitlements.active["pro"] != nil {
continueToLockedContent()
}
// Otherwise, the customer stays where they are.
}
}

Pair this with a rule whose audience matches customers without the Entitlement, so they always see a Paywall here. getCustomerInfo returns cached data when it's available, so this check doesn't hold up the customer.

Use your own paywall UI​

To show your own paywall UI, pass a paywall presenter to the checkpoint() call. You can also register a global presenter as an app-wide default; it receives the Offering and Checkpoint identifier, so it can show a different Paywall per Checkpoint.

@_spi(InviteOnlyCheckpointsApi) import RevenueCat
@_spi(InviteOnlyCheckpointsApi) import RevenueCatUI

@MainActor
final class MyPaywallPresenter: PaywallPresenter {
func present(
params: PaywallPresentationParams,
completion: @escaping PaywallPresentationCompletion
) {
// Show your own paywall for `params.offering`.
// Call completion exactly once when it closes.
showCustomPaywall(offering: params.offering) {
completion(.closed)
}
}
}

@MainActor
func configureRevenueCat() {
Purchases.configure() // Your existing configuration code.

// Used for every Checkpoint that serves an Offering.
Purchases.shared.paywallPresenter = MyPaywallPresenter()
}

func onOnboardingCompleteClick() {
// You can also override the presenter for a single Checkpoint call.
Purchases.shared.checkpoint(
"onboarding_completed",
paywallPresenter: { params, completion in
showCustomPaywall(offering: params.offering) {
completion(.closed)
}
}
) { _ in
showHomeScreen()
}
}

Complete with continued, closed, or navigatedBack. Call the completion exactly once; if you never call it, the Checkpoint never finishes.

Use your own error UI​

To show your own UI for errors the SDK encounters while serving a Checkpoint, pass an error presenter to checkpoint(). You can also register a global presenter as an app-wide default. It receives the error, Checkpoint identifier, custom variables, and failure kind. If you don't set an error presenter, the SDK shows its own error dialog. Your presenter owns the error UI; the SDK decides what happens after it finishes. Your custom paywall UI remains responsible for its own errors.

@_spi(InviteOnlyCheckpointsApi) import RevenueCat
@_spi(InviteOnlyCheckpointsApi) import RevenueCatUI

@MainActor
final class MyErrorPresenter: ErrorPresenter {
func present(params: ErrorPresentationParams, completion: ErrorPresentationCompletion) {
// Show your own error UI. The SDK decides what happens afterwards.
showCustomError(params) {
completion.complete()
}
}
}

@MainActor
func configureRevenueCat() {
Purchases.configure() // Your existing configuration code.

// Used for every Checkpoint error the SDK presents.
Purchases.shared.errorPresenter = MyErrorPresenter()
}

func onOnboardingCompleteClick() {
// You can also override the presenter for a single Checkpoint call.
Purchases.shared.checkpoint(
"onboarding_completed",
errorPresenter: { params, completion in
showCustomError(params) {
completion.complete()
}
}
) { _ in
showHomeScreen()
}
}

Call complete() exactly once after your error UI finishes. If you never call it, error handling never finishes.

Migrate from Placements and Offerings​

Migrating means replacing the code that picks and presents a Paywall with checkpoint() calls, then recreating your targeting as rules.

TodayWith Checkpoints
getOfferings / getCurrentOfferingForPlacementDeleted. The rule picks the Flow or Offering.
presentPaywallIfNeeded with requiredEntitlementIdentifiercheckpoint(), with the Entitlement condition in the rule's audience
Branching on the paywall resultCode inside the Checkpoint continuation
NOT_PRESENTED treated as an errorNormal: no rule matched, and the Checkpoint continuation runs immediately
Falling back to the current OfferingA catch-all rule
Embedded PaywallViewNot supported yet. Checkpoints present Flows modally.

In your app​

  1. Replace each presentPaywall, presentPaywallIfNeeded, or Placement call with checkpoint(). Your Placement identifiers usually work as Checkpoint identifiers. Delete wrappers whose only job was presenting the Paywall.
  2. Remove any getOfferings calls that served a paywall.
  3. Move the code that ran after the Paywall into the Checkpoint continuation.
  4. Remove Entitlement pre-checks. Put the Entitlement condition in the rule's audience instead.
  5. For a custom paywall, pass a paywall presenter.
  6. Optionally, use an error presenter to handle errors from RevenueCat-presented Flows.

In the dashboard​

  1. Create a Checkpoint for each moment.
  2. Recreate your targeting conditions as audiences.
  3. Create a rule per Checkpoint that serves the Paywall or Offering you used before.
  4. If you relied on falling back to the current Offering, add a catch-all rule last. Without it, customers who match nothing see nothing.

Legacy Targeting rules aren't copied over automatically yet.

Release​

Older app versions keep using Offerings and legacy Targeting. Keep those rules live until most customers have updated.

Reference​

SDK API​

iOS (RevenueCatUI, iOS 15+)

@_spi(InviteOnlyCheckpointsApi) import RevenueCat
@_spi(InviteOnlyCheckpointsApi) import RevenueCatUI

extension Purchases {
func checkpoint(
_ identifier: String,
customVariables: [String: CustomVariableValue] = [:],
presentationMode: FlowPresentationMode = .default,
paywallPresenter: PaywallPresentationHandler? = nil,
errorPresenter: ErrorPresentationHandler? = nil,
_ onPassed: @escaping (CheckpointResult) -> Void
)

@MainActor var paywallPresenter: PaywallPresenter? { get set }
@MainActor var errorPresenter: ErrorPresenter? { get set }
}

Android (revenuecatui)

@file:OptIn(InviteOnlyCheckpointsAPI::class)

import com.revenuecat.purchases.ui.revenuecatui.InviteOnlyCheckpointsAPI

fun Purchases.checkpoint(checkpointIdentifier: String, callback: CheckpointPassedCallback)
fun Purchases.checkpoint(checkpointIdentifier: String, params: CheckpointParams?, callback: CheckpointPassedCallback)

var Purchases.paywallPresenter: PaywallPresenter?
var Purchases.errorPresenter: ErrorPresenter?

// Per-call options
CheckpointParams {
customVariables { "key" to "value" }
presentationMode(FlowPresentationMode.DEFAULT)
paywallPresenter { params, completion -> /* ... */ }
errorPresenter { params, completion -> /* ... */ }
}

Minimum versions: iOS SDK 5.96.0, Android SDK 10.28.1.

Result​

When the continuation runs, it receives a CheckpointResult. Its flow is nil / null when nothing was presented. Otherwise, flow contains a FlowResult whose obtainedEntitlements are the Entitlements the customer obtained during the Flow. Each item wraps an EntitlementInfo.

SituationOutcome
Checkpoint isn't configured, or no rule matchedCheckpointResult with flow set to nil / null
Moved past after gaining a new EntitlementCheckpointResult with a FlowResult containing Entitlements
Moved past without gaining a new EntitlementCheckpointResult with a FlowResult containing no Entitlements
Error prevented the Flow from completingDoesn't run
Navigated backDoesn't run

Custom variables​

  • Types: String, number, boolean.
  • Keys: Letters, digits, and _. Invalid keys are dropped.
  • Coming soon: rules will see them as custom.<key>. Paywalls can show them as text.

Presentation mode​

FlowPresentationMode controls how the SDK presents a Flow over your app.

ModeiOSAndroidBehavior
Default.defaultDEFAULTLets the SDK choose the presentation mode. It currently uses a modal sheet.
Modal full screen.modalFullScreenMODAL_FULL_SCREENCovers the whole screen.
Modal sheet.modalSheetMODAL_SHEETPresents a modal sheet over your app's content.

Custom paywall presenters receive the resolved mode and are responsible for applying it to their own UI.

Paywall presenter completion values​

When your custom paywall presentation ends, call its completion with one of these values:

iOSAndroidMeaning
.continuedContinuedCustomer continued past your paywall. The SDK refreshes CustomerInfo, then the Checkpoint continuation runs.
.closedClosedCustomer closed your paywall. The SDK refreshes CustomerInfo, then the Checkpoint continuation runs.
.navigatedBackNavigatedBackCustomer navigated back. The Checkpoint continuation doesn't run.

Error presenter failure kinds​

Call complete() after your error UI finishes. This only reports that your UI is done; the SDK still owns the Flow outcome:

iOSAndroidFailureAfter complete()
.flowCouldNotBeShownFLOW_COULD_NOT_BE_SHOWNThe Flow couldn't be presented or its initial screen couldn't load.No Flow continues. The customer remains before the Checkpoint, and the Checkpoint continuation doesn't run.
.flowActionFailedFLOW_ACTION_FAILEDAn action failed, such as a purchase, restore, or loading the next screen.The SDK returns to the current Flow if it remains usable. If presentation already ended, the customer remains before the Checkpoint. The Checkpoint continuation doesn't run.
.flowResultSyncFailedFLOW_RESULT_SYNC_FAILEDA PaywallPresenter completed, but the SDK couldn't synchronize the resulting CustomerInfo.The Checkpoint continuation runs with a CheckpointResult containing a FlowResult with no reported Entitlements.

Troubleshooting​

The Flow completed but no Entitlements were reported​

If a PaywallPresenter completes but the resulting CustomerInfo can't be synchronized, the SDK presents the error and then runs the Checkpoint continuation with a CheckpointResult containing a FlowResult with no reported Entitlements.

The Checkpoint continuation never runs​

  • The customer navigated back. This is expected.
  • An error prevented the Flow from completing. This is expected.
  • Your paywall presenter never called its completion.
  • Your error presenter never called its completion.

Next steps​

Was this page helpful?