Stripe Integration Guide#

This guide shows you how to integrate Pulsora revenue tracking with Stripe payments, enabling you to attribute every transaction back to its original traffic source and marketing campaign.

Overview#

The integration process involves three steps:

  1. Collect tracking data during checkout
  2. Pass data to Stripe as metadata
  3. Track revenue in your webhook handler

Prerequisites#

  • Stripe account with webhook endpoint configured
  • Pulsora account with API token
  • Node.js backend (Express, Next.js, etc.)

Step 1: Frontend Setup#

First, collect the visitor's fingerprint and session ID during checkout:

// checkout.js
import { Pulsora } from '@pulsora/core';

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

async function createCheckoutSession(priceId) {
  // Get tracking data
  const fingerprint = await pulsora.getVisitorFingerprint();
  const sessionId = pulsora.getSessionId();

  // Send to your backend
  const response = await fetch('/api/create-checkout-session', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      priceId,
      visitor_fingerprint: fingerprint,
      session_id: sessionId,
    }),
  });

  const { sessionId: stripeSessionId } = await response.json();

  // Redirect to Stripe Checkout
  const stripe = await loadStripe('pk_live_...');
  await stripe.redirectToCheckout({ sessionId: stripeSessionId });
}

Step 2: Backend - Create Checkout Session#

Pass the tracking data to Stripe as metadata:

// api/create-checkout-session.js
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

export async function createCheckoutSession(req, res) {
  const { priceId, visitor_fingerprint, session_id } = req.body;

  try {
    const session = await stripe.checkout.sessions.create({
      payment_method_types: ['card'],
      line_items: [
        {
          price: priceId,
          quantity: 1,
        },
      ],
      mode: 'payment', // or 'subscription'
      success_url: `${process.env.APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${process.env.APP_URL}/cancel`,
      metadata: {
        // Store Pulsora tracking data
        visitor_fingerprint,
        session_id,
      },
    });

    res.json({ sessionId: session.id });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

Step 3: Webhook Handler#

Track revenue when payment succeeds:

// api/webhooks/stripe.js
import Stripe from 'stripe';
import { Revenue } from '@pulsora/revenue';

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

export async function handleStripeWebhook(req, res) {
  const sig = req.headers['stripe-signature'];

  let event;

  try {
    // Verify webhook signature
    event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET,
    );
  } catch (err) {
    console.error('Webhook signature verification failed');
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Handle different event types
  switch (event.type) {
    case 'checkout.session.completed':
      await handleCheckoutComplete(event.data.object);
      break;

    case 'invoice.payment_succeeded':
      await handleInvoicePayment(event.data.object);
      break;

    case 'customer.subscription.updated':
      await handleSubscriptionUpdate(event.data.object);
      break;

    case 'customer.subscription.deleted':
      await handleSubscriptionCancelled(event.data.object);
      break;

    case 'charge.refunded':
      await handleRefund(event.data.object);
      break;
  }

  res.json({ received: true });
}

async function handleCheckoutComplete(session) {
  // Get full session details with line items
  const fullSession = await stripe.checkout.sessions.retrieve(session.id, {
    expand: ['line_items'],
  });

  // Track revenue with Pulsora
  await revenue.track({
    visitor_fingerprint: fullSession.metadata.visitor_fingerprint,
    session_id: fullSession.metadata.session_id,
    customer_id: fullSession.customer,
    amount: fullSession.amount_total / 100, // Convert cents to dollars
    currency: fullSession.currency.toUpperCase(),
    transaction_id: fullSession.id,
    payment_processor: 'stripe',
    event_type:
      fullSession.mode === 'subscription' ? 'subscription' : 'purchase',
    metadata: {
      payment_intent: fullSession.payment_intent,
      products: fullSession.line_items.data.map((item) => ({
        name: item.description,
        amount: item.amount_total / 100,
      })),
    },
  });
}

Subscription Tracking#

For subscription events, track MRR changes:

async function handleSubscriptionUpdate(subscription) {
  const customer = await stripe.customers.retrieve(subscription.customer);

  // Calculate MRR impact
  const newMRR = calculateMRR(subscription);
  const oldMRR = subscription.previous_attributes?.items
    ? calculateMRR(subscription.previous_attributes)
    : 0;
  const mrrImpact = newMRR - oldMRR;

  // Determine event type
  let eventType = 'subscription';
  if (oldMRR > 0 && newMRR > oldMRR) {
    eventType = 'upgrade';
  } else if (oldMRR > 0 && newMRR < oldMRR) {
    eventType = 'downgrade';
  }

  await revenue.track({
    visitor_fingerprint: customer.metadata.visitor_fingerprint,
    session_id: customer.metadata.session_id,
    customer_id: subscription.customer,
    amount: newMRR,
    currency: subscription.currency.toUpperCase(),
    transaction_id: subscription.id,
    payment_processor: 'stripe',
    event_type: eventType,
    customer_subscription_id: subscription.id,
    mrr_impact: mrrImpact,
    is_recurring: true,
    metadata: {
      status: subscription.status,
      interval: subscription.items.data[0].price.recurring.interval,
      products: subscription.items.data.map((item) => ({
        product: item.price.product,
        amount: item.price.unit_amount / 100,
      })),
    },
  });
}

function calculateMRR(subscription) {
  let total = 0;

  subscription.items.data.forEach((item) => {
    const amount = item.price.unit_amount / 100;
    const interval = item.price.recurring.interval;

    // Normalize to monthly
    if (interval === 'year') {
      total += amount / 12;
    } else if (interval === 'month') {
      total += amount;
    }
  });

  return total;
}

Handling Refunds#

Track refunds as negative revenue:

async function handleRefund(charge) {
  const refundAmount = charge.amount_refunded / 100;

  if (refundAmount > 0) {
    await revenue.track({
      visitor_fingerprint: charge.metadata.visitor_fingerprint,
      session_id: charge.metadata.session_id,
      customer_id: charge.customer,
      amount: -refundAmount, // Negative amount
      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 || 'unknown',
        partial: charge.amount_refunded < charge.amount,
      },
    });
  }
}

Customer Portal Integration#

For existing customers using the billing portal:

// Store tracking data on customer creation
async function createCustomer(email, visitorFingerprint, sessionId) {
  const customer = await stripe.customers.create({
    email,
    metadata: {
      visitor_fingerprint: visitorFingerprint,
      session_id: sessionId,
      first_seen: new Date().toISOString(),
    },
  });

  return customer;
}

// Update tracking data on new sessions
async function updateCustomerTracking(
  customerId,
  visitorFingerprint,
  sessionId,
) {
  await stripe.customers.update(customerId, {
    metadata: {
      visitor_fingerprint: visitorFingerprint,
      session_id: sessionId,
      last_seen: new Date().toISOString(),
    },
  });
}

Testing Your Integration#

1. Use Stripe Test Mode#

Start with Stripe's test mode and test cards:

const stripe = new Stripe(
  process.env.NODE_ENV === 'production'
    ? process.env.STRIPE_SECRET_KEY
    : process.env.STRIPE_TEST_SECRET_KEY,
);

2. Test Webhook Locally#

Use Stripe CLI to forward webhooks to your local server:

stripe listen --forward-to localhost:3000/api/webhooks/stripe

3. Verify Attribution#

Check your Pulsora dashboard to ensure:

  • Revenue events appear in real-time
  • Attribution shows correct traffic sources
  • Customer journeys are properly linked

Common Issues#

Missing Attribution Data#

If revenue events show "Direct" traffic:

  • Ensure fingerprint and session_id are passed correctly
  • Check that metadata isn't stripped by your payment flow
  • Verify the visitor had pageviews before payment

Duplicate Events#

Prevent duplicate revenue tracking:

// Store processed event IDs
const processedEvents = new Set();

async function handleWebhook(event) {
  // Skip if already processed
  if (processedEvents.has(event.id)) {
    return;
  }

  // Process event
  await trackRevenue(event);

  // Mark as processed
  processedEvents.add(event.id);

  // Store in database for persistence
  await db.processedEvents.create({
    event_id: event.id,
    processed_at: new Date(),
  });
}

Currency Conversion#

Always track revenue in your account currency:

// Convert to account currency if needed
const amount = await convertCurrency(
  session.amount_total / 100,
  session.currency,
  'USD', // Your account currency
);

await revenue.track({
  amount,
  currency: 'USD',
  // ... other fields
});

Security Considerations#

1. Validate Webhook Signatures#

Always verify webhooks are from Stripe:

try {
  event = stripe.webhooks.constructEvent(
    req.body,
    sig,
    process.env.STRIPE_WEBHOOK_SECRET,
  );
} catch (err) {
  return res.status(400).send('Invalid signature');
}

2. Use Environment Variables#

Never hardcode sensitive keys:

// .env
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
PULSORA_API_TOKEN=sec_...

3. Implement Rate Limiting#

Protect your webhook endpoint:

import rateLimit from 'express-rate-limit';

const webhookLimiter = rateLimit({
  windowMs: 1 * 60 * 1000, // 1 minute
  max: 100, // Max 100 requests per minute
});

app.post('/api/webhooks/stripe', webhookLimiter, handleStripeWebhook);

Next Steps#

  • Test your integration thoroughly
  • Set up monitoring for failed webhooks
  • Configure retry logic for resilience
  • Review Stripe's webhook best practices

For more payment integrations, see: