The closest thing Stripe has to an online tip is a price the buyer fills in. Payment Links call it "customers choose what to pay"; in the API it is custom_unit_amount:
curl https://api.stripe.com/v1/prices \
-u "sk_test_...:" \
-d currency=usd \
-d "custom_unit_amount[enabled]=true" \
-d "custom_unit_amount[preset]=500" \
-d product=prod_...A Checkout Session or Payment Link built on that price takes nothing else. Stripe's docs list the limits: no other line items, a quantity of 1, no promotion codes or discounts, and no recurring payments or optional items. That makes it a tip jar, where the tip is the whole payment. It cannot put a tip on top of a $120.00 service in the same checkout.
Which path applies to you
| What you have | Your path |
|---|---|
| A Checkout Session your server creates | Ask on your page, send the tip as a line item |
| A Payment Link, with no server code | Fixed tip add-ons with optional items |
| The Payment Element on PaymentIntents | Your own tip selector |
| The buyer wants to tip after paying | A tip jar link on your confirmation page |
| A card reader, in person | Terminal on-reader tipping |
| You want percent buttons and a custom amount on the hosted payment page | Tipping on Flint |
Where Stripe supports tipping today
Stripe documents two tipping features, and both belong to Terminal:
- On-reader tipping. The card reader shows three suggestions before the customer presents a card: percentages, fixed amounts, or smart tips that switch between the two at a threshold you set. The customer can also enter a custom tip or leave none. It runs on the Stripe Reader S700 and S710, the BBPOS WisePOS E, and the BBPOS WisePad 3, and the tip comes back on the PaymentIntent in
amount_details.tip. - On-receipt tipping. The tip is written on a paper receipt and added when you capture the payment. It needs manual capture and is available in the US only.
Neither reaches a browser. Online, the hosted payment page offers quantity controls, optional items, promotion codes, and custom fields, and no tip prompt. The four workarounds below build one out of those parts or out of your own UI.
Checkout Sessions: ask on your page, send the tip as a line item
Your cart page shows the tip buttons, works out each percentage from your subtotal, and posts the buyer's choice to your server. The server validates the amount and appends one more line item when it creates the session:
// The buyer chose a tip on your cart page. Recompute it on the server
// from your own subtotal; never trust an amount from the browser.
const tipAmount = clampTip(req.body.tip_amount, cartSubtotal);
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [
...cartLineItems,
...(tipAmount > 0
? [
{
quantity: 1,
price_data: {
currency: "usd",
unit_amount: tipAmount,
// A Product named "Tip" with tax_code txcd_00000000
product: process.env.STRIPE_TIP_PRODUCT_ID,
},
},
]
: []),
],
payment_intent_data: { metadata: { tip_amount: String(tipAmount) } },
success_url: "https://example.com/thanks?session_id={CHECKOUT_SESSION_ID}",
cancel_url: "https://example.com/cart/tip",
});The buyer sees "Tip" as a product row on Stripe's page. Use a real Product for it rather than inline product_data: a Product can carry the Nontaxable tax code, and coupons can be scoped away from it. Both matter in the traps below.
Payment Links: fixed tip add-ons with optional items
Optional items are Stripe's upsell feature: up to 10 extra products the buyer can add on the payment page, on Checkout Sessions and Payment Links alike. A $1.00 "Tip" price with an adjustable quantity works as a tip stepper with no server code:
# price_tip_100 is a one-time $1.00 price on a Product named "Tip"
curl https://api.stripe.com/v1/payment_links \
-u "sk_test_...:" \
-d "line_items[0][price]=price_..." \
-d "line_items[0][quantity]=1" \
-d "optional_items[0][price]=price_tip_100" \
-d "optional_items[0][quantity]=5" \
-d "optional_items[0][adjustable_quantity][enabled]=true" \
-d "optional_items[0][adjustable_quantity][minimum]=0" \
-d "optional_items[0][adjustable_quantity][maximum]=50"The buyer adds the item and sets how many dollars to tip. There are no percentages and no typed amount, the control reads as a product add-on rather than a tip prompt, and the buyer can always remove it. Optional items cannot be combined with pay-what-you-want prices, so a free-form tip field is still out of reach on the link.
Payment Element: your own tip selector
With the Payment Element you own the page, so the tip selector is yours to build. When the buyer picks a tip, update the PaymentIntent on your server and record the tip where you can find it later:
// POST /cart/tip: the buyer picked a tip in your own selector
const subtotal = await cartSubtotal(cartId); // from your database
const tipAmount = clampTip(req.body.tip_amount, subtotal);
await stripe.paymentIntents.update(paymentIntentId, {
amount: subtotal + tipAmount,
metadata: { tip_amount: String(tipAmount) },
});Then refresh the Payment Element so the total it shows matches:
await fetch("/cart/tip", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ tip_amount: tipAmount }),
});
// Pull the new amount into the Payment Element and the wallet sheets
await elements.fetchUpdates();Stripe sees one amount. The split between sale and tip exists only in the metadata you wrote and in your own database.
After payment: a tip jar link on your confirmation page
A tip cannot be added to an online payment that has already succeeded, so a later tip is a second payment. Create a pay-what-you-want Payment Link for tips, then send buyers from the purchase to a page of yours that links to it:
curl https://api.stripe.com/v1/payment_links \
-u "sk_test_...:" \
-d "line_items[0][price]=price_..." \
-d "line_items[0][quantity]=1" \
-d "after_completion[type]=redirect" \
--data-urlencode "after_completion[redirect][url]=https://example.com/thanks"Append ?prefilled_amount=500 to the tip link to suggest $5.00. The buyer enters payment details a second time, the tip arrives as a separate payment with no reference to the order it thanks, and matching the two is your job.

