ChargeForms
All posts
Engineering

How We Verify Every Stripe Payment Server-Side

ChargeForms Team·2026-07-01·2 min read

Accepting payments through a form sounds simple until you think about everything a malicious visitor could try to get past it. Here's the actual chain of checks ChargeForms runs before a paid entry is ever created — not as a marketing claim, but as a walkthrough of the real logic.

Problem 1: the client lies about the total

A visitor's browser is not a trusted source of truth. If a form has a $25 Payment Item and a Custom Amount donation field, nothing stops a modified request from claiming the Payment Item costs $0.01.

The fix: the charge amount is always recalculated server-side from the form's own field configuration — Stripe_Gateway::calculate_total() reads the form's actual field definitions, not anything submitted in the request. A Custom Amount below its configured minimum is floored to that minimum, server-side, regardless of what was sent.

Problem 2: the PaymentIntent might not actually be for this form

Even with a correct amount, what stops someone from creating a legitimate $5 PaymentIntent on Form A, then submitting Form B (which requires a payment) with that same intent ID?

The fix: every PaymentIntent's metadata includes the form ID it was created for. Before accepting it, we check that metadata matches the form actually being submitted — an intent minted for a different form is rejected outright.

Problem 3: the payment might already have been used

A successful, correctly-priced, correctly-scoped payment can still be replayed — resubmit the same request twice, get two free entries for one payment.

The fix: before accepting a PaymentIntent, we check whether its ID has already been attached to a previous entry. If it has, the second submission is rejected — the same payment can never produce two entries.

The result

An entry backed by a payment field is never created unless all three checks pass: correct server-recalculated amount, correct form scope, and not previously used. Any one failing is a hard stop, not a warning.