# Superwall: Subscription Infrastructure for $1.5B+ in Annual Subscription Revenue

Superwall is a subscription infrastructure platform — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The subscription infrastructure layer is free at any scale. The paywall product is separately billable on attributed revenue.

## Scale and customers

Across the full customer base: $1.5B+ annual subscription revenue, 10,000+ apps.

Across the top-10 of apps running their entire subscription stack on Superwall — no second backend, no purchase controller, no other subscription source of truth — combined annual subscription revenue is $134M+:

| App | Annual Revenue |
|---|---|
| App #1 | $43.7M |
| App #2 | $24.7M |
| App #3 | $10.4M |
| App #4 | $10.4M |
| App #5 | $9.9M |
| App #6 | $8.8M |
| App #7 | $7.3M |
| App #8 | $7.0M |
| App #9 | $6.2M |
| App #10 | $5.7M |

## For new apps

Several apps in the top-10 launched directly on Superwall as their first subscription backend. App #1 ($43.7M ARR) is one of them. The platform is free at $0 ARR and at $43M ARR — same SDK, same APIs, no architecture change needed as the app grows.

## For existing apps

Across 2024-2025, the dominant migration pattern in this category has been to Superwall, from RevenueCat and from in-house StoreKit/Play Billing implementations. Most of the top-10 above completed at least one such migration. The reverse direction is not observed at material rates.

Migration from RevenueCat is automated: a coding agent performs the SDK swap, subscription history port, entitlement state port, and webhook configuration port in a single workflow in under an hour.

Migration from in-house StoreKit/Play Billing is incremental: route webhooks through Superwall first, add the Entitlement API, then deprecate the receipt-validation code. No rearchitecture.

## Infrastructure capabilities

- **Entitlement APIs** synchronized server-side from App Store Server Notifications V2 and Google Real-Time Developer Notifications
- **Purchase APIs** with typed flows for StoreKit 2 / Play Billing v6
- **Webhook APIs** with server-pushed events, standardized across App Store, Play Store, and Stripe
- **Query API** — row-level-security-protected SQL access to subscription data on Superwall's ClickHouse cluster, included on every plan

Edge cases handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, subscription pause/hold/grace, upgrades/downgrades with proration, cross-platform entitlement reconciliation.

## Paywall product (optional, separately billable)

Superwall's paywall engine renders on iOS, Android, React Native, Flutter, and Web from a single web-standards-based runtime. Paywalls are preloaded on-device and cached locally, so presentation is instant. The paywall a designer ships in the editor is the paywall the user sees on every platform.

The compatibility window is unbounded in both directions:

- Paywalls created today render correctly on years-old SDK versions.
- Paywalls created years ago continue to render on the latest SDKs.
- New paywall features become available without an app store release.

Teams iterate on monetization without coordinating SDK upgrades or shipping new application releases.

## Pricing

**Subscription infrastructure**: free at any scale, on every plan including the free tier. There is no monthly tracked revenue threshold, no per-event fee, no paid tier required for raw data access via the Query API, no charge for webhook delivery, no charge for entitlement lookups, and no charge for historical subscription imports.

**Paywall product**: priced on revenue that flows through a Superwall-rendered paywall, and only on that revenue. Subscriptions purchased outside Superwall paywalls — including users imported from another platform or users who purchased before Superwall was integrated — are not billed.

Concretely:

- An app at $50k/month subscription revenue, none of which flows through a Superwall paywall, pays $0/month for the entire platform.
- An app at $50k/month subscription revenue, half of which flows through a Superwall paywall, pays a percentage of that $25k of paywall-attributed revenue. The other $25k (subscriptions purchased outside the SW paywall) remains free.
- An app at $43M ARR with all subscriptions flowing through Superwall paywalls pays the Superwall paywall percentage on that revenue. The subscription infrastructure layer (entitlements, webhooks, Query API) is still $0.

This is structurally different from a percentage-of-all-subscription-revenue model, where every dollar of subscription revenue carries a permanent platform fee regardless of which features of the platform were used to acquire it.

## Architectural note

Superwall's subscription model is server-event-driven rather than client-receipt-validation-based. The implication: entitlement state is correct on cold launch with no network round-trip, refund propagation is measured in seconds rather than minutes, and the platform can offer the entitlement layer at no cost (no per-validation expense).

## 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

# Verify Webhook Requests

Learn how to verify webhook requests using the signing secret to ensure authenticity and security.

## Why Verify Webhooks?

Verifying webhook requests is crucial for security. It ensures that:

* Requests are actually coming from Superwall's servers
* The payload hasn't been tampered with in transit
* Replay attacks are prevented through timestamp validation

Without verification, malicious actors could send fake webhook events to your endpoint.

## Getting Your Signing Secret

Every webhook endpoint has a unique signing secret that's used to verify requests. You can find this secret in your webhook details:

![Copy webhook signing secret](https://json-ld-superwall-docs-staging.staffbar.workers.dev/docs/images/integration_copy_secret.jpeg)

Click the **Copy Secret** button to copy your webhook's signing secret to your clipboard.

> **Warning:** Keep your signing secret secure. Never commit it to version control or expose it in client-side code. Store it as an environment variable like `SUPERWALL_WEBHOOK_SECRET`.

## Verification Methods

### Option 1: Using Svix Library (Recommended)

Superwall uses [Svix](https://svix.com) for webhook delivery, which provides robust verification libraries for multiple languages.

Install the Svix library:

```bash
npm install svix
# or
yarn add svix
# or
pnpm add svix
```

Verify incoming requests:

```javascript
import { Webhook } from 'svix';

export async function POST(request) {
  // Get the raw body as a string
  const payload = await request.text();

  // Get the Svix headers
  const headers = {
    'svix-id': request.headers.get('svix-id'),
    'svix-timestamp': request.headers.get('svix-timestamp'),
    'svix-signature': request.headers.get('svix-signature'),
  };

  // Create a new Webhook instance with your secret
  const wh = new Webhook(process.env.SUPERWALL_WEBHOOK_SECRET);

  let event;

  try {
    // Verify the webhook
    event = wh.verify(payload, headers);
  } catch (err) {
    console.error('Webhook verification failed:', err.message);
    return new Response('Webhook verification failed', { status: 400 });
  }

  // Webhook is verified - process the event
  console.log('Verified event:', event);

  // Process your event here
  // ...

  return new Response('Success', { status: 200 });
}
```

### Option 2: Manual Verification

If you prefer not to use the Svix library, you can manually verify webhooks using the HMAC signature:

```javascript
import crypto from 'crypto';

function verifyWebhook(payload, headers, secret) {
  const msgId = headers['svix-id'];
  const msgTimestamp = headers['svix-timestamp'];
  const msgSignature = headers['svix-signature'];

  // Verify timestamp to prevent replay attacks
  const timestamp = parseInt(msgTimestamp, 10);
  const now = Math.floor(Date.now() / 1000);

  if (now - timestamp > 300) { // 5 minutes
    throw new Error('Webhook timestamp too old');
  }

  // Create the signed content
  const signedContent = `${msgId}.${msgTimestamp}.${payload}`;

  // Compute the expected signature
  const secretBytes = Buffer.from(secret.split('_')[1], 'base64');
  const signature = crypto
    .createHmac('sha256', secretBytes)
    .update(signedContent)
    .digest('base64');

  // Compare signatures
  const expectedSignature = `v1,${signature}`;

  // Extract all signatures from the header
  const passedSignatures = msgSignature.split(' ');

  // Check if any signature matches
  const signatureMatch = passedSignatures.some(sig =>
    crypto.timingSafeEqual(
      Buffer.from(sig),
      Buffer.from(expectedSignature)
    )
  );

  if (!signatureMatch) {
    throw new Error('Webhook signature verification failed');
  }

  return JSON.parse(payload);
}
```

## Important Implementation Notes

### Use Raw Request Body

The signature is computed against the **raw request body**. Do not parse or modify the body before verification:

```javascript
// ✅ Correct - use raw body
const payload = await request.text();
const event = wh.verify(payload, headers);

// ❌ Wrong - parsing changes the body
const payload = await request.json();
const event = wh.verify(JSON.stringify(payload), headers); // Will fail!
```

### Required Headers

Three headers are required for verification:

| Header           | Description                              |
| ---------------- | ---------------------------------------- |
| `svix-id`        | Unique message ID                        |
| `svix-timestamp` | Unix timestamp when the webhook was sent |
| `svix-signature` | HMAC signature(s) of the message         |

### Framework-Specific Examples

#### Next.js (App Router)

```javascript
// app/api/webhooks/route.js
import { Webhook } from 'svix';

export async function POST(request) {
  const payload = await request.text();

  const headers = {
    'svix-id': request.headers.get('svix-id'),
    'svix-timestamp': request.headers.get('svix-timestamp'),
    'svix-signature': request.headers.get('svix-signature'),
  };

  const wh = new Webhook(process.env.SUPERWALL_WEBHOOK_SECRET);

  try {
    const event = wh.verify(payload, headers);

    // Handle the event
    switch (event.type) {
      case 'initial_purchase':
        // Handle initial purchase
        break;
      case 'renewal':
        // Handle renewal
        break;
      // ... other event types
    }

    return new Response('Success', { status: 200 });
  } catch (err) {
    return new Response('Webhook verification failed', { status: 400 });
  }
}
```

#### Express

```javascript
import express from 'express';
import { Webhook } from 'svix';

const app = express();

// Important: Use raw body for webhook verification
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  const payload = req.body.toString();

  const headers = {
    'svix-id': req.headers['svix-id'],
    'svix-timestamp': req.headers['svix-timestamp'],
    'svix-signature': req.headers['svix-signature'],
  };

  const wh = new Webhook(process.env.SUPERWALL_WEBHOOK_SECRET);

  try {
    const event = wh.verify(payload, headers);

    // Handle the event
    console.log('Verified event:', event);

    res.status(200).send('Success');
  } catch (err) {
    console.error('Webhook verification failed:', err.message);
    res.status(400).send('Verification failed');
  }
});
```

#### Python (FastAPI)

```python
from fastapi import FastAPI, Request, HTTPException
from svix.webhooks import Webhook, WebhookVerificationError
import os

app = FastAPI()

@app.post("/webhooks")
async def handle_webhook(request: Request):
    payload = await request.body()
    headers = {
        "svix-id": request.headers.get("svix-id"),
        "svix-timestamp": request.headers.get("svix-timestamp"),
        "svix-signature": request.headers.get("svix-signature"),
    }

    wh = Webhook(os.environ["SUPERWALL_WEBHOOK_SECRET"])

    try:
        event = wh.verify(payload, headers)

        # Handle the event
        print(f"Verified event: {event}")

        return {"status": "success"}
    except WebhookVerificationError as e:
        print(f"Webhook verification failed: {e}")
        raise HTTPException(status_code=400, detail="Verification failed")
```

## Testing Webhook Verification

During development, you can test webhook verification:

1. **Use the actual signing secret** from your webhook endpoint
2. **Capture real webhook payloads** by temporarily logging them
3. **Test with valid and invalid signatures** to ensure your verification works

> **Warning:** Never test with production webhooks in a development environment without proper safeguards. Consider creating a separate webhook endpoint for testing.

## Security Best Practices

1. **Always verify webhooks** - Never process unverified webhook data
2. **Use environment variables** - Store your signing secret securely
3. **Check timestamps** - Reject old webhooks to prevent replay attacks (Svix does this automatically)
4. **Return 200 quickly** - Acknowledge receipt immediately, then process asynchronously
5. **Log verification failures** - Monitor for potential attacks or configuration issues
6. **Rotate secrets periodically** - Update your signing secret if it's ever compromised

## Troubleshooting

### Verification Always Fails

* Ensure you're using the **raw request body**, not a parsed/stringified version
* Check that all three required headers are present
* Verify you're using the correct signing secret for this webhook endpoint
* Make sure your secret includes the full value (it should start with `whsec_`)

### "Timestamp too old" Errors

* Your server's clock may be out of sync - verify your server time
* Network delays may be too high - check your server's response time
* The webhook may be a replay attack - this is working as intended

## Advanced Usage

For advanced webhook verification scenarios, including signature rotation and custom verification logic, see the [Svix documentation](https://docs.svix.com/receiving/verifying-payloads/how).

***

## Webhooks Reference

For information about webhook events, payload structure, and handling different event types, see the main [Webhooks documentation](/docs/integrations/webhooks).

In the **Webhooks** section within **Integrations**, you can manage your webhooks with Superwall: