# Examples

URL: https://supapulse.app/docs/revenue/examples

End-to-end revenue tracking with Stripe Checkout and Express, the SDK, Laravel, and payments that never touch a browser.

## Express and Stripe Checkout [#express-and-stripe-checkout]

With the built-in Stripe webhook, your server only creates the checkout. Supapulse receives the payment from Stripe directly.

```ts title="server.ts"
import express from 'express';
import Stripe from 'stripe';

const app = express();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

// The browser calls supapulse.identify(user.id) after login, then posts
// { priceId, fingerprint, sessionId } here when checkout starts.
app.post('/checkout', express.json(), async (req, res) => {
  const { priceId, fingerprint, sessionId } = req.body;
  const metadata = {
    supapulse_customer_id: req.user.id,
    supapulse_visitor_fingerprint: fingerprint ?? '',
    supapulse_session_id: sessionId ?? '',
  };

  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: 'https://acme.com/thanks',
    cancel_url: 'https://acme.com/pricing',
    metadata,
    subscription_data: { metadata },
  });
  res.json({ url: session.url });
});

app.listen(3000);
```

Add the Supapulse endpoint to Stripe once, in the dashboard. See the [Stripe guide](https://supapulse.app/docs/guides/stripe).

## Express with the SDK [#express-with-the-sdk]

For a processor Supapulse has no webhook for, send revenue from your own handler with `@supapulse/revenue`:

```ts title="webhooks.ts"
import { Revenue } from '@supapulse/revenue';

const revenue = new Revenue({ apiToken: process.env.SUPAPULSE_SECRET_TOKEN! });

app.post('/webhooks/acme-pay', express.json(), async (req, res) => {
  const payment = verify(req); // your processor's signature check

  await revenue.track({
    customer_id: payment.metadata.user_id,
    amount: payment.amount / 100,
    currency: payment.currency.toUpperCase(),
    transaction_id: payment.id,
    event_type: 'purchase',
    payment_processor: 'acme-pay',
  });

  res.json({ received: true });
});
```

If `track()` throws, the handler returns a `5xx` and the processor retries. The repeated `transaction_id` isn't stored twice.

## Laravel [#laravel]

```php title="app/Services/Supapulse.php"
<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;

class Supapulse
{
    /**
     * Send a revenue event. Throws on a non-2xx response so the webhook is retried.
     */
    public function trackRevenue(string $customerId, float $amount, string $transactionId, string $currency = 'USD', string $eventType = 'purchase', string $processor = 'paddle'): void
    {
        Http::withHeaders(['X-API-Token' => config('services.supapulse.secret_token')])
            ->post('https://supapulse.app/api/ingest', [
                'type' => 'revenue',
                'data' => [
                    'customer_id' => $customerId,
                    'amount' => $amount,
                    'currency' => $currency,
                    'transaction_id' => $transactionId,
                    'event_type' => $eventType,
                    'payment_processor' => $processor,
                ],
            ])
            ->throw();
    }
}
```

```php title="In a payment webhook handler"
app(\App\Services\Supapulse::class)->trackRevenue(
    customerId: (string) $order->user_id,
    amount: $payment->amount / 100,
    transactionId: $payment->id,
    currency: strtoupper($payment->currency),
);
```

Add `'supapulse' => ['secret_token' => env('SUPAPULSE_SECRET_TOKEN')]` to `config/services.php`.

## Paddle Billing [#paddle-billing]

See the [Paddle guide](https://supapulse.app/docs/guides/paddle) for the checkout and the webhook handler.

## Payments without a browser [#payments-without-a-browser]

Invoices paid by bank transfer, or deals closed by your sales team, have no browser visit. Send them with a customer ID so totals stay right:

```bash
curl -X POST https://supapulse.app/api/ingest \
  -H "Content-Type: application/json" \
  -H "X-API-Token: $SUPAPULSE_SECRET_TOKEN" \
  -d '{
    "type": "revenue",
    "data": {
      "customer_id": "acme-corp",
      "amount": 4800,
      "currency": "USD",
      "transaction_id": "invoice-2026-0142",
      "payment_processor": "manual"
    }
  }'
```

## Handling errors [#handling-errors]

- Throw when Supapulse answers with an error, and let the webhook fail. Processors retry, and the unique `transaction_id` stops a retry from counting twice.
- Don't swallow a `401`: it means the secret token changed, and every payment until you fix it is lost.
- A `402` means the site's team has no active plan. Revenue is never blocked by the monthly event limit.
- A `422` names the field that failed. Fix the payload; retrying the same request won't help.
