How We Verify Every Stripe Payment Server-Side
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.