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.