A Laravel application integrating six payment gateways, with emphasis on server-side verification and secure webhook handling.
A single product page offering checkout through multiple providers — each implemented as an independent gateway controller.
| Gateway | Status | Notes |
|---|---|---|
| Stripe | ✅ Complete | Checkout Session + signed webhook |
| Mollie | ✅ Working | Payment creation + return verification |
| Paystack | ✅ Working | Inline JS + server-side verification |
| PayPal | ✅ Working | Order creation + capture |
| Razorpay | ✅ Working | |
| 2Checkout | Requires merchant account activation |
Hosted Checkout Session with multi-currency display and Link support.
Inline modal with test-flow simulation — success, bank authentication, and declined states.
Tokenized card fields via 2Pay.js — card data never touches the server.
Live output from stripe listen. The 400 block is a deliberate test with an invalid signing secret — every event is rejected before reaching the database. The 200 block below it is the same flow with the correct secret.
Note that Stripe fires several events per checkout (product.created, charge.succeeded, payment_intent.*, checkout.session.completed). All return 200, since a non-2xx response on an unhandled event would trigger indefinite retries — only checkout.session.completed is acted upon.
- PHP 8.2+
- Laravel 12
- MySQL
git clone <repo-url>
cd Laravel_Payments_Methods
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate
php artisan serveAdd your gateway keys to .env:
STRIPE_SK=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
MOLLIE_KEY=test_xxx
PAYSTACK_PUBLIC_KEY=pk_test_xxx
PAYSTACK_SECRET_KEY=sk_test_xxx
PAYPAL_CLIENT_ID=xxx
PAYPAL_SECRET=xxxstripe login
stripe listen --forward-to localhost:8000/stripe/webhook
stripe trigger checkout.session.completedCopy the whsec_... printed by stripe listen into .env, then run php artisan config:clear.
Note the webhook route is excluded from CSRF verification in bootstrap/app.php — Stripe posts server-to-server and carries no token.
Signature verification — every webhook is validated via HMAC against the raw request body before processing. Using $request->getContent() rather than $request->all() matters here: re-encoding the JSON would alter the bytes and break the signature.
Idempotency — a unique constraint on session_id prevents duplicate records when Stripe retries an event. Retries are expected behaviour, not edge cases, and the database constraint holds even under concurrent delivery where an exists() check alone would race.
Server-side verification — payment status is always read back from the gateway's API, never trusted from browser parameters.
Currency handling — amounts are stored in minor units (cents) as integers to avoid floating-point drift. Gateways differ here: Mollie expects a decimal string, Paystack and Stripe expect integers.
| Gateway | Card |
|---|---|
| Stripe | 4242 4242 4242 4242 |
| Mollie | 4242 4242 4242 4242 |
| Paystack | 4084 0840 8408 4081 |
| 2Checkout | 5555 5555 5555 4444 |
Any future expiry date and CVV.
MIT







