Actions
Close the paywall, restore purchases, open links, request OS permissions, and call back into your app, everything a paywall asks its host to do.
A paywall runs inside your app, and some things only the host can do: dismiss the paywall, open a link, prompt for a permission, run your app's code. All of it goes through useActions():
import { useActions } from "superwall/hooks";
const { close, restore, openUrl, requestPermission, requestCallback } = useActions();The actions
| Action | Use it for |
|---|---|
close() | Closing the paywall. The X button. Closing is not navigation. |
restore() | Restore purchases. Resolves { status: "restored" } or { status: "failed" } — the SDK reports "nothing to restore" as a failure like any other. See Purchases. |
openUrl(url) | Terms, privacy, any link. Always this, never <a href>. |
openExternalUrl(url) | Open in the system browser instead of in-app. |
openDeepLink(link) | Deep link into the app. |
customPlacement(name, params?) | Fire a Superwall placement, which can present another paywall. |
requestPermission(type) | OS permission prompt. Resolves "granted" | "denied" | "unsupported". |
requestCallback(name, options?) | Run your app's code and await its answer. Resolves { status: "success" | "failure", data? }. |
requestStoreReview("in-app" | "external") | Store review prompt. |
setUserAttributes({ key: value }) | Set attributes on the user, an email a funnel page captured, a plan they picked. The host SDK merges and persists them (the same as Superwall.shared.setUserAttributes), and the paywall's own useUser() sees them at once. Values are sent as strings. A user.email also becomes the email on every web checkout session from then on, see Web checkout. |
Links go through openUrl, never an <a href>. Inside a webview, an anchor either does nothing or navigates the paywall away from itself. openUrl hands the URL to the host so it opens the way the platform expects.
Closing works the same way: the paywall lives on a navigation stack of its own pages, but leaving the paywall isn't a navigation. It's close(). See Pages & navigation.
Permissions
const status = await requestPermission("notification");
// "granted" | "denied" | "unsupported"Permission types: notification, camera, microphone, location, background_location, contacts, read_images, read_video (Android only), tracking.
Callbacks: ask your app a question
A callback runs code in your app and hands the answer back to the paywall, anything the paywall cannot know on its own: does this account exist, is this referral code valid, what did the user pick during signup.
const result = await requestCallback<{ exists: boolean }>("checkAccount");
if (result.status === "success" && result.data?.exists) {
router.push("welcome-back");
}Type the answer with a claim, as above. The generic is your statement of what the app returns.
Permission vs callback
A permission asks the OS; a callback asks your app. Both resolve from code the paywall does not control, which shapes how you use them:
- Show something while they run. The OS prompt or your app's code takes as long as it takes.
- Treat a denial as an ordinary outcome, not an error. A user who declines notifications is still a user, design the path that continues without.
In development
In superwall dev, the studio is the host. Every action reaches it over the same protocol the SDK uses: close() and the URL actions surface as toasts, and permission, callback, purchase and restore requests prompt you to pick the outcome, which the studio then reports back with the messages the SDK would send. That makes both branches of every flow testable before a device ever sees it. See The studio, and Host Protocol for the messages underneath.
The permissions example shows requestPermission and requestCallback side by side, with a denial treated as an outcome rather than an error. See Examples.
How is this guide?