supapulsedocs
Revenue packageAPI reference

API reference

The revenue and identify events the ingest API accepts, the fingerprint endpoint, the Stripe webhook, what each stores, and the @supapulse/revenue SDK surface.

Revenue request

POST /api/ingest HTTP/1.1
Host: supapulse.app
Content-Type: application/json
X-API-Token: sec_your_secret_token

{
  "type": "revenue",
  "data": {
    "customer_id": "user_123",
    "amount": 49.90,
    "currency": "USD",
    "transaction_id": "in_1Q8xYz",
    "event_type": "renewal",
    "payment_processor": "stripe"
  }
}

Revenue needs the secret token. A public token gets 422, because revenue isn't an allowed type for it.

Fields

FieldTypeRequiredDefaultRule
customer_idstringYesnoneUp to 255 characters.
amountnumberYesnoneMajor units, from 0 to 99999999.99. Never negative.
transaction_idstringYesnoneUp to 255 characters. Unique per site and event_type.
event_typestringNopurchaseOne of the event types.
currencystringNoUSDThree letters. Uppercased. Not converted.
visitor_fingerprintstringNonullUp to 64 characters. From getVisitorFingerprint().
session_idstringNonullUp to 64 characters. From getSessionId().
subscription_idstringNonullUp to 255 characters. customer_subscription_id is accepted as an alias.
mrr_impactnumberNo0Change in monthly recurring revenue, from -99999999.99 to 99999999.99.
is_recurringbooleanNoby event typeDefaults to true for subscription, renewal, upgrade, downgrade and churn.
metadataobjectNonullUp to 50 keys. Stored as JSON.
payment_processorstringNostripeUp to 32 characters. Free text.
occurred_atdateNonowISO 8601. Not more than a day in the future.

Fields not listed are ignored.

Event types

TypeStored amountmrr_impact
purchaseAs sentAs sent
subscriptionAs sentPositive
renewalAs sentAs sent, or 0 once the subscription has MRR
upgradeAs sentPositive
downgradeAs sentNegative
churnAs sentNegative; with subscription_id, the MRR recorded for it
refundNegativeAs sent

Send amount as a positive number. For refund, Supapulse stores it as negative so revenue is net of refunds. mrr_impact signs are fixed for you: send the size of the change.

Idempotency

A revenue event is identified by the site, transaction_id and event_type. Sending the same combination again returns 202 and stores nothing, so retries are safe. The same transaction_id with a different event_type is a different event.

Attribution

Supapulse resolves the visit in this order: visitor_fingerprint, session_id, then the touch stored for the customer_id. See Attribution.

Responses from /api/ingest

StatusBodyMeaning
202{ "success": true }Validated and queued. The worker stores it shortly after.
401{ "error": "Missing API token" }No X-API-Token header and no token in the body.
401{ "error": "Invalid token format" }The token doesn't start with pub_ or sec_.
401{ "error": "Invalid API token" }No active site has this token.
402{ "error": "…", "code": "plan_inactive" }The site's team has no active plan.
402{ "error": "…", "code": "quota_exceeded" }Monthly event limit reached. Never returned for revenue.
403{ "error": "Origin not allowed" }An Origin or Referer header was sent and doesn't match the site's domain. Server-side requests usually send neither.
422Validation errorsA field broke a rule above, type or data is missing, or type isn't allowed for the token (a public token sending revenue).
429More than 2,000 requests in an hour from one IP address. Wait for Retry-After.

Identify request

POST /api/ingest HTTP/1.1
Host: supapulse.app
Content-Type: text/plain;charset=UTF-8

{
  "type": "identify",
  "token": "pub_your_public_token",
  "data": { "customer_id": "user_123", "session_id": "6f1c2e9a-…" }
}
FieldTypeRequiredRule
customer_idstringYesUp to 255 characters.
session_idstringNoUp to 64 characters.
visitor_fingerprintstringNoUp to 64 characters. Secret token only.
source_customer_idstringNoUp to 255 characters. Secret token only. Copies the attribution already stored for another customer ID.

Identify works with the public or the secret token. With a public token, visitor_fingerprint and source_customer_id are discarded and the fingerprint is built from the request's IP address and user agent. Identify events never count toward usage.

Fingerprint request

POST /api/fingerprint HTTP/1.1
Host: supapulse.app
Content-Type: text/plain;charset=UTF-8

{ "token": "pub_your_public_token" }
200 OK
{ "visitor_fingerprint": "9b74c9897bac770ffc029102a200c5de…" }

The value is the same hash Supapulse stores on the visitor's pageviews. Call it from the browser: it's built from the caller's IP address and user agent. It returns 401, 403 and 402 like /api/ingest, but is never limited by the monthly event quota.

Stripe webhook

POST https://supapulse.app/api/webhooks/stripe/{publicToken}

Stripe calls this endpoint. It needs a signing secret saved in the site's settings.

StatusBodyMeaning
200{ "received": true }Verified and queued.
200{ "ignored": "plan_inactive" }Verified, but the team has no active plan. Nothing was stored.
400{ "error": "Invalid signature" }The signature didn't verify, or the timestamp is older than 5 minutes.
404{ "error": "Not found" }Unknown public token, or no signing secret saved for the site.
429Over the ingest rate limit.

See the Stripe guide for what each event becomes.

The @supapulse/revenue SDK

import { Revenue, type RevenueConfig, type RevenueData } from '@supapulse/revenue';

const revenue = new Revenue(config: RevenueConfig);
await revenue.track(data: RevenueData);

track() validates its input, then posts the same request as above with the token in both the X-API-Token header and the body. It retries network errors, 5xx and 429 responses up to maxRetries times, then throws. Other 4xx responses, including 402 and 422, throw immediately with the status and body.

What track() validates

CheckError message
customer_id is setcustomer_id is required
amount is a number ≥ 0amount must be a positive number
transaction_id is settransaction_id is required
event_type, if set, is one of the seven typesevent_type must be one of: …
mrr_impact, if set, is a numbermrr_impact must be a number
occurred_at, if set, is a valid dateoccurred_at must be a valid date

visitor_fingerprint and session_id are optional.

Types

interface RevenueConfig {
  apiToken: string;
  endpoint?: string;
  debug?: boolean;
  maxRetries?: number;
  retryBackoff?: number;
}

interface RevenueData {
  visitor_fingerprint?: string;
  session_id?: string;
  customer_id: string;
  amount: number;
  currency?: string;
  transaction_id: string;
  payment_processor?: string;
  event_type?: 'purchase' | 'subscription' | 'renewal' | 'upgrade' | 'downgrade' | 'churn' | 'refund';
  occurred_at?: Date | string;
  subscription_id?: string;
  /** @deprecated Use `subscription_id`. */
  customer_subscription_id?: string;
  mrr_impact?: number;
  is_recurring?: boolean;
  metadata?: Record<string, any>;
}