@pulsora/revenue#

The Pulsora revenue package enables server-side revenue tracking with full attribution to visitor journeys. Track purchases, subscriptions, and other monetary events while maintaining user privacy.

When to Use Revenue Package#

Use @pulsora/revenue to:

  • Attribute sales to campaigns - Know which marketing channels drive revenue
  • Track MRR and churn - Monitor subscription metrics over time
  • Calculate CAC and LTV - Understand customer acquisition costs and lifetime value
  • Server-side tracking - Keep API tokens secure, never expose in browser
  • Multi-touch attribution - See the entire visitor journey before purchase
  • Payment processor integration - Works with Stripe, Paddle, and custom solutions

Important: This is a server-side package. Never use it in client-side code.

Key Features#

💰 Complete Revenue Tracking#

  • One-time purchases - Track individual transactions
  • Subscriptions - Monitor MRR, upgrades, and churn
  • Refunds - Handle negative revenue events
  • Multi-currency - Support for all major currencies

🎯 Full Attribution#

  • First-touch attribution - Original traffic source
  • Last-touch attribution - Final conversion source
  • Multi-touch journey - Complete visitor path
  • UTM parameters - Campaign tracking

🔒 Privacy-First#

  • Server-side only - API tokens stay secure
  • Hashed customer IDs - No PII storage
  • GDPR compliant - Privacy by design
  • Anonymous tracking - No personal data required

🚀 Easy Integration#

  • Webhook ready - Stripe, Paddle, and more
  • TypeScript support - Full type safety
  • Automatic retries - Reliable data delivery
  • Small bundle - ~1.3KB gzipped

How It Works#

1. Visitor Journey Tracking#

When a visitor browses your site, Pulsora tracks:

  • Traffic sources (Google, Facebook, etc.)
  • Campaign parameters (UTM tags)
  • Page views and events
  • Session information

2. Payment Attribution#

During checkout, you pass the visitor's fingerprint and session ID to your payment processor as metadata.

3. Revenue Recording#

When payment webhooks fire, you use the metadata to attribute revenue back to the original visitor journey.

Installation#

npm install @pulsora/revenue

⚠️ Important: This package is for server-side use only. Never expose your API token in client-side code.

Quick Start#

Basic Setup#

import { Revenue } from '@pulsora/revenue';

const revenue = new Revenue({
  apiToken: process.env.PULSORA_API_TOKEN,
});

Track a Purchase#

await revenue.track({
  visitor_fingerprint: 'fp_from_checkout',
  session_id: 'session_from_checkout',
  customer_id: 'cus_123',
  amount: 99.99,
  currency: 'USD',
  transaction_id: 'tx_abc123',
  payment_processor: 'stripe',
  event_type: 'purchase',
});

Track a Subscription#

await revenue.track({
  visitor_fingerprint: 'fp_from_checkout',
  session_id: 'session_from_checkout',
  customer_id: 'cus_123',
  amount: 29.99,
  currency: 'USD',
  transaction_id: 'sub_def456',
  payment_processor: 'stripe',
  event_type: 'subscription',
  customer_subscription_id: 'sub_123',
  mrr_impact: 29.99,
  is_recurring: true,
  metadata: {
    plan: 'pro',
    interval: 'monthly',
  },
});

Event Types#

purchase#

One-time payment for a product or service.

{
  event_type: 'purchase',
  amount: 99.99,
  currency: 'USD'
}

subscription#

New recurring subscription.

{
  event_type: 'subscription',
  amount: 29.99,
  mrr_impact: 29.99,
  is_recurring: true
}

renewal#

Subscription renewal payment.

{
  event_type: 'renewal',
  amount: 29.99,
  is_recurring: true
}

upgrade#

Customer upgraded their plan.

{
  event_type: 'upgrade',
  amount: 49.99,
  mrr_impact: 20.00, // Increase from previous plan
  metadata: {
    old_plan: 'basic',
    new_plan: 'pro'
  }
}

downgrade#

Customer downgraded their plan.

{
  event_type: 'downgrade',
  amount: 9.99,
  mrr_impact: -20.00, // Decrease from previous plan
  metadata: {
    old_plan: 'pro',
    new_plan: 'basic'
  }
}

churn#

Customer cancelled their subscription.

{
  event_type: 'churn',
  amount: 0,
  mrr_impact: -29.99,
  metadata: {
    reason: 'too_expensive',
    feedback: 'Great product but outside our budget'
  }
}

refund#

Full or partial refund issued.

{
  event_type: 'refund',
  amount: -99.99, // Negative amount
  transaction_id: 'refund_123',
  metadata: {
    original_transaction: 'tx_abc123',
    reason: 'customer_request'
  }
}

Attribution Flow#

Step 1: Collect Tracking Data#

In your frontend during checkout:

import { Pulsora } from '@pulsora/core';

const pulsora = new Pulsora();
pulsora.init({ apiToken: 'public-token' });

// During checkout
const fingerprint = await pulsora.getVisitorFingerprint();
const sessionId = pulsora.getSessionId();

// Send to your backend
await createCheckout({
  product_id: 'pro-plan',
  visitor_fingerprint: fingerprint,
  session_id: sessionId,
});

Step 2: Pass to Payment Processor#

Include tracking data as metadata:

// Stripe example
const session = await stripe.checkout.sessions.create({
  // ... other options
  metadata: {
    visitor_fingerprint: fingerprint,
    session_id: sessionId,
  },
});

Step 3: Track in Webhook#

When payment completes:

// In your webhook handler
const revenue = new Revenue({ apiToken: process.env.PULSORA_API_TOKEN });

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',
});

Security Best Practices#

1. Environment Variables#

Always store your API token in environment variables:

const revenue = new Revenue({
  apiToken: process.env.PULSORA_API_TOKEN,
});

2. Server-Side Only#

Never use this package in the browser:

// ❌ BAD - Exposes your API token
const revenue = new Revenue({ apiToken: 'sec_...' });

// ✅ GOOD - Server-side only
app.post('/webhook', async (req, res) => {
  const revenue = new Revenue({ apiToken: process.env.PULSORA_API_TOKEN });
  // ...
});

3. Validate Webhooks#

Always validate webhook signatures:

// Stripe example
const sig = req.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent(
  req.body,
  sig,
  process.env.STRIPE_WEBHOOK_SECRET,
);

Error Handling#

The package includes automatic retry logic:

try {
  await revenue.track({
    // ... revenue data
  });
} catch (error) {
  console.error('Revenue tracking failed:', error);

  // Log to your error service
  errorReporter.log(error, {
    context: 'revenue_tracking',
    customer_id: customerId,
  });
}

Configuration Options#

const revenue = new Revenue({
  apiToken: 'your-token', // Required
  endpoint: 'https://custom.api', // Optional custom endpoint
  debug: true, // Enable debug logging
  maxRetries: 5, // Max retry attempts (default: 3)
  retryBackoff: 2000, // Initial retry delay in ms (default: 1000)
});

Real-World Integration Examples#

Complete Stripe Integration#

// app/api/webhooks/stripe/route.ts
import Stripe from 'stripe';
import { Revenue } from '@pulsora/revenue';
import { headers } from 'next/headers';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const revenue = new Revenue({ apiToken: process.env.PULSORA_API_TOKEN! });

export async function POST(req: Request) {
  const body = await req.text();
  const signature = headers().get('stripe-signature')!;

  let event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    return Response.json({ error: 'Webhook signature verification failed' }, { status: 400 });
  }

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object;

      // Track purchase
      await revenue.track({
        visitor_fingerprint: session.metadata?.visitor_fingerprint,
        session_id: session.metadata?.session_id,
        customer_id: session.customer as string,
        amount: session.amount_total! / 100,
        currency: session.currency!.toUpperCase(),
        transaction_id: session.id,
        payment_processor: 'stripe',
        event_type: 'purchase',
        metadata: {
          product: session.metadata?.product_name,
          mode: session.mode
        }
      });
      break;
    }

    case 'customer.subscription.created': {
      const subscription = event.data.object;
      const amount = subscription.items.data[0].price.unit_amount! / 100;

      await revenue.track({
        visitor_fingerprint: subscription.metadata.visitor_fingerprint,
        session_id: subscription.metadata.session_id,
        customer_id: subscription.customer as string,
        customer_subscription_id: subscription.id,
        amount,
        currency: subscription.currency.toUpperCase(),
        transaction_id: subscription.id,
        payment_processor: 'stripe',
        event_type: 'subscription',
        mrr_impact: amount,
        is_recurring: true,
        metadata: {
          plan: subscription.items.data[0].price.id,
          interval: subscription.items.data[0].price.recurring?.interval
        }
      });
      break;
    }

    case 'invoice.paid': {
      const invoice = event.data.object;

      // Skip first invoice (already tracked as subscription)
      if (invoice.billing_reason === 'subscription_create') break;

      await revenue.track({
        customer_id: invoice.customer as string,
        customer_subscription_id: invoice.subscription as string,
        amount: invoice.amount_paid / 100,
        currency: invoice.currency.toUpperCase(),
        transaction_id: invoice.id,
        payment_processor: 'stripe',
        event_type: 'renewal',
        is_recurring: true
      });
      break;
    }

    case 'customer.subscription.updated': {
      const subscription = event.data.object;
      const previousAttributes = event.data.previous_attributes;

      // Check if plan changed
      if (previousAttributes?.items) {
        const oldAmount = previousAttributes.items.data[0].price.unit_amount / 100;
        const newAmount = subscription.items.data[0].price.unit_amount / 100;
        const mrrImpact = newAmount - oldAmount;

        await revenue.track({
          customer_id: subscription.customer as string,
          customer_subscription_id: subscription.id,
          amount: newAmount,
          currency: subscription.currency.toUpperCase(),
          transaction_id: subscription.id,
          payment_processor: 'stripe',
          event_type: mrrImpact > 0 ? 'upgrade' : 'downgrade',
          mrr_impact: mrrImpact,
          is_recurring: true,
          metadata: {
            old_plan: previousAttributes.items.data[0].price.id,
            new_plan: subscription.items.data[0].price.id
          }
        });
      }
      break;
    }

    case 'customer.subscription.deleted': {
      const subscription = event.data.object;
      const amount = subscription.items.data[0].price.unit_amount! / 100;

      await revenue.track({
        customer_id: subscription.customer as string,
        customer_subscription_id: subscription.id,
        amount: 0,
        currency: subscription.currency.toUpperCase(),
        transaction_id: subscription.id,
        payment_processor: 'stripe',
        event_type: 'churn',
        mrr_impact: -amount,
        metadata: {
          cancellation_reason: subscription.cancellation_details?.reason,
          feedback: subscription.cancellation_details?.feedback
        }
      });
      break;
    }

    case 'charge.refunded': {
      const charge = event.data.object;

      await revenue.track({
        customer_id: charge.customer as string,
        amount: -(charge.amount_refunded / 100),
        currency: charge.currency.toUpperCase(),
        transaction_id: `refund_${charge.id}`,
        payment_processor: 'stripe',
        event_type: 'refund',
        metadata: {
          original_charge: charge.id,
          reason: charge.refunds?.data[0]?.reason
        }
      });
      break;
    }
  }

  return Response.json({ received: true });
}

Paddle Webhook Integration#

// app/api/webhooks/paddle/route.ts
import { Revenue } from '@pulsora/revenue';
import crypto from 'crypto';

const revenue = new Revenue({ apiToken: process.env.PULSORA_API_TOKEN! });

function verifyPaddleWebhook(body: string, signature: string): boolean {
  const hash = crypto
    .createHmac('sha256', process.env.PADDLE_PUBLIC_KEY!)
    .update(body)
    .digest('hex');
  return hash === signature;
}

export async function POST(req: Request) {
  const body = await req.text();
  const signature = req.headers.get('paddle-signature')!;

  if (!verifyPaddleWebhook(body, signature)) {
    return Response.json({ error: 'Invalid signature' }, { status: 401 });
  }

  const event = JSON.parse(body);

  switch (event.event_type) {
    case 'subscription_payment_succeeded': {
      const isFirstPayment = event.subscription_payment_id === event.subscription_id;

      await revenue.track({
        visitor_fingerprint: event.passthrough?.visitor_fingerprint,
        session_id: event.passthrough?.session_id,
        customer_id: event.user_id,
        customer_subscription_id: event.subscription_id,
        amount: parseFloat(event.sale_gross),
        currency: event.currency,
        transaction_id: event.subscription_payment_id,
        payment_processor: 'paddle',
        event_type: isFirstPayment ? 'subscription' : 'renewal',
        mrr_impact: isFirstPayment ? parseFloat(event.sale_gross) : 0,
        is_recurring: true,
        metadata: {
          plan_id: event.subscription_plan_id,
          next_payment: event.next_payment_date
        }
      });
      break;
    }

    case 'subscription_cancelled': {
      await revenue.track({
        customer_id: event.user_id,
        customer_subscription_id: event.subscription_id,
        amount: 0,
        currency: event.currency,
        transaction_id: event.subscription_id,
        payment_processor: 'paddle',
        event_type: 'churn',
        mrr_impact: -parseFloat(event.cancellation_effective_date_mrr),
        metadata: {
          cancellation_reason: event.cancellation_reason
        }
      });
      break;
    }
  }

  return Response.json({ success: true });
}

Custom Payment Solution#

// For custom payment processors or manual invoicing
import { Revenue } from '@pulsora/revenue';

const revenue = new Revenue({ apiToken: process.env.PULSORA_API_TOKEN! });

// Track manual payment
async function recordManualPayment(payment) {
  await revenue.track({
    visitor_fingerprint: payment.visitor_fingerprint,
    session_id: payment.session_id,
    customer_id: payment.customer_id,
    amount: payment.amount,
    currency: payment.currency || 'USD',
    transaction_id: payment.invoice_number,
    payment_processor: 'manual',
    event_type: 'purchase',
    metadata: {
      invoice_number: payment.invoice_number,
      payment_method: payment.payment_method,
      notes: payment.notes
    }
  });
}

// Track enterprise deal
async function recordEnterpriseDeal(deal) {
  await revenue.track({
    customer_id: deal.company_id,
    amount: deal.annual_contract_value / 12,
    currency: 'USD',
    transaction_id: deal.contract_id,
    payment_processor: 'enterprise',
    event_type: 'subscription',
    customer_subscription_id: deal.contract_id,
    mrr_impact: deal.annual_contract_value / 12,
    is_recurring: true,
    metadata: {
      contract_term: '12 months',
      seats: deal.seats,
      acv: deal.annual_contract_value
    }
  });
}

Attribution Models Explained#

First-Touch Attribution#

Credits revenue to the first marketing touchpoint (e.g., the original Google ad).

Use case: Understanding which channels drive new customer acquisition.

Example: A user clicks a Facebook ad, browses your site, leaves, returns via Google search 3 days later, and purchases. First-touch attributes to Facebook.

Last-Touch Attribution#

Credits revenue to the last touchpoint before conversion (e.g., the final email click).

Use case: Understanding which channels close deals.

Example: Same journey as above, last-touch attributes to Google organic search.

Multi-Touch Attribution#

Credits revenue across all touchpoints in the journey.

Use case: Understanding the complete customer journey and channel interactions.

Example: Facebook ad gets 40% credit, email gets 20%, Google gets 40%.

Pulsora tracks all touchpoints automatically, allowing you to analyze revenue with any attribution model in the dashboard.

Troubleshooting#

Missing Attribution Data#

Symptoms:

  • Revenue tracked successfully but shows no attribution
  • "Unknown source" in dashboard reports

Cause: Visitor fingerprint or session ID not passed from frontend to webhook

Solutions:

  1. Verify fingerprint collection:
// Frontend checkout page
const fingerprint = await pulsora.getVisitorFingerprint();
const sessionId = pulsora.getSessionId();

console.log('Fingerprint:', fingerprint); // Should be ~40 char string
console.log('Session:', sessionId); // Should be UUID format

if (!fingerprint || !sessionId) {
  console.error('Missing tracking data!');
}
  1. Verify metadata is passed to payment processor:
// Stripe checkout - check metadata is set
const session = await stripe.checkout.sessions.create({
  // ...
  metadata: {
    visitor_fingerprint: fingerprint,
    session_id: sessionId,
  },
});

console.log('Metadata:', session.metadata);
  1. Verify metadata reaches webhook:
// In webhook handler
console.log('Received metadata:', event.data.object.metadata);

if (!event.data.object.metadata?.visitor_fingerprint) {
  console.error('Missing fingerprint in webhook!');
}

Revenue Not Appearing in Dashboard#

Symptoms:

  • Webhook fires successfully (200 response)
  • No errors in logs
  • Revenue not visible in Pulsora dashboard

Debugging checklist:

  1. Verify API token:
// ❌ Wrong - using public token (pub_)
const revenue = new Revenue({ apiToken: 'pub_xxx' });

// ✅ Correct - using secret token (sec_)
const revenue = new Revenue({ apiToken: 'sec_xxx' });
  1. Check customer_id format:
// Ensure customer_id is a string
await revenue.track({
  customer_id: String(customerId), // Convert to string
  // ...
});
  1. Enable debug mode:
const revenue = new Revenue({
  apiToken: process.env.PULSORA_API_TOKEN,
  debug: true, // See detailed logs
});

Duplicate Revenue Events#

Cause: Webhook fired multiple times or manual tracking duplicates

Prevention:

  1. Use idempotency via transaction_id:
// Pulsora automatically deduplicates based on transaction_id
await revenue.track({
  transaction_id: uniqueTransactionId, // Must be unique per event
  // ...
});
  1. Check webhook signature:
// Always verify webhooks are legitimate
try {
  const event = stripe.webhooks.constructEvent(body, sig, secret);
} catch (err) {
  return Response.json({ error: 'Invalid signature' }, { status: 400 });
}

TypeScript Type Errors#

Error: Property 'visitor_fingerprint' does not exist

Solution:

import type { RevenueEvent } from '@pulsora/revenue';

const event: RevenueEvent = {
  visitor_fingerprint: fingerprint,
  session_id: sessionId,
  customer_id: 'cus_123',
  amount: 99.99,
  currency: 'USD',
  transaction_id: 'tx_123',
  payment_processor: 'stripe',
  event_type: 'purchase',
};

await revenue.track(event);

MRR Calculation Issues#

Problem: MRR doesn't match expectations

Common mistakes:

  1. Wrong mrr_impact for renewals:
// ❌ Wrong - renewals shouldn't affect MRR
await revenue.track({
  event_type: 'renewal',
  mrr_impact: 29.99, // Don't set for renewals!
});

// ✅ Correct - only set mrr_impact for new subs, upgrades, downgrades, churn
await revenue.track({
  event_type: 'renewal',
  // No mrr_impact
});
  1. Annual subscriptions:
// For annual plans, divide by 12 for MRR
await revenue.track({
  amount: 299.99, // Annual payment
  mrr_impact: 299.99 / 12, // Monthly recurring
  event_type: 'subscription',
  metadata: { interval: 'yearly' },
});

Next Steps#