How to Accept Payments in a Browser Extension: Technical Guide

Adding payment processing to your browser extension might seem daunting, but with the right approach and tools, it’s straightforward. This technical guide walks you through implementing payments from scratch.

Architecture Overview

A typical payment flow for browser extensions involves four components:

  1. Extension - Detects premium feature access, initiates payment flow
  2. Payment service - Handles checkout UI, processes payment securely
  3. Backend API - Verifies payment status, grants access rights
  4. Database - Stores payment records, subscription status, customer data

Why You Need a Backend

Browser extensions run in users’ browsers, so you cannot:

  • Store API keys securely (any user can inspect your code)
  • Directly call payment APIs without exposing credentials
  • Trust client-side payment verification (users could fake it)

A backend server is essential for secure payment processing.

Prerequisites

Before implementing payments, you’ll need:

  • Payment processor account - Stripe (recommended), PayPal, or Paddle
  • Backend server - Node.js, Python, Go, or serverless (AWS Lambda, Cloudflare Workers)
  • Database - PostgreSQL, MySQL, or MongoDB for payment records
  • HTTPS domain - Required for webhook endpoints
  • Extension manifest - Version 3 recommended for modern browsers

Step-by-Step Implementation

Step 1: Set Up Stripe Account

  1. Create a Stripe account
  2. Get your API keys from Dashboard → Developers → API keys
  3. Note your Secret key (starts with sk_test_ or sk_live_)
  4. Note your Publishable key (starts with pk_test_ or pk_live_)

Security: Never expose your secret key in your extension code. Only use it on your backend.

Step 2: Create Backend API

Here’s a minimal Express.js backend for handling payments:

// server.js
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const app = express();

app.use(express.json());

// Create checkout session
app.post('/api/create-checkout', async (req, res) => {
  try {
    const { userId, priceId } = req.body;

    const session = await stripe.checkout.sessions.create({
      mode: 'subscription', // or 'payment' for one-time
      customer_email: req.body.email,
      client_reference_id: userId,
      line_items: [
        {
          price: priceId, // Your Stripe price ID
          quantity: 1,
        },
      ],
      success_url: `https://yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `https://yourdomain.com/cancel`,
    });

    res.json({ url: session.url });
  } catch (error) {
    console.error('Checkout error:', error);
    res.status(500).json({ error: error.message });
  }
});

// Check user's subscription status
app.get('/api/subscription-status/:userId', async (req, res) => {
  try {
    const { userId } = req.params;

    // Look up user in your database
    const user = await db.users.findOne({ id: userId });

    if (!user || !user.stripeSubscriptionId) {
      return res.json({ active: false });
    }

    // Verify with Stripe
    const subscription = await stripe.subscriptions.retrieve(
      user.stripeSubscriptionId
    );

    res.json({
      active: subscription.status === 'active',
      currentPeriodEnd: subscription.current_period_end,
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(3000);

Step 3: Handle Stripe Webhooks

Webhooks notify your server when payments succeed, fail, or subscriptions change:

// Add to server.js
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;

app.post('/webhooks/stripe', express.raw({type: 'application/json'}), async (req, res) => {
  const sig = req.headers['stripe-signature'];

  let event;

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

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

      // Save payment to database
      await db.payments.create({
        userId: session.client_reference_id,
        stripeSessionId: session.id,
        amount: session.amount_total,
        status: 'completed',
        createdAt: new Date(),
      });

      // If subscription, save subscription ID
      if (session.mode === 'subscription') {
        await db.users.update(
          { id: session.client_reference_id },
          { stripeSubscriptionId: session.subscription }
        );
      }
      break;

    case 'customer.subscription.deleted':
      const subscription = event.data.object;

      // Mark subscription as canceled
      await db.users.update(
        { stripeSubscriptionId: subscription.id },
        { stripeSubscriptionId: null, subscriptionStatus: 'canceled' }
      );
      break;

    case 'invoice.payment_failed':
      const invoice = event.data.object;

      // Notify user of failed payment
      console.log('Payment failed for:', invoice.customer);
      break;
  }

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

Configure webhook endpoint in Stripe:

  1. Dashboard → Developers → Webhooks
  2. Add endpoint: https://yourdomain.com/webhooks/stripe
  3. Select events: checkout.session.completed, customer.subscription.deleted, invoice.payment_failed
  4. Copy webhook signing secret to your environment variables

Step 4: Extension Implementation

Now integrate payments into your extension:

// background.js (Manifest V3 service worker)

// When user clicks "Upgrade" button
chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
  if (request.action === 'startCheckout') {
    try {
      // Get current user ID (from your auth system)
      const userId = await getCurrentUserId();

      // Create checkout session via your API
      const response = await fetch('https://yourapi.com/api/create-checkout', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          userId: userId,
          email: request.email,
          priceId: 'price_xxxxx', // Your Stripe price ID
        }),
      });

      const { url } = await response.json();

      // Open Stripe checkout in new tab
      chrome.tabs.create({ url });

    } catch (error) {
      console.error('Checkout error:', error);
    }
  }
});

// Check subscription status before allowing premium features
async function isPremiumUser() {
  try {
    const userId = await getCurrentUserId();

    const response = await fetch(
      `https://yourapi.com/api/subscription-status/${userId}`
    );

    const { active } = await response.json();

    return active;
  } catch (error) {
    console.error('Status check error:', error);
    return false;
  }
}

// Example: Gate premium feature
chrome.action.onClicked.addListener(async () => {
  const isPremium = await isPremiumUser();

  if (isPremium) {
    // Execute premium feature
    executePremiumFeature();
  } else {
    // Show upgrade prompt
    chrome.tabs.create({ url: 'upgrade.html' });
  }
});
<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Extension Premium</title>
  <style>
    body { padding: 20px; width: 300px; font-family: Arial; }
    .premium-box { background: #f8f9fa; padding: 15px; border-radius: 8px; }
    .btn { background: #0066cc; color: white; border: none; padding: 10px 20px;
           border-radius: 5px; cursor: pointer; width: 100%; }
    .btn:hover { background: #0052a3; }
  </style>
</head>
<body>
  <div class="premium-box">
    <h2>Upgrade to Premium</h2>
    <p>Unlock all features:</p>
    <ul>
      <li>Unlimited usage</li>
      <li>Advanced analytics</li>
      <li>Priority support</li>
    </ul>
    <button class="btn" id="upgradeBtn">Upgrade - $9.99/month</button>
  </div>

  <script src="popup.js"></script>
</body>
</html>
// popup.js
document.getElementById('upgradeBtn').addEventListener('click', async () => {
  // Send message to background script to start checkout
  chrome.runtime.sendMessage({
    action: 'startCheckout',
    email: 'user@example.com', // Get from user input or auth
  });
});

// Check and display current status
async function checkStatus() {
  const response = await chrome.runtime.sendMessage({ action: 'checkStatus' });

  if (response.isPremium) {
    document.body.innerHTML = '<h2>✓ Premium Active</h2>';
  }
}

checkStatus();

Step 5: Success Page

Create a page to handle post-checkout redirects:

<!-- success.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Payment Successful</title>
  <style>
    body { font-family: Arial; text-align: center; padding: 50px; }
    .success { color: #28a745; font-size: 48px; }
  </style>
</head>
<body>
  <div class="success">✓</div>
  <h1>Payment Successful!</h1>
  <p>Your premium features are now active.</p>
  <p>You can close this tab and return to the extension.</p>

  <script>
    // Notify extension of successful payment
    const urlParams = new URLSearchParams(window.location.search);
    const sessionId = urlParams.get('session_id');

    if (sessionId) {
      // Send message to extension
      chrome.runtime.sendMessage({
        action: 'paymentComplete',
        sessionId: sessionId
      });
    }

    // Auto-close after 3 seconds
    setTimeout(() => window.close(), 3000);
  </script>
</body>
</html>

Security Best Practices

1. Never Store Card Data

Let Stripe handle all payment details. Never store:

  • Card numbers
  • CVV codes
  • Expiration dates

2. Validate Webhook Signatures

Always verify webhook signatures to prevent fake events:

stripe.webhooks.constructEvent(req.body, sig, endpointSecret);

3. Use API Keys Securely

Backend only:

  • Store secret keys in environment variables
  • Never commit keys to Git
  • Rotate keys if exposed

Extension:

  • Only use public/publishable keys if necessary
  • Prefer making all calls through your backend

4. Implement Idempotency

Prevent duplicate charges if requests retry:

const session = await stripe.checkout.sessions.create({
  // ... other params
}, {
  idempotencyKey: `checkout-${userId}-${timestamp}`
});

5. Rate Limiting

Protect your API from abuse:

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});

app.use('/api/', limiter);

Testing

Use Stripe Test Mode

During development:

  1. Use test API keys (sk_test_...)
  2. Test with Stripe test cards:
    • Success: 4242 4242 4242 4242
    • Declined: 4000 0000 0000 0002
    • 3D Secure: 4000 0025 0000 3155

Test Webhooks Locally

Use Stripe CLI to forward webhooks:

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

Common Test Scenarios

  • ✓ Successful payment
  • ✓ Declined payment
  • ✓ Subscription creation
  • ✓ Subscription cancellation
  • ✓ Payment failure (expired card)
  • ✓ Webhook retry

Production Checklist

Before going live:

  • Switch to live Stripe keys (sk_live_...)
  • Configure production webhook endpoint
  • Test complete payment flow end-to-end
  • Verify webhook handling in production
  • Implement error monitoring (Sentry, Rollbar)
  • Set up email notifications for failed payments
  • Create customer support process
  • Write refund policy
  • Test from different countries (VAT/tax)
  • Verify HTTPS everywhere
  • Set up database backups

Common Issues & Solutions

Issue: Webhooks Not Received

Causes:

  • Firewall blocking Stripe IPs
  • Wrong endpoint URL
  • Server not responding quickly enough

Solutions:

  • Verify endpoint is publicly accessible
  • Return 200 status immediately
  • Process webhook asynchronously

Issue: CORS Errors

Solution:

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'chrome-extension://YOUR_EXTENSION_ID');
  res.header('Access-Control-Allow-Methods', 'GET,POST');
  next();
});

Issue: User Status Not Updating

Causes:

  • Webhook delay
  • Database not syncing

Solutions:

  • Implement polling for recent payments
  • Add manual refresh button
  • Show “Processing…” state

Alternative: Using GateVector

Instead of building everything from scratch, you can use GateVector, which provides:

  • ✓ Ready-to-use payment API
  • ✓ Automatic webhook handling
  • ✓ Subscription management
  • ✓ Customer portal
  • ✓ Analytics dashboard
  • ✓ Flat monthly pricing (no per-transaction fees beyond Stripe)

Implementation with GateVector:

// Extension code
const response = await fetch('https://api.gatevector.com/v1/payments/session', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    userId: userId,
    priceId: 'your_stripe_price_id',
    successUrl: 'https://yourdomain.com/success',
    cancelUrl: 'https://yourdomain.com/cancel',
  }),
});

const { checkoutUrl } = await response.json();
chrome.tabs.create({ url: checkoutUrl });

Much simpler - no backend needed!

Next Steps

  1. Set up Stripe account and get API keys
  2. Create a backend API for checkout sessions
  3. Implement webhook handling for payment events
  4. Build success/cancel pages for post-checkout
  5. Add premium checks to your extension
  6. Test thoroughly with Stripe test mode
  7. Deploy to production with monitoring

Building payments into your extension takes effort, but it enables you to build a sustainable business around your work. Start simple, test thoroughly, and iterate based on user feedback.


Looking for a simpler solution? GateVector handles all the complexity of extension payments so you can focus on building features.