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.
| Method | Stripe fee | You keep on S$100 |
|---|---|---|
| Card | 3.4% + S$0.50 | S$96.10 |
| GrabPay | 2.6% | S$97.40 |
| PayNow | 1.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-elementClient: 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.
Fast Charger
Payment method