<coded>


PHP Payment Integration (Stripe + PayPal)

September 16, 2025

Looking for a copy-paste PHP payment integration that supports Stripe Checkout and PayPal Checkout with webhook verification and secure server-side code?
This guide gives you a minimal, production-ready baseline you can drop into any PHP project.


What We’ll Build

You’ll replace the test API keys, domain URLs, and product info with your real data.


Prerequisites


1. Project Setup

Folder structure:

/payments-demo
/public
index.html
stripe-create-checkout.php
stripe-webhook.php
paypal-create-order.php
paypal-capture-order.php
paypal-webhook.php
composer.json
.env

Install SDKs:

composer require stripe/stripe-php paypal/paypal-checkout-sdk vlucas/phpdotenv

.env (create in project root):

# Stripe
STRIPE_SECRET_KEY=sk_test_XXXXXXXXXXXXXXXXXXXXXXXX
STRIPE_WEBHOOK_SECRET=whsec_XXXXXXXXXXXXXXXXXXXXXX

# PayPal (use Sandbox first)
PAYPAL_ENV=sandbox
PAYPAL_CLIENT_ID=AbcYourClientId
PAYPAL_CLIENT_SECRET=YourSecret

# App config
APP_BASE_URL=https://your-domain.com  # no trailing slash
CURRENCY=eur
PRICE_UNIT=1900                       # €19.00 -> amount in cents
PRODUCT_NAME=Pro Plan

composer.json (minimal example):

{
  "require": {
    "stripe/stripe-php": "^15.0",
    "paypal/paypal-checkout-sdk": "^2.0",
    "vlucas/phpdotenv": "^5.6"
  },
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  }
}

(No src/ code needed for this tutorial, but PSR-4 is handy if you extend later.)


Bootstrap env in each PHP entry file (top of every PHP file below):

<?php
require __DIR__ . "/../vendor/autoload.php";
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . "/..");
$dotenv->safeLoad();
header("Content-Type: application/json; charset=utf-8");
?>

2. Frontend — public/index.html

Simple checkout page with two buttons. Stripe uses a server endpoint to create a Checkout Session and redirects; PayPal uses server endpoints for order create/capture, with a tiny JS helper.

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Checkout — Stripe & PayPal (PHP)</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Inter,Arial,sans-serif;padding:40px;max-width:680px;margin:auto}
        .card{border:1px solid #e5e7eb;border-radius:12px;padding:24px;margin:12px 0}
        .btn{display:inline-block;padding:12px 16px;border-radius:10px;border:1px solid #111;cursor:pointer;text-decoration:none}
        .btn + .btn{margin-left:8px}
        .row{display:flex;gap:12px;align-items:center}
        .muted{color:#6b7280}
    </style>
</head>
<body>
    <h1>Checkout</h1>
    <p class="muted">Demo: Pro Plan — <strong>€19.00</strong></p>

    <div class="card">
        <h2>Pay with Stripe</h2>
        <p>Uses Stripe Checkout Session with server-side creation & webhook verification.</p>
        <button class="btn" id="pay-stripe">Pay €19.00 with Stripe</button>
    </div>

    <div class="card">
        <h2>Pay with PayPal</h2>
        <p>Server-created PayPal Order + server capture (no client secrets exposed).</p>
        <button class="btn" id="pay-paypal">Pay €19.00 with PayPal</button>
    </div>

    <div class="card">
        <h3>Result</h3>
        <pre id="log" class="muted">Awaiting action…</pre>
    </div>

    <script>
        const log = (msg) => document.getElementById("log").textContent = msg;

        document.getElementById("pay-stripe").addEventListener("click", async () => {
            log("Creating Stripe Checkout Session…");
            const res = await fetch("./stripe-create-checkout.php", { method: "POST" });
            const data = await res.json();
            if (data.url) {
                window.location.href = data.url;
            } else {
                log("Stripe error: " + JSON.stringify(data));
            }
        });

        document.getElementById("pay-paypal").addEventListener("click", async () => {
            log("Creating PayPal order…");
            const res = await fetch("./paypal-create-order.php", { method: "POST" });
            const data = await res.json();

            if (data.approve_url && data.order_id) {
                log("Redirecting to PayPal approval…");
                // After buyer approves, PayPal redirects back to your RETURN_URL
                window.location.href = data.approve_url;
            } else {
                log("PayPal error: " + JSON.stringify(data));
            }
        });
    </script>
</body>
</html>

For Stripe: set your success/cancel URLs in stripe-create-checkout.php. For PayPal: set your RETURN_URL to a page that calls paypal-capture-order.php?order_id=....


3. Stripe — public/stripe-create-checkout.php

Creates a Checkout Session server-side and returns the redirect URL.

<?php
require __DIR__ . "/../vendor/autoload.php";
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . "/..");
$dotenv->safeLoad();
header("Content-Type: application/json; charset=utf-8");

\Stripe\Stripe::setApiKey($_ENV["STRIPE_SECRET_KEY"] ?? "");

$baseUrl = rtrim($_ENV["APP_BASE_URL"] ?? "", "/");
$currency = $_ENV["CURRENCY"] ?? "eur";
$unit = (int)($_ENV["PRICE_UNIT"] ?? 1900);
$name = $_ENV["PRODUCT_NAME"] ?? "Pro Plan";

try {
    $session = \Stripe\Checkout\Session::create([
        "mode" => "payment",
        "line_items" => [[
            "price_data" => [
                "currency" => $currency,
                "unit_amount" => $unit,
                "product_data" => ["name" => $name],
            ],
            "quantity" => 1
        ]],
        "success_url" => $baseUrl . "/success?session_id={CHECKOUT_SESSION_ID}",
        "cancel_url"  => $baseUrl . "/cancel",
        "payment_method_types" => ["card", "sepa_debit"], // adjust as needed
    ]);

    echo json_encode(["url" => $session->url]);
} catch (Throwable $e) {
    http_response_code(400);
    echo json_encode(["error" => $e->getMessage()]);
}
?>

Stripe Webhook — public/stripe-webhook.php

Verifies events (e.g., checkout.session.completed) and marks orders as paid.

<?php
require __DIR__ . "/../vendor/autoload.php";
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . "/..");
$dotenv->safeLoad();
header("Content-Type: application/json; charset=utf-8");

$payload = @file_get_contents("php://input");
$sig = $_SERVER["HTTP_STRIPE_SIGNATURE"] ?? "";
$endpointSecret = $_ENV["STRIPE_WEBHOOK_SECRET"] ?? "";

try {
    $event = \Stripe\Webhook::constructEvent($payload, $sig, $endpointSecret);
} catch (\UnexpectedValueException $e) {
    http_response_code(400); echo json_encode(["error"=>"Invalid payload"]); exit;
} catch (\Stripe\Exception\SignatureVerificationException $e) {
    http_response_code(400); echo json_encode(["error"=>"Invalid signature"]); exit;
}

switch ($event->type) {
    case "checkout.session.completed":
        $session = $event->data->object;
        // TODO: fulfill the order:
        // - lookup by $session->id or client_reference_id
        // - mark as paid, send email, unlock features, etc.
        break;

    case "charge.refunded":
        // TODO: mark as refunded
        break;
}

echo json_encode(["received" => true]);
?>

Stripe CLI (local testing):

stripe listen --forward-to localhost:8000/stripe-webhook.php

4. PayPal — Create & Capture (server-side)

We’ll create the order on the server, redirect the buyer to PayPal for approval, and then capture on our server after approval.

public/paypal-create-order.php

<?php
require __DIR__ . "/../vendor/autoload.php";
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . "/..");
$dotenv->safeLoad();
header("Content-Type: application/json; charset=utf-8");

use PayPalCheckoutSdk\Core\PayPalHttpClient;
use PayPalCheckoutSdk\Core\SandboxEnvironment;
use PayPalCheckoutSdk\Core\ProductionEnvironment;
use PayPalCheckoutSdk\Orders\OrdersCreateRequest;

$envName = $_ENV["PAYPAL_ENV"] ?? "sandbox";
$clientId = $_ENV["PAYPAL_CLIENT_ID"] ?? "";
$clientSecret = $_ENV["PAYPAL_CLIENT_SECRET"] ?? "";
$baseUrl = rtrim($_ENV["APP_BASE_URL"] ?? "", "/");
$currency = strtoupper($_ENV["CURRENCY"] ?? "EUR");
$unitCents = (int)($_ENV["PRICE_UNIT"] ?? 1900);
$amount = number_format($unitCents / 100, 2, ".", "");

$environment = ($envName === "production")
    ? new ProductionEnvironment($clientId, $clientSecret)
    : new SandboxEnvironment($clientId, $clientSecret);

$client = new PayPalHttpClient($environment);

$request = new OrdersCreateRequest();
$request->prefer('return=representation');
$request->body = [
    "intent" => "CAPTURE",
    "purchase_units" => [[
        "amount" => [
            "currency_code" => $currency,
            "value" => $amount
        ],
        "description" => $_ENV["PRODUCT_NAME"] ?? "Pro Plan"
    ]],
    "application_context" => [
        "brand_name" => "Your Brand",
        "landing_page" => "LOGIN",
        "user_action" => "PAY_NOW",
        "return_url" => $baseUrl . "/paypal-return",  // After approval -> your route/page
        "cancel_url" => $baseUrl . "/paypal-cancel"
    ]
];

try {
    $response = $client->execute($request);
    $approve = null;
    foreach ($response->result->links as $lnk) {
        if ($lnk->rel === "approve") { $approve = $lnk->href; break; }
    }
    echo json_encode([
        "order_id" => $response->result->id ?? null,
        "approve_url" => $approve
    ]);
} catch (Throwable $e) {
    http_response_code(400);
    echo json_encode(["error" => $e->getMessage()]);
}
?>

public/paypal-capture-order.php

Call this from your return page after approval: paypal-capture-order.php?order_id=...

<?php
require __DIR__ . "/../vendor/autoload.php";
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . "/..");
$dotenv->safeLoad();
header("Content-Type: application/json; charset=utf-8");

use PayPalCheckoutSdk\Core\PayPalHttpClient;
use PayPalCheckoutSdk\Core\SandboxEnvironment;
use PayPalCheckoutSdk\Core\ProductionEnvironment;
use PayPalCheckoutSdk\Orders\OrdersCaptureRequest;

$envName = $_ENV["PAYPAL_ENV"] ?? "sandbox";
$clientId = $_ENV["PAYPAL_CLIENT_ID"] ?? "";
$clientSecret = $_ENV["PAYPAL_CLIENT_SECRET"] ?? "";

$environment = ($envName === "production")
    ? new ProductionEnvironment($clientId, $clientSecret)
    : new SandboxEnvironment($clientId, $clientSecret);

$client = new PayPalHttpClient($environment);

$orderId = $_GET["order_id"] ?? "";

if (!$orderId) {
    http_response_code(400);
    echo json_encode(["error" => "Missing order_id"]);
    exit;
}

try {
    $request = new OrdersCaptureRequest($orderId);
    $request->prefer('return=representation');
    $response = $client->execute($request);

    // Inspect status: COMPLETED = paid
    $status = $response->result->status ?? "UNKNOWN";

    if ($status === "COMPLETED") {
        // TODO: fulfill order, mark as paid, send email, etc.
    }

    echo json_encode([
        "order_id" => $orderId,
        "status" => $status,
        "raw" => $response->result
    ]);
} catch (Throwable $e) {
    http_response_code(400);
    echo json_encode(["error" => $e->getMessage()]);
}
?>

Optional: PayPal Webhook — public/paypal-webhook.php

Set up in PayPal dashboard to receive async events (e.g., refunds, disputes).

<?php
require __DIR__ . "/../vendor/autoload.php";
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . "/..");
$dotenv->safeLoad();
header("Content-Type: application/json; charset=utf-8");

/**
 * Minimal handler; for production:
 * - Validate transmission (cert verification or webhook signature)
 * - Check event type + resource to update orders
 */
$body = file_get_contents("php://input");
$event = json_decode($body, true) ?: [];

$type = $event["event_type"] ?? "UNKNOWN";

switch ($type) {
    case "CHECKOUT.ORDER.APPROVED":
        // Optional: respond or log
        break;
    case "PAYMENT.CAPTURE.COMPLETED":
        // TODO: mark as paid
        break;
    case "PAYMENT.CAPTURE.REFUNDED":
        // TODO: mark as refunded
        break;
}

echo json_encode(["ok" => true]);
?>

Webhook security: In production, verify PayPal webhooks using the transmission verification method (cert/signature). Stripe verification is already shown above.


5. Success & Return Pages

Example (very basic):

<?php // public/paypal-return (pseudo route / page) $orderId = $_GET["token"] ?? ""; if ($orderId) { header("Location: ./paypal-capture-order.php?order_id=" . urlencode($orderId)); exit; } echo "Missing token (order_id)."; ?>


6. Security Checklist (Production)


7. Quick Test Commands

PHP dev server (from /public):

php -S localhost:8000

Stripe webhook (local):

stripe listen --forward-to localhost:8000/stripe-webhook.php

8. Next Steps / Extensions


Final Notes

Did you find this useful? Please rate this post: