Basic Usage
Learn how to track revenue events with full attribution to customer journeys.
Quick Start
import { Revenue } from '@pulsora/revenue';
// Initialize
const revenue = new Revenue({
apiToken: process.env.PULSORA_SECRET_TOKEN,
});
// Track a purchase
await revenue.track({
visitor_fingerprint: 'fp_from_payment_metadata',
session_id: 'session_from_payment_metadata',
customer_id: 'cus_123',
amount: 99.99,
currency: 'USD',
transaction_id: 'tx_abc123',
payment_processor: 'stripe',
event_type: 'purchase',
});
Core Concepts
Attribution Flow
- Frontend: Collect visitor fingerprint and session ID
- Payment: Pass data through payment processor metadata
- Webhook: Track revenue with attribution data
- Dashboard: View attributed revenue and ROI
Required Fields
Every revenue event must include:
{
visitor_fingerprint: string, // From @pulsora/core
session_id: string, // From @pulsora/core
customer_id: string, // Your customer identifier
amount: number, // Revenue amount (positive)
transaction_id: string // Unique transaction ID
}
Basic Revenue Tracking
One-Time Purchase
await revenue.track({
visitor_fingerprint: metadata.visitor_fingerprint,
session_id: metadata.session_id,
customer_id: 'cus_123',
amount: 49.99,
currency: 'USD',
transaction_id: 'tx_' + Date.now(),
payment_processor: 'stripe',
event_type: 'purchase',
metadata: {
product_name: 'Premium Plan',
product_id: 'plan_premium',
coupon: 'SAVE20',
},
});
Subscription
await revenue.track({
visitor_fingerprint: metadata.visitor_fingerprint,
session_id: metadata.session_id,
customer_id: 'cus_456',
amount: 29.99,
currency: 'USD',
transaction_id: 'sub_abc123',
payment_processor: 'stripe',
event_type: 'subscription',
customer_subscription_id: 'sub_abc123',
mrr_impact: 29.99,
is_recurring: true,
metadata: {
plan: 'pro',
billing_interval: 'monthly',
seats: 5,
},
});
Refund
await revenue.track({
visitor_fingerprint: metadata.visitor_fingerprint,
session_id: metadata.session_id,
customer_id: 'cus_789',
amount: -49.99, // Negative amount for refunds
currency: 'USD',
transaction_id: 'refund_xyz789',
payment_processor: 'stripe',
event_type: 'refund',
metadata: {
original_transaction_id: 'tx_original',
refund_reason: 'customer_request',
},
});
Event Types
purchase
One-time payment for products or services.
{
event_type: 'purchase',
amount: 99.99,
metadata: {
product_id: 'sku_123',
quantity: 1
}
}
subscription
New recurring subscription.
{
event_type: 'subscription',
amount: 29.99,
customer_subscription_id: 'sub_123',
mrr_impact: 29.99,
is_recurring: true
}
renewal
Subscription renewal payment.
{
event_type: 'renewal',
amount: 29.99,
customer_subscription_id: 'sub_123',
is_recurring: true
}
upgrade
Subscription plan upgrade.
{
event_type: 'upgrade',
amount: 49.99,
customer_subscription_id: 'sub_123',
mrr_impact: 20.00, // Increase from 29.99 to 49.99
metadata: {
old_plan: 'pro',
new_plan: 'enterprise'
}
}
downgrade
Subscription plan downgrade.
{
event_type: 'downgrade',
amount: 19.99,
customer_subscription_id: 'sub_123',
mrr_impact: -10.00, // Decrease from 29.99 to 19.99
metadata: {
old_plan: 'pro',
new_plan: 'basic'
}
}
churn
Subscription cancellation.
{
event_type: 'churn',
amount: 0,
customer_subscription_id: 'sub_123',
mrr_impact: -29.99,
metadata: {
cancellation_reason: 'too_expensive',
churned_mrr: 29.99
}
}
refund
Full or partial refund.
{
event_type: 'refund',
amount: -99.99, // Negative amount
metadata: {
original_transaction_id: 'tx_123',
refund_type: 'full'
}
}
Complete Integration Example
1. Frontend - Collect Attribution Data
// In your checkout page
import { Pulsora } from '@pulsora/core';
const pulsora = new Pulsora();
pulsora.init({ apiToken: 'pub_public_token' });
async function startCheckout(priceId) {
// Get attribution data
const fingerprint = await pulsora.getVisitorFingerprint();
const sessionId = pulsora.getSessionId();
// Send to your backend
const response = await fetch('/api/create-checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
priceId,
visitor_fingerprint: fingerprint,
session_id: sessionId,
}),
});
const { checkoutUrl } = await response.json();
window.location.href = checkoutUrl;
}
2. Backend - Create Payment Session
// Express endpoint
app.post('/api/create-checkout', async (req, res) => {
const { priceId, visitor_fingerprint, session_id } = req.body;
// Create Stripe checkout session
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price: priceId,
quantity: 1,
},
],
mode: 'payment',
success_url: 'https://yoursite.com/success',
cancel_url: 'https://yoursite.com/cancel',
metadata: {
visitor_fingerprint,
session_id,
},
});
res.json({ checkoutUrl: session.url });
});
3. Webhook - Track Revenue
// Stripe webhook endpoint
app.post('/webhooks/stripe', async (req, res) => {
const event = req.body;
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object;
await revenue.track({
visitor_fingerprint: session.metadata.visitor_fingerprint,
session_id: session.metadata.session_id,
customer_id: session.customer,
amount: session.amount_total / 100,
currency: session.currency.toUpperCase(),
transaction_id: session.id,
payment_processor: 'stripe',
event_type: 'purchase',
});
break;
case 'customer.subscription.created':
const subscription = event.data.object;
await revenue.track({
visitor_fingerprint: subscription.metadata.visitor_fingerprint,
session_id: subscription.metadata.session_id,
customer_id: subscription.customer,
amount: subscription.items.data[0].price.unit_amount / 100,
currency: subscription.currency.toUpperCase(),
transaction_id: subscription.id,
payment_processor: 'stripe',
event_type: 'subscription',
customer_subscription_id: subscription.id,
mrr_impact: subscription.items.data[0].price.unit_amount / 100,
is_recurring: true,
});
break;
}
res.json({ received: true });
});
Advanced Patterns
Batch Processing
Process multiple revenue events efficiently:
async function processBatchRevenue(events) {
const results = await Promise.allSettled(
events.map((event) => revenue.track(event)),
);
results.forEach((result, index) => {
if (result.status === 'rejected') {
console.error(`Failed to track event ${index}:`, result.reason);
}
});
const successful = results.filter((r) => r.status === 'fulfilled').length;
console.log(`Tracked ${successful}/${events.length} revenue events`);
}
Retry Logic
Handle transient failures:
async function trackRevenueWithRetry(data, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await revenue.track(data);
return; // Success
} catch (error) {
if (attempt === maxAttempts) {
// Log to error tracking service
console.error('Revenue tracking failed after retries:', error);
throw error;
}
// Wait before retry
const delay = Math.pow(2, attempt) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
Deduplication
Prevent duplicate revenue tracking:
const processedTransactions = new Set();
async function trackRevenueOnce(data) {
const key = `${data.transaction_id}_${data.event_type}`;
if (processedTransactions.has(key)) {
console.log('Transaction already processed:', key);
return;
}
try {
await revenue.track(data);
processedTransactions.add(key);
} catch (error) {
console.error('Failed to track revenue:', error);
throw error;
}
}
Currency Conversion
Track revenue in consistent currency:
const exchangeRates = {
EUR: 1.18,
GBP: 1.38,
// ... more rates
};
async function trackRevenueInUSD(data) {
let amountUSD = data.amount;
if (data.currency !== 'USD') {
const rate = exchangeRates[data.currency];
if (!rate) {
throw new Error(`Unknown currency: ${data.currency}`);
}
amountUSD = data.amount * rate;
}
await revenue.track({
...data,
amount: amountUSD,
currency: 'USD',
metadata: {
...data.metadata,
original_amount: data.amount,
original_currency: data.currency,
exchange_rate: exchangeRates[data.currency],
},
});
}
Testing
Unit Tests
// __tests__/revenue.test.js
const { Revenue } = require('@pulsora/revenue');
describe('Revenue Tracking', () => {
let revenue;
beforeEach(() => {
revenue = new Revenue({
apiToken: process.env.PULSORA_TEST_TOKEN,
debug: true,
});
});
test('tracks purchase event', async () => {
const data = {
visitor_fingerprint: 'test_fp_123',
session_id: 'test_session_123',
customer_id: 'test_customer',
amount: 99.99,
currency: 'USD',
transaction_id: 'test_tx_' + Date.now(),
payment_processor: 'test',
event_type: 'purchase',
};
await expect(revenue.track(data)).resolves.not.toThrow();
});
test('validates required fields', async () => {
const invalidData = {
customer_id: 'test_customer',
amount: 99.99,
// Missing required fields
};
await expect(revenue.track(invalidData)).rejects.toThrow();
});
});
Integration Tests
// Test with real webhook data
const testWebhook = {
type: 'checkout.session.completed',
data: {
object: {
id: 'cs_test_123',
customer: 'cus_test_123',
amount_total: 9999,
currency: 'usd',
metadata: {
visitor_fingerprint: 'fp_test_123',
session_id: 'session_test_123',
},
},
},
};
// Process webhook
await processStripeWebhook(testWebhook);
// Verify in dashboard
// Check that revenue appears with correct attribution
Best Practices
1. Always Include Attribution
// ❌ Bad - Missing attribution
await revenue.track({
customer_id: 'cus_123',
amount: 99.99,
transaction_id: 'tx_123',
});
// ✅ Good - Complete attribution
await revenue.track({
visitor_fingerprint: metadata.visitor_fingerprint,
session_id: metadata.session_id,
customer_id: 'cus_123',
amount: 99.99,
transaction_id: 'tx_123',
payment_processor: 'stripe',
event_type: 'purchase',
});
2. Use Meaningful Metadata
// ✅ Good metadata
metadata: {
plan_name: 'Professional',
billing_period: 'annual',
discount_code: 'SAVE20',
referral_source: 'partner_abc',
seats: 10,
addons: ['advanced_analytics', 'priority_support']
}
3. Handle Errors Gracefully
app.post('/webhook', async (req, res) => {
try {
await revenue.track(data);
res.json({ success: true });
} catch (error) {
// Log error but don't fail webhook
console.error('Revenue tracking error:', error);
// Still acknowledge webhook
res.json({ success: true });
// Queue for retry
await queueForRetry(data);
}
});
4. Validate Amounts
function validateAmount(amount, eventType) {
if (eventType === 'refund' && amount > 0) {
throw new Error('Refund amount must be negative');
}
if (eventType !== 'refund' && amount <= 0) {
throw new Error('Revenue amount must be positive');
}
if (!Number.isFinite(amount)) {
throw new Error('Invalid amount');
}
return true;
}
Debugging
Enable Debug Mode
const revenue = new Revenue({
apiToken: process.env.PULSORA_SECRET_TOKEN,
debug: true,
});
// Logs detailed information:
// [Pulsora Revenue] Tracking revenue event...
// [Pulsora Revenue] Payload: { ... }
// [Pulsora Revenue] Response: 200 OK
Common Issues
"Missing visitor_fingerprint"
- Ensure frontend passes attribution data
- Check payment metadata is preserved
- Verify webhook receives metadata
"Invalid amount"
- Amount must be a number
- Positive for revenue, negative for refunds
- No currency symbols or formatting
"Duplicate transaction"
- Each transaction_id must be unique
- Use payment provider's transaction ID
- Add timestamp for uniqueness if needed
Next Steps
- Webhook Integration - Set up payment webhooks
- Attribution - Understand visitor attribution
- API Reference - Complete method documentation
- Examples - Real-world implementations