# Basic usage

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

Send purchases, subscription payments, refunds and cancellations to Supapulse from your server, keyed by customer ID.

These examples use the `trackRevenue()` helper from [Installation](https://supapulse.app/docs/revenue/installation#http-api). It posts a `revenue` event to `/api/ingest` with the secret token. With the SDK, the same fields go to `revenue.track()`.

If you use Stripe, you don't need this page: the [Stripe webhook](https://supapulse.app/docs/guides/stripe) sends all of it for you.

## The steps [#the-steps]

1. **Browser:** call `supapulse.identify(user.id)` after login or signup.
2. **Server:** when a payment arrives, call `trackRevenue()` with `customerId` set to the same ID.
3. **Optional:** pass `getVisitorFingerprint()` and `getSessionId()` through checkout when payment can happen before an account exists. See [Attribution](https://supapulse.app/docs/revenue/attribution#checkout-metadata).

## A one-time purchase [#a-one-time-purchase]

```ts
await trackRevenue({
  customerId: user.id,
  amount: 99.0,
  currency: 'USD',
  transactionId: payment.id,
  eventType: 'purchase',
  paymentProcessor: 'paddle',
});
```

## Subscriptions [#subscriptions]

Send one event per paid invoice, with an event type that says what it was. Use the invoice ID as the transaction ID so each payment is counted once.

```ts
// First payment: starts the subscription and adds to MRR.
await trackRevenue({
  customerId: user.id,
  amount: 29,
  transactionId: invoice.id,
  eventType: 'subscription',
  mrrImpact: 29,
  subscriptionId: subscription.id,
});

// Every renewal: revenue, no MRR change once the subscription has MRR.
await trackRevenue({
  customerId: user.id,
  amount: 29,
  transactionId: invoice.id,
  eventType: 'renewal',
  subscriptionId: subscription.id,
});
```

Renewals take the first touch stored for the customer, so you don't carry the visit forward yourself.

## Upgrades, downgrades and churn [#upgrades-downgrades-and-churn]

`mrr_impact` is the change in monthly recurring revenue. Supapulse fixes its sign for you: `upgrade` and `subscription` are positive, `downgrade` and `churn` are negative.

```ts
await trackRevenue({ customerId: user.id, amount: 0, transactionId: `up-${change.id}`, eventType: 'upgrade', mrrImpact: 20 });
await trackRevenue({ customerId: user.id, amount: 0, transactionId: `cancel-${sub.id}`, eventType: 'churn', mrrImpact: 49 });
```

## Refunds [#refunds]

Send the refunded amount as a positive number with `event_type: "refund"`. Supapulse stores it as negative, so revenue is net of refunds. Give each refund its own `transaction_id`.

```ts
await trackRevenue({ customerId: user.id, amount: 15, transactionId: refund.id, eventType: 'refund' });
```

## Rules that matter [#rules-that-matter]

- **Amounts are in major units.** Processors report minor units (cents); divide by 100 for two-decimal currencies.
- **`customer_id` is required.** Use one stable ID per customer, the same one you pass to `identify()`.
- **`transaction_id` must be unique per event type.** A repeat of the same ID and type isn't stored, which makes webhook retries safe. Two different payments must never share one.
- **One currency per site.** Reports add amounts without converting them.

## Payments with no visit [#payments-with-no-visit]

Invoices paid by bank transfer or sales closed by hand have no browser visit. Send them with a `customer_id` and no fingerprint or session. If that customer was identified before, the stored touch applies. Otherwise the payment counts in totals and shows as `direct`.

```ts
await trackRevenue({
  customerId: 'acme-corp',
  amount: 4800,
  currency: 'USD',
  transactionId: `invoice-${invoice.number}`,
  paymentProcessor: 'manual',
});
```

## Errors [#errors]

`trackRevenue()` throws on a non-2xx response. Contents are checked before the event is queued, so a missing field or a bad value comes back as `422` with the field named:

```json
{
  "message": "The data.customer_id field is required.",
  "errors": { "data.customer_id": ["The data.customer_id field is required."] }
}
```

A `402` means the site's team has no active plan. If Supapulse is unreachable or answers `5xx`, let your webhook fail so the provider retries it. The `transaction_id` keeps the retry from counting twice.
