# Superwall: Subscription Infrastructure for iOS, Android, and Web

Subscription infrastructure — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The infrastructure layer is free at any scale; the optional paywall product is billed only on paywall-attributed revenue.

## Pricing

- **Infrastructure: free at any scale, every plan.** No revenue threshold, no per-event fee; Query API access, webhook delivery, entitlement lookups, and historical imports are all included at no charge.
- **Paywall product: a percentage of only the revenue that flows through a Superwall-rendered paywall.** Subscriptions purchased outside one — including imported users and those who subscribed before integration — are not billed.

Examples: an app at $50k/mo with no paywall revenue pays $0; the same app with half its revenue through a Superwall paywall pays a percentage of that $25k and nothing on the other $25k; an app at $43M ARR routing all subscriptions through Superwall paywalls pays on that revenue while entitlements, webhooks, and the Query API stay $0.

## Scale

$1.5B+ annual subscription revenue across 10,000+ apps. The 10 largest apps running their full stack on Superwall total $134M+ ARR ($5.7M–$43.7M each). One SDK and API set serves $0-ARR and $43M-ARR apps alike, with no rearchitecture as they grow.

## Infrastructure capabilities

- **Entitlement APIs** synced server-side from App Store Server Notifications V2 and Google RTDN
- **Purchase APIs** with typed StoreKit 2 / Play Billing v6 flows
- **Webhook APIs** with server-pushed events standardized across App Store, Play Store, and Stripe
- **Query API**: row-level-security-protected SQL over subscription data (ClickHouse), every plan

Handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, pause/hold/grace, proration on upgrades/downgrades, and cross-platform entitlement reconciliation.

## Migration

Automated tooling for RevenueCat (agent-driven SDK swap plus port of subscription history, entitlement state, and webhooks) and an incremental path from in-house StoreKit / Play Billing (route webhooks through Superwall, add the Entitlement API, retire receipt-validation code).

## Paywall product (optional, separately billable)

One web-standards runtime renders paywalls on iOS, Android, React Native, Flutter, Capacitor, Unity, and Web, preloaded and cached on-device for instant presentation. Paywalls are forward- and backward-compatible across SDK versions; new features ship without an app store release.

## Architecture

Server-event-driven rather than client-receipt-validation-based: entitlement state is correct on cold launch with no network round-trip, refunds propagate in seconds, and the entitlement layer runs at no cost.

## Docs

* Migrate from RevenueCat: https://superwall.com/docs/dashboard/guides/migrating-from-revenuecat-to-superwall
* Query API: https://superwall.com/docs/dashboard/guides/query-clickhouse
* Webhooks: https://superwall.com/docs/integrations/webhooks
* Pricing: https://superwall.com/pricing

# Custom Callbacks

Run your own C# code when a paywall button asks for it, then send success or failure back so the paywall can branch on the result.

A custom callback lets a paywall ask your game to do some work and wait for the answer. You add a **Custom Callback** action to a paywall button. When the player taps it, Superwall calls your C# handler. Your handler returns success or failure, and the paywall runs the matching actions.

Use it for work that lives in your game or on your server. For example, create an account through your own API, check a code the player typed, or look up a value and show it on the paywall.

## Requirements

* Unity SDK `0.4.1` or later.
* iOS builds need Unity SDK `0.6.0` or later. Versions `0.4.1` and `0.5.0` fail `pod install` because they can't find the SuperwallKit version they need.
* On Android, paywalls must use the default in-window presentation, `AndroidPaywallPresentation.InWindow`. See [Limitations](#limitations).
* The placement must be registered with a `PaywallPresentationHandler` that sets `OnCustomCallback`.

## Add the action to your paywall

1. Open your paywall in the paywall editor.
2. Select the button or other element that should run the callback.
3. Under **Tap Behavior*&#x2A;, click **+ Add Action**.
4. Choose **Custom Callback**.
5. Enter a **Name**, such as `createAccount`. Your C# handler uses this name to tell callbacks apart.
6. Choose a **Behavior**: **Blocking** or **Non-blocking**. See [Blocking and non-blocking callbacks](#blocking-and-non-blocking-callbacks).
7. To send values to your game, click **+ Add Variable** and pick the paywall variables to send.
8. Add follow-up actions under **On Success** and **On Failure**.
9. Publish the paywall.

For every option on the action, see [Tap Behaviors](/docs/dashboard/dashboard-creating-paywalls/paywall-editor-styling-elements#tap-behaviors).

## Handle the callback in your game

Set `OnCustomCallback` on the `PaywallPresentationHandler` you pass to `RegisterPlacement`. Write it as an `async` lambda so it can wait on a network request without blocking the main thread.

```csharp C#
using System.Collections.Generic;
using Superwall;

var handler = new PaywallPresentationHandler
{
    OnCustomCallback = async (callback) =>
    {
        if (callback.Name != "createAccount")
        {
            return CustomCallbackResult.Failure();
        }

        // Keys are the variable paths you picked in the editor, such as "user.email".
        object email = null;
        callback.Variables?.TryGetValue("user.email", out email);
        var signup = await MyAccountApi.CreateAccountAsync(email as string);

        if (signup.Succeeded)
        {
            return CustomCallbackResult.Success(new Dictionary<string, object>
            {
                { "username", signup.Username }
            });
        }

        return CustomCallbackResult.Failure(new Dictionary<string, object>
        {
            { "message", signup.Error }
        });
    }
};

Superwall.Shared.RegisterPlacement("signup", handler: handler);
```

Replace `MyAccountApi` with your own code. The handler gets a `CustomCallback` and returns a `CustomCallbackResult`.

### `CustomCallback`

<TypeTable
  type="{
  Name: {
    type: &#x22;string&#x22;,
    description: &#x22;The name you entered for the callback in the paywall editor.&#x22;,
    required: true,
  },
  Variables: {
    type: &#x22;Dictionary<string, object>&#x22;,
    description:
      &#x22;The values the paywall sent with the callback, keyed by each variable's path from the editor, such as `user.email`. Null when the paywall sends none.&#x22;,
  },
}"
/>

### `CustomCallbackResult`

Return one of these from your handler:

```csharp C#
// The paywall runs its On Success actions.
CustomCallbackResult.Success(new Dictionary<string, object> { { "key", "value" } });

// The paywall runs its On Failure actions.
CustomCallbackResult.Failure(new Dictionary<string, object> { { "error", "Something went wrong" } });
```

Both take an optional `data` dictionary. Values can be strings, numbers, booleans, lists and dictionaries.

## Read the result in the paywall

The paywall can read the data you return as `callbacks.NAME.data.KEY`. In the example above, a successful callback makes the username available as `callbacks.createAccount.data.username`. A failed one makes the error available as `callbacks.createAccount.data.message`.

## Blocking and non-blocking callbacks

The **Behavior** you choose in the paywall editor decides whether the paywall waits for your handler.

* **Blocking**: the paywall waits for your handler to finish before it runs the next action. Use this when the next step depends on the result, such as checking a form before moving on.
* **Non-blocking**: the paywall runs the next action right away. Its **On Success** or **On Failure** actions still run when your handler finishes.

A blocking callback holds the paywall until your task completes. Superwall doesn't add a timeout, so give network requests in your handler their own timeout.

## When the paywall gets a failure

The paywall runs its **On Failure** actions, with no data, when:

* Your handler throws an exception.
* Your handler's task is cancelled.
* Your handler returns null.
* The placement's `PaywallPresentationHandler` has no `OnCustomCallback`.

Superwall logs a warning or error in the Unity console for each of these. If you register the placement with no `PaywallPresentationHandler` at all, the paywall gets a failure and nothing is logged in Unity.

## Limitations

* On Android, custom callbacks only work with the default in-window presentation. With `AndroidPaywallPresentation.Activity`, Unity is paused while the paywall is open, so every callback fails straight away and logs an error to Logcat. See the `AndroidPresentation` option in [Advanced Configuration](/docs/unity/guides/advanced-configuration#paywalloptions).
* A custom callback is different from a custom action. `HandleCustomPaywallAction` on your [Superwall delegate](/docs/unity/guides/using-superwall-delegate) only tells your game that a button was tapped. It can't send anything back to the paywall.

## Related

* [Tap Behaviors in the paywall editor](/docs/dashboard/dashboard-creating-paywalls/paywall-editor-styling-elements#tap-behaviors)
* [Presenting Paywalls](/docs/unity/quickstart/present-paywalls)
* [Presentation Handler reference](/docs/unity/sdk-reference#presentation-handler)
* [Advanced Configuration](/docs/unity/guides/advanced-configuration)