Pushlane: Recover RevenueCat BILLING_ISSUE with Push
Build a careful iOS payment recovery flow from RevenueCat BILLING_ISSUE webhooks in Pushlane, with grace period logic, stop events and useful copy.
A failed renewal is often a payment problem, not a decision to leave. The subscriber may have replaced a card, hit a temporary bank decline, or missed a store notice. A useful push gives that person a direct path to fix the issue while the store is still retrying.
RevenueCat's BILLING_ISSUE webhook is a strong trigger for that message, but it is not proof that access has expired. The difference determines the tone of your notification, the branch logic in your flow, and when the flow must stop.
This tutorial shows how to turn the server-side event into a restrained iOS recovery flow in Pushlane. It does not assume a universal grace period or promise that one message will recover a particular amount of revenue. Store configuration and current subscription state remain authoritative.
Understand the events before building the flow
RevenueCat defines BILLING_ISSUE as a failed attempt to charge the subscriber. Its official event types and fields reference explicitly says that the event does not mean the subscription has expired.
For an active subscription with a billing problem, RevenueCat can dispatch both:
BILLING_ISSUE;CANCELLATIONwithcancel_reasonequal toBILLING_ERROR.
If no grace period applies, an EXPIRATION event can follow immediately. If a grace period applies, the subscriber keeps access while the store retries payment. Successful recovery produces RENEWAL; unsuccessful recovery eventually produces EXPIRATION.
RevenueCat's common webhook flows also warns that network irregularities can change the order in which events arrive. Build around event meaning and timestamps, not the assumption that every request reaches your system in a perfect sequence.
Pushlane maps these webhooks to server-side flow events:
| RevenueCat webhook | Pushlane flow event | Meaning for this flow |
|---|---|---|
BILLING_ISSUE | revenuecat.billing_issue | Start or update payment recovery |
RENEWAL | revenuecat.renewal | Stop recovery because payment succeeded |
EXPIRATION | revenuecat.expiration | Stop grace messaging and move to an expired-state path |
CANCELLATION | revenuecat.cancellation | Inspect the reason before treating it as voluntary churn |
These events arrive server to server. Do not fabricate billing_issue from client code, and do not wait for the app to open before recording it.
Prerequisite: make the webhook user reachable
The webhook's app_user_id must resolve to the same user identified in Pushlane. On iOS, use one stable account ID for RevenueCat and Pushlane:
let userID = Pushlane.identifyForRevenueCat(currentUser.id)
Purchases.configure(withAPIKey: "appl_...", appUserID: userID)
That shared ID connects the subscription signal to the user's registered iOS installation. The APNs token remains a device address, not a RevenueCat identity. The complete model is explained in matching RevenueCat App User IDs to push device tokens.
Pushlane's iOS delivery through APNs is proven end to end. Android FCM tokens may be registered and counted as reachable devices, but Android delivery is not supported yet. Scope this recovery flow to iOS until that delivery path exists.
You also need the RevenueCat webhook connected with Pushlane's dedicated rcw_... secret. The SDK write key must not authenticate revenue events. Follow the RevenueCat + Pushlane setup guide before creating the flow.
Design the smallest useful recovery flow
Start with one immediate message and one conditional reminder. More messages are not automatically better, especially when RevenueCat Billing can also send its own billing notice or the store presents payment information.
revenuecat.billing_issue
-> branch: production event and supported iOS device
-> send: neutral payment issue notice
-> wait until a justified reminder point
-> stop if revenuecat.renewal or revenuecat.expiration arrived
-> send: final reminder only if the issue remains open
The first branch should reject sandbox events from customer-facing delivery. Pushlane exposes the RevenueCat environment and an is_production property on the synthetic flow event. Sandbox events are valuable for testing, but they should not share the production send path.
The next branch should check reachability and consent. A resolved user can have no active iOS device, can have denied notification permission, or can have uninstalled the app. That is a suppress decision, not a successful delivery.
Use grace period data carefully
RevenueCat's current webhook schema includes grace_period_expiration_at_ms on BILLING_ISSUE, and the value can be null. Store and product configuration determine whether a grace period exists. Your flow should therefore support at least three states:
Known future grace deadline. Schedule a reminder before that timestamp, leaving enough time for the person to act. Avoid claiming access ends at an exact local hour unless your timestamp handling and locale formatting make that statement accurate.
No grace period. EXPIRATION may arrive alongside the billing events. Do not send copy that says the subscriber still has access. Let the expiration event route the user to an expired subscription recovery path.
Unknown grace deadline. Send only the immediate factual message, or use a conservative delay followed by a current-state check. Do not invent a deadline from the product's normal billing period.
Pushlane currently exposes expiration_at_ms in flow event properties. Treat it as event context, not as permission to assume every billing issue has the same recovery window. If your flow needs a property-relative schedule, validate the actual field populated for your store events in sandbox first.
Write like support, not collections
The subscriber may still have access and may not know the charge failed. Your copy should describe the problem without blame and make the next action clear.
Useful first message:
Payment needs your attention
We could not renew your subscription. Review your payment method to keep access uninterrupted.
Useful reminder when a real deadline is known:
Keep your subscription active
Your payment issue is still open. Update your billing details before access ends.
Avoid messages such as "Your account has been canceled" on BILLING_ISSUE. That can be false during a grace period. Avoid "Your card was declined" because the webhook does not necessarily establish which payment method or why it failed. Avoid quoting a balance unless you have a reliable, localized amount and the legal basis to present it.
Open the closest valid payment-management experience for the store involved. Apple's notification action documentation describes how the app receives an interaction. Test the destination on a real device for the store that owns the subscription.
Stop conditions prevent the worst message
A recovery flow is not complete until it knows when to stop. The most damaging failure is a "fix your payment" reminder delivered after the renewal succeeded.
Exit the flow on revenuecat.renewal. RevenueCat uses that event when a retry recovers the payment. If the renewal arrives during your wait, cancel every remaining billing reminder.
Exit or transfer the user on revenuecat.expiration. Access has ended, so any later message must use expired-state language. It can still offer a path to resubscribe or correct billing, but it must not claim the user remains in grace.
Handle duplicate webhook delivery idempotently. Pushlane deduplicates RevenueCat events by their event ID and derives a deterministic synthetic flow event ID. Your flow design should still avoid starting overlapping recovery journeys for the same unresolved billing episode.
Finally, do not use every CANCELLATION event as a stop or as a voluntary churn trigger. A billing issue can produce CANCELLATION with BILLING_ERROR at the same time. Verify that distinction while mapping the webhook events in the RevenueCat and Pushlane integration guide.
Add decision branches that reflect the person
One generic push is a sound first version. If you later add branches, use facts that change the helpful action:
Store. Payment management differs across App Store, Google Play, Stripe, and RevenueCat Billing. Pushlane delivery is currently iOS-only, but RevenueCat can send billing events from multiple stores. Do not deep link an App Store customer to a web billing portal or imply that an Android subscriber can receive a Pushlane push today.
Entitlement. If the webhook includes entitlement_ids, choose copy for the affected access rather than the internal product SKU. A person understands "Pro access" more readily than com.example.pro.monthly.
Locale. Pushlane selects localized notification content at send time from the user's locale attribute. Translate the meaning, not just the words, and keep the payment destination valid for that market.
Current recovery state. Before a delayed reminder, verify that a renewal or expiration has not superseded the original event. Event history is a trigger source; current entitlement state is what protects you from stale messaging.
Do not branch on guessed card type, presumed ability to pay, or a churn-risk label that the webhook does not provide.
Test a state machine, not a single webhook
RevenueCat provides test and sandbox tools, but a dashboard TEST webhook only proves authenticated reachability. It is not a complete billing lifecycle.
Exercise these cases with unique sandbox users:
BILLING_ISSUEfollowed byRENEWALduring the delay. The first message may send; the reminder must not.BILLING_ISSUEfollowed byEXPIRATION. Grace-period messaging must stop.BILLING_ISSUE,CANCELLATIONwithBILLING_ERROR, andEXPIRATIONarriving in an unexpected order. The user must not enter both dunning and voluntary-cancel campaigns.- Duplicate delivery of the same RevenueCat event ID. The flow must not send twice.
- Sandbox payload reaching the production flow. It must be suppressed by environment.
- Correct identity with no active APNs device. The outcome must be recorded as suppressed, not sent.
- A notification tap. It must land on a working payment-management route for the store.
Inspect event_timestamp_ms and lifecycle timestamps when reviewing sequence behavior. Arrival order alone is not a safe clock.
Measure recovery without inventing causality
The primary outcome is a subsequent successful renewal for the same billing episode. Notification opens are diagnostic, not the business result. A subscriber may read the lock-screen copy, update payment elsewhere, and never tap the notification.
Track at least:
- billing issues that entered the flow;
- sends, suppressions, and APNs delivery outcomes;
- notification interactions;
- renewals after a billing issue;
- expirations after a billing issue;
- time from issue to renewal or expiration;
- notification opt-outs during the recovery flow.
Do not claim that every renewal after a push was caused by the push. App-store retries can recover payment without any message. To estimate incremental impact, use a properly assigned holdout when your volume supports one and define the recovery window before reading results.
The rule that keeps dunning honest
BILLING_ISSUE means "a charge attempt failed." It does not mean "the subscriber left," and it does not always mean "access ended."
Use it to begin a helpful, event-driven iOS flow. Branch on real store and grace information. Stop on renewal. Change state on expiration. Keep identity server-linked through the shared RevenueCat and Pushlane user ID. That produces a recovery experience that is useful even when payment is never recovered, because every message remains true when the subscriber reads it.