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:
- Collect tracking data during checkout
- Pass data to Stripe as metadata
- 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: