Category:
Tutorials & Guides
|| Posted Aug 15, 2026
How to Build a Custom Payment Gateway in PHP: A Step-by-Step Guide for Beginners
How to Build a Custom Payment Gateway Integration in PHP: A Step-by-Step Guide for Beginners
Processing online payments is a cornerstone of modern web development. While building a full-fledged payment network (like Visa or Mastercard) requires specialized financial licensure and complex infrastructure, building a custom payment gateway integration in PHP is very achievable.
Whether you are building a custom e-commerce engine, integrating a regional payment processor, or creating a standardized payment abstraction layer for your application, this step-by-step guide will walk you through building a clean, modern, and secure PHP payment gateway driver.
Prerequisites & Architecture
Before diving into code, ensure your development environment meets these baseline requirements:
- PHP 8.2+ installed on your machine.
- Composer for dependency management.
- Basic understanding of Object-Oriented Programming (OOP) in PHP.
- cURL or Guzzle for handling HTTP API requests.
Key Terminology
- Merchant Account: Where business funds land after processing.
- Tokenization: Replacing sensitive credit card numbers with a secure string token before sending data to your server.
- Webhook: An automated HTTP callback sent by the payment processor to notify your application of asynchronous events (e.g., successful recurring payments or chargebacks).
Step 1: Project Setup & Dependencies
First, create a project directory and initialize Composer. We will use guzzlehttp/guzzle for HTTP communications and vlucas/phpdotenv to manage sensitive API keys securely.
Bash
mkdir php-payment-gateway && cd php-payment-gateway
composer init --no-interaction
composer require guzzlehttp/guzzle vlucas/phpdotenv
Configure composer.json to enable PSR-4 autoloading for your project namespace:
JSON
{
"require": {
"guzzlehttp/guzzle": "^7.8",
"vlucas/phpdotenv": "^5.6"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
Run composer dump-autoload to register your directory structure:
php-payment-gateway/
├── .env
├── public/
│ ├── index.php
│ └── webhook.php
├── src/
│ ├── Contracts/
│ │ └── PaymentGatewayInterface.php
│ └── Gateways/
│ └── CustomGateway.php
└── vendor/
Create a .env file in your root folder to hold your API keys:
Ini, TOML
PAYMENT_GATEWAY_KEY="sk_test_123456789"
PAYMENT_GATEWAY_URL="https://api.sandbox.gateway.com/v1"
Step 2: Design the Payment Interface
To ensure your code remains extensible, define a PaymentGatewayInterface. This allows you to swap out or add alternative processors (e.g., Stripe, PayPal, Razorpay) in the future without breaking your core application logic.
Create src/Contracts/PaymentGatewayInterface.php:
PHP
<?php
declare(strict_types=1);
namespace App\Contracts;
interface PaymentGatewayInterface
{
/**
* Charge a customer account or payment token.
*/
public function charge(float $amount, string $currency, string $token, array $metadata = []): array;
/**
* Refund a previously processed transaction.
*/
public function refund(string $transactionId, float $amount): array;
}
Step 3: Implement the Payment Gateway Driver
Now, create the custom gateway class implementing your interface. This class communicates with the remote payment processor's REST API.
Create src/Gateways/CustomGateway.php:
PHP
<?php
declare(strict_types=1);
namespace App\Gateways;
use App\Contracts\PaymentGatewayInterface;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use RuntimeException;
class CustomGateway implements PaymentGatewayInterface
{
private Client $httpClient;
private string $apiKey;
public function __construct(string $apiKey, string $baseUrl)
{
$this->apiKey = $apiKey;
$this->httpClient = new Client([
'base_uri' => rtrim($baseUrl, '/') . '/',
'timeout' => 10.0,
'headers' => [
'Authorization' => 'Bearer ' . $this->apiKey,
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]);
}
public function charge(float $amount, string $currency, string $token, array $metadata = []): array
{
try {
// Convert amount to minor currency units (e.g., $10.00 -> 1000 cents)
$payload = [
'amount' => (int) round($amount * 100),
'currency' => strtolower($currency),
'source' => $token,
'description' => $metadata['description'] ?? 'Standard Order Payment',
];
$response = $this->httpClient->post('charges', [
'json' => $payload,
]);
return json_decode($response->getBody()->getContents(), true);
} catch (GuzzleException $e) {
throw new RuntimeException('Payment Processing Error: ' . $e->getMessage(), $e->getCode(), $e);
}
}
public function refund(string $transactionId, float $amount): array
{
try {
$response = $this->httpClient->post("refunds", [
'json' => [
'charge_id' => $transactionId,
'amount' => (int) round($amount * 100),
],
]);
return json_decode($response->getBody()->getContents(), true);
} catch (GuzzleException $e) {
throw new RuntimeException('Refund Processing Error: ' . $e->getMessage(), $e->getCode(), $e);
}
}
}
Step 4: Process Payments via Front Controller
Set up public/index.php to receive payment processing requests from your application's checkout page:
PHP
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use App\Gateways\CustomGateway;
use Dotenv\Dotenv;
// Load environment variables
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();
// Simple router check for incoming POST payment requests
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_SERVER['REQUEST_URI'] === '/checkout') {
header('Content-Type: application/json');
$paymentToken = $_POST['payment_token'] ?? null;
$amount = (float) ($_POST['amount'] ?? 0.00);
$currency = 'USD';
if (!$paymentToken || $amount <= 0) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Invalid payment payload.']);
exit;
}
try {
$gateway = new CustomGateway(
$_ENV['PAYMENT_GATEWAY_KEY'],
$_ENV['PAYMENT_GATEWAY_URL']
);
$result = $gateway->charge($amount, $currency, $paymentToken, [
'description' => 'E-commerce Checkout Order #1042',
]);
echo json_encode([
'status' => 'success',
'transaction_id' => $result['id'] ?? 'TXN_' . uniqid(),
'data' => $result,
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'status' => 'failed',
'message' => $e->getMessage(),
]);
}
}
Step 5: Handle Webhooks for Asynchronous Events
Payment processing often involves asynchronous verification (e.g., bank transfers or fraud checks). Use a dedicated webhook endpoint to handle status updates.
Create public/webhook.php:
PHP
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
// Retrieve the raw request body
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_GATEWAY_SIGNATURE'] ?? '';
// Verify Webhook Signature (Crucial for Security)
$secret = $_ENV['PAYMENT_GATEWAY_KEY'];
$computedSignature = hash_hmac('sha256', $payload, $secret);
if (!hash_equals($computedSignature, $signature)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature verification']);
exit;
}
$event = json_decode($payload, true);
// Handle event types
switch ($event['type'] ?? '') {
case 'payment_intent.succeeded':
$charge = $event['data'];
// TODO: Update database status to "Paid"
break;
case 'payment_intent.failed':
// TODO: Update database status to "Failed" & notify customer
break;
default:
// Unhandled event
break;
}
http_response_code(200);
echo json_encode(['status' => 'acknowledged']);
Critical Security Checklist for Payment Integration
PCI-DSS Warning: Never store, transmit, or record raw Credit Card Numbers (PAN), CVVs, or expiration dates on your PHP application server. Always utilize client-side tokenization (via JavaScript SDKs provided by your payment processor) to convert raw card details into a single-use token on the browser side before submitting your HTML form.
| Security Layer | Implementation Requirement |
| HTTPS / TLS | Enforce HTTPS globally across your web server. Never process payment tokens over unencrypted HTTP. |
| Idempotency Keys | Send a unique UUID per transaction to prevent double-charging users during network retries. |
| Signature Verification | Always authenticate incoming webhooks using HMAC hash checks (hash_equals). |
| Environment Isolation | Store API credentials strictly inside uncommitted .env configuration files. |
Summary
Building a custom payment gateway integration in PHP boils down to three main components:
- Designing an extensible interface using OOP principles.
- Handling external HTTP requests via Guzzle/cURL.
- Safeguard your application with tokenization, HTTPS, and webhook validation.
By decoupling your payment logic behind an interface, you ensure your PHP application remains clean, testable, and ready to scale with any payment provider.