supapulsedocs
Revenue packageBasic usage

Basic 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. 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 sends all of it for you.

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.

A one-time purchase

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

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.

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

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.

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

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.

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

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

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.

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

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:

{
  "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.