Payment Gateways
By the end of this lesson you'll be able to take a real card payment with Stripe — creating the charge on the server, keeping card data entirely out of your code, and using a verified webhook to fulfil the order reliably.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free Php course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
1️⃣ The Flow: You Never Touch the Card
A payment gateway like Stripe is the service that actually moves money between a customer's card and your bank. The golden rule: raw card numbers must never reach your server. If they did, you'd fall under the full weight of PCI DSS (the card industry's security standard) — months of audits you do not want. Instead, the card details go straight from the customer's browser to Stripe via Stripe.js / Elements (a hosted card form) or Stripe's hosted Checkout page. Stripe hands you back a harmless token , and your server works with that.
Two objects drive every payment. A PaymentIntent is Stripe's record of one payment as it moves through its states (needs action → processing → succeeded). A Checkout Session is a higher-level wrapper that builds Stripe's entire hosted pay-page for you and creates a PaymentIntent for you behind the scenes. Start with Checkout — it's the least code and the most secure.
2️⃣ Creating the Charge on the Server
Your PHP backend creates the session with the price you decide and a couple of redirect URLs, then sends the customer to Stripe. Notice the price lives in server code, and amounts are in the smallest currency unit — cents for USD, so 4900 means $49.00. Your secret key is read from an environment variable, never written in the file.
In a live app the last step is a redirect — header("Location: " . $session->url, true, 303); exit; — sending the browser to Stripe's secure page where the customer enters their card. PayPal works the same way conceptually: you create an order server-side via its REST SDK, redirect the buyer to PayPal to approve, then capture the order on return. The provider differs; the "never touch the card, confirm server-side" shape does not.
3️⃣ Webhooks: Fulfil Only When Money Clears
Never treat the success_url redirect as proof of payment — the customer can close the tab or lose signal. A webhook is a server-to-server message Stripe POSTs to a URL you own after the payment settles, and it's the authoritative signal to fulfil the order (grant access, ship, email a receipt). Because anyone on the internet can POST to that URL, you must verify the signature : Webhook::constructEvent() checks the Stripe-Signature header against your webhook secret and throws if it's forged.
Return 200 quickly so Stripe stops retrying; if a webhook handler is slow, kick the heavy work to a queued job and return 200 right away. And store only the minimum : the Stripe ids ( cs_… , pi_… ), an order status, and maybe the last 4 digits Stripe gives you — never the full card number, CVC, or expiry.
4️⃣ Idempotency & Refunds
Networks retry. Without protection, a retried "create charge" or "refund" could run twice and bill the customer twice. An idempotency key — any unique string you attach to the request — fixes this: send the same key again and Stripe performs the action once and returns the original result. Refunds are a normal server-side call; refund the full amount or a partial one by passing amount in cents.
5️⃣ Your Turn
Now you try. The first script is almost complete — fill in each ___ using the 👉 hint, then run it and check it against the Output panel. This one drills the rule that mattered most: set the price on the server, in cents.
One more. Here the webhook must reject a forged request with the right HTTP status. Fill in the blank so a bad signature returns a "Bad Request".
📋 Quick Reference — Stripe Payments
No code is filled in this time — just a brief and an outline. Write it yourself, run it in your own project against a test charge, then check your result against the expected output in the comments. This is the write-run-check loop you'll use on every real integration.
Practice quiz
In a Stripe integration, where must the raw card number be handled?
- On your PHP server, validated before charging
- Stored encrypted in your database
- Browser to Stripe directly — it must never reach your server
- Sent to Stripe by your webhook
Answer: Browser to Stripe directly — it must never reach your server. Card data goes straight from the browser to Stripe (via Stripe.js/Elements or hosted Checkout); keeping it off your server is what stays out of full PCI scope.
Why must the charge amount be set on the server, not read from the browser?
- The browser is under the customer's control and the amount can be tampered with
- The browser cannot send numbers
- Stripe rejects amounts from the browser
- It is faster to compute on the server
Answer: The browser is under the customer's control and the amount can be tampered with. Anyone can edit a hidden field from $49.00 to $0.01, so the price must come from server code, ideally looked up by product id.
In Stripe, the unit_amount 4900 for USD represents what price?
- $4,900.00
- $490.00
- $4.90
- $49.00
Answer: $49.00. Stripe amounts are in the smallest currency unit — cents for USD — so 4900 means $49.00.
What is the authoritative signal that a payment actually cleared?
- The customer reaching the success_url page
- The checkout.session.completed webhook from Stripe
- The browser console showing 200 OK
- The session being created
Answer: The checkout.session.completed webhook from Stripe. The success_url redirect is best-effort UX; the webhook is the reliable, authoritative signal that the money cleared, so fulfilment belongs there.
Which function verifies that a webhook event genuinely came from Stripe?
- Webhook::constructEvent()
- json_decode()
- hash_equals()
- curl_exec()
Answer: Webhook::constructEvent(). Webhook::constructEvent() checks the Stripe-Signature header against your webhook secret and throws if the request was forged.
What does an idempotency key do on a money-moving request?
- Encrypts the request body
- Speeds up the API call
- Ensures a retried request runs the action only once and returns the original result
- Authenticates the customer
Answer: Ensures a retried request runs the action only once and returns the original result. Send the same idempotency key twice and Stripe performs the action once, so a flaky network can't double-charge or double-refund.
Which HTTP status should a webhook return for a forged/invalid signature?
- 200
- 400
- 301
- 500
Answer: 400. Reject a forged or tampered webhook with http_response_code(400) — Bad Request — and do not fulfil the order.
Why return 200 quickly from a webhook handler?
- So the customer sees the success page
- It is required to issue refunds
- To verify the signature
- So Stripe stops retrying the event
Answer: So Stripe stops retrying the event. Returning 200 tells Stripe the event was received so it stops retrying; push slow work to a queued job and return 200 right away.
Which key belongs ONLY on the server and must never reach the browser or git?
- The publishable key (pk_...)
- The secret key (sk_...)
- The session id (cs_...)
- The PaymentIntent id (pi_...)
Answer: The secret key (sk_...). Load sk_... from a git-ignored env file with getenv(); only the publishable pk_... key belongs in front-end code.
What is the relationship between a Checkout Session and a PaymentIntent?
- They are unrelated objects
- A PaymentIntent wraps many Checkout Sessions
- A Checkout Session is a higher-level wrapper that builds the hosted page and creates a PaymentIntent for you
- A Checkout Session replaces refunds
Answer: A Checkout Session is a higher-level wrapper that builds the hosted page and creates a PaymentIntent for you. A Checkout Session builds Stripe's hosted pay page and creates a PaymentIntent (which tracks one payment's lifecycle) behind the scenes.