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#

  1. Frontend: Collect visitor fingerprint and session ID
  2. Payment: Pass data through payment processor metadata
  3. Webhook: Track revenue with attribution data
  4. 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#