stripe-surcharge-element

Stop eating Stripe's card fees.

A drop-in Stripe Elements component that charges the card fee to the customer and keeps PayNow flat. You net the same on every sale.

Why

Stripe charges you 2 to 3 times more for card payments than for other methods like PayNow.

Stripe fees
MethodStripe feeYou keep on S$100
Card3.4% + S$0.50S$96.10
GrabPay2.6%S$97.40
PayNow1.3%S$98.70

How

We built a drop-in element on Stripe deferred Elements that automatically surcharges the card fee, the fee you would have paid, to the customer.

The element shows the customer exactly what each method costs: card adds the fee, PayNow stays flat. Your server sets the real amount at charge time, so no one games it.

npm install stripe-surcharge-element

Client: render the element in your checkout.

"use client";
import { SurchargePaymentElement } from "stripe-surcharge-element";
import "stripe-surcharge-element/styles.css";

export function Checkout() {
  return (
    <SurchargePaymentElement
      publishableKey={process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!}
      baseCents={10000}                 // your price, in cents
      currency="sgd"
      productId="fast-charger"          // server looks up the price by this
      confirmUrl="/api/pay/confirm"     // your route (below)
      feeConfig={{
        cardRatePct: 3.4,
        cardFixedCents: 50,
        paynowRatePct: 1.3,
        passOn: 1,                 // 0 = absorb, 1 = pass on fully
      }}
    />
  );
}

Server:one route creates the PaymentIntent and reads the true method from the confirmation token, so a card payer can't dodge the surcharge.

// app/api/pay/confirm/route.ts  (your server, your secret key)
import Stripe from "stripe";
import { amountForMethod } from "stripe-surcharge-element/fees";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const FEE_CONFIG = { cardRatePct: 3.4, cardFixedCents: 50, paynowRatePct: 1.3, passOn: 1 };

export async function POST(req: Request) {
  const { confirmationTokenId, productId } = await req.json();

  // Read the true method from the token. The client can't spoof it.
  const token = await stripe.confirmationTokens.retrieve(confirmationTokenId);
  const method = token.payment_method_preview.type;        // 'card' | 'paynow' | ...
  const amount = amountForMethod(getPrice(productId), method, FEE_CONFIG);

  const pi = await stripe.paymentIntents.create({
    amount, currency: "sgd", confirm: true,
    confirmation_token: confirmationTokenId,
    return_url: "https://you.com/return",
  });
  return Response.json({ status: pi.status, clientSecret: pi.client_secret, amountCents: amount });
}

The client only needs your publishable key, already there if you use Elements. Your secret key never leaves your server. We never see it.

Example

Move the surcharge dial, then toggle Card and PayNow.

Merchant (you) configures
Customer sees

Fast Charger

Fast ChargerS$100.00
Card processing fee+S$2.69
Total by cardS$102.69
TipSelect a non-card payment method to skip the card fee.