A self-hosted payment infrastructure for Indian merchants, featuring dynamic UPI QR codes, payment verification, account rotation, and secure webhook notifications.
Features · Screenshots · Architecture · Installation · API Reference
Important
Stable Release Candidate — November 2026
The stable-release candidate has been pushed to the main branch. It includes the browser-based installer, MySQL production support, payment verification integrations, timestamped webhook signatures, and cPanel/VPS deployment guides.
External verification is still pending: installation on a real cPanel shared-hosting account, production hosting and cron behavior, real-device UPI testing with GPay, PhonePe, Paytm and BHIM, and confirmation of GitHub Private Vulnerability Reporting settings.
Review the deployment guides and complete your own environment-specific testing before accepting live payments. Local automated tests and a disposable MySQL installation do not prove that every hosting provider or UPI app has been verified.
Status: Release candidate. A v1.1.0 Git tag and GitHub Release have not been published. Real cPanel hosting, production cron behavior, and mobile UPI app flows still require external verification.
Production ZIP: The locally prepared distribution is dist/upi-gateway-release-candidate.zip; it is not committed to GitHub or available as a GitHub download. Before distributing a v1.1.0 package, rebuild it from the intended source and verify that it contains production Composer dependencies in vendor/, compiled assets in public/build/, both installer entry points, .htaccess, .env.example, and the deployment guides. Exclude .env, databases, logs, node_modules/, tests, and nested archives.
cPanel installation: Extract the verified ZIP into a directory outside public_html, create a MySQL database and user, and point the domain document root to the application's public/ directory. Open /install over HTTPS to complete the browser wizard. Configure the scheduler and queue-worker cron jobs described in the cPanel installation guide. The prepared package does not require Composer, Node.js, or npm on the hosting server. For an Ubuntu server, follow the VPS guide.
Requirements and security: Use PHP 8.2 or newer with the extensions listed in the installation guide, a MySQL-compatible server through the MySQL driver, HTTPS, and writable Laravel runtime directories. The installer sets APP_DEBUG=false and ALLOW_REGISTRATION=false; keep the application and .env outside the public web root. Verify webhook signatures and deduplicate event IDs before fulfilling orders. Review SECURITY.md before accepting live payments.
GitHub Release link: [To be added when v1.1.0 is published].
UPI Gateway is a self-hosted, multi-merchant payment management platform designed for businesses that want to integrate UPI payments into their applications.
Each merchant can configure their own UPI accounts, generate dynamic payment QR codes, track transactions, and receive automated payment notifications.
Payments are intended to go directly to the configured merchant UPI accounts. The platform is not designed to hold or pool merchant funds.
- Multi-merchant account management
- Dynamic UPI QR generation and hosted checkout
- Automated payment verification integrations
- Multiple UPI account rotation and daily limits
- Separate Live and Sandbox environments
- Timestamped HMAC-SHA256 webhook signatures with a five-minute validation window
- Separate encrypted Live and Sandbox webhook signing secrets
- Webhook event IDs and sequence numbers for merchant-side deduplication and ordering
- Manual UTR verification and review flow
- Merchant API authentication
- Centralized administration dashboard
- Browser-based installation wizard for cPanel and VPS deployments
|
Payment Page
|
Payment Verification
|
|
Payment Success
|
|
- Independent merchant accounts.
- Merchant-specific API credentials.
- Individual UPI payment account configuration.
- Centralized administration and monitoring.
- Dynamic UPI payment QR generation.
- Unique transaction reference for every order.
- Hosted checkout and payment pages.
- UPI intent integration.
- Paytm for Business verification integration.
- Paytm Payment Gateway integration.
- PhonePe Payment Gateway integration.
- Manual UTR verification and review fallback.
- Configure multiple UPI accounts.
- Distribute payment requests across configured accounts for operational load balancing and redundancy.
- Account-level daily limits with automatic rollover to available accounts.
- Strict validation when
payment_account_idis explicitly requested (never silently falls back to another account). - Rotation is intended for legitimate load distribution across registered business accounts and must not be used to bypass bank-imposed velocity caps or regulatory limits.
- Separate Live and Sandbox credentials.
- Isolated test and live transactions.
- Sandbox transactions do not represent real payments.
- Timestamped HMAC-SHA256 request signatures (
t=...,v1=...) with a five-minute default tolerance. - Separate encrypted Live and Sandbox webhook signing secrets.
order.paidandorder.expiredevents, plus supported review and Sandbox events.- Durable webhook outbox, delivery logs, and queue retry handling.
- At-least-once delivery with event IDs and sequence numbers for merchant-side idempotency and ordering.
- API key and secret authentication.
- Merchant isolation.
- Transaction monitoring.
- Admin dashboard.
- Payment verification history.
- Installation lock after successful setup.
flowchart TD
A[Customer Application] --> B[Merchant Backend]
B --> C[UPI Gateway API]
C --> D[Order Creation]
D --> E[UPI QR / Hosted Checkout]
E --> F[Customer UPI App]
F --> G[Merchant UPI Account]
G --> H[Payment Verification]
H --> I[Provider Integration]
I --> J[Order Status Update]
J --> K[Signed Webhook]
K --> L[Merchant Backend]
- Merchant creates a payment order through the API.
- Gateway generates a unique order reference and payment URL.
- Customer scans the QR code or opens a UPI intent.
- Customer completes the payment using their UPI application.
- Gateway checks payment status through the configured verification integration.
- Order status is updated after successful verification.
- Merchant receives a signed webhook notification.
| Component | Technology |
|---|---|
| Backend Framework | Laravel 12 |
| Programming Language | PHP 8.2+ |
| Database | MySQL driver with MySQL 8.x or MariaDB 10.x/11.x (mandatory in production) |
| Frontend | Blade / JavaScript |
| Queue Processing | Laravel Queue |
| API Authentication | API Key & Secret |
| Webhook Security | HMAC-SHA256 |
| Web Server | Nginx / Apache-compatible PHP hosting |
| Build Tools | Composer, Node.js, NPM |
UPI Gateway includes a browser-based Installation Wizard. You do not need to run Laravel installation commands through the browser. First prepare your hosting environment and MySQL database, then complete the wizard.
Both supported deployment paths use the same Installation Wizard:
- cPanel shared hosting: prepare the hosting account, upload the prepared production package, and create a MySQL database in cPanel.
- Ubuntu VPS: prepare Ubuntu, Nginx, PHP-FPM, and MySQL, then configure the application web root.
| Guide | Description |
|---|---|
| Web Installation Wizard | Step-by-step explanation of every wizard screen |
| cPanel Installation | Shared hosting setup, database creation, upload, and wizard |
| Ubuntu VPS Installation | Ubuntu, Nginx, PHP-FPM, MySQL, SSL, queue, and scheduler |
- PHP 8.2 or newer.
- MySQL database or supported MariaDB server through the MySQL driver (mandatory in production; SQLite is only for automated tests).
- PDO and PDO MySQL.
- Required Laravel PHP extensions, including cURL, OpenSSL, Mbstring, Tokenizer, XML, JSON, and Fileinfo.
- Writable Laravel runtime directories with appropriately restricted permissions.
- HTTPS for production deployments.
- A prepared production package containing production Composer dependencies in
vendor/and compiled frontend assets inpublic/build/. The cPanel production server does not need Composer, Node.js, or npm.
Check the deployment-specific guide for the complete server preparation instructions. Hosting environments can differ, so verify PHP extensions and database access with your hosting provider.
A prepared cPanel release package must include the Laravel application source, production vendor/ dependencies, compiled public/build/ assets, installer entry points, required hidden server files, and deployment documentation.
The production package must not contain .env, SQLite/database files, runtime logs, node_modules/, tests, or nested development archives. Build assets and install production Composer dependencies before creating the package; cPanel users should not need Composer, Node.js, or npm on the hosting server.
Keep the Laravel application outside the public web root and configure the domain document root to point only to the application's public/ directory. Follow the cPanel guide for the supported directory layout.
Before opening the wizard:
- Configure the domain or subdomain.
- Select PHP 8.2 or newer and enable the required PHP extensions.
- Create a MySQL database and database user.
- Assign the required privileges to that user.
- Configure the web server document root to point to the Laravel
public/directory. - Enable HTTPS.
cPanel users: Create the database and user through MySQL Database Wizard (or MySQL Databases) in cPanel. Save the full database name, username, and password. cPanel may prepend your hosting account username to the database and user names.
VPS users: Install and configure Nginx, PHP-FPM, MySQL, required PHP extensions, and the application files as described in the VPS guide.
Visit your domain:
https://yourdomain.com/install
The wizard guides you through the following steps.
The wizard checks whether the server meets the application's installation requirements. Resolve any failed requirement in your hosting control panel or server configuration before continuing.
Enter your application name and URL, then select MySQL and provide the database connection details created during server preparation.
Database Setup — enter the MySQL details created in cPanel or configured on your VPS.
Use the Test connection and continue button to verify the database connection.
After the database connection succeeds, the wizard creates the required application tables.
Run Migrations — the wizard prepares the database tables.
Create the initial administrator account using your name, email address, and a strong password.
Create Admin Account — these credentials are used to access the administration dashboard.
After successful setup, sign in to the admin dashboard. The installer is locked after installation; do not bypass or remove the installation protection.
For command-line or development deployments, configure the following variables in your .env file. The browser installer configures the application during guided installation.
| Variable | Description | Example |
|---|---|---|
APP_NAME |
Application name | UPI Gateway |
APP_ENV |
Application environment | production |
APP_DEBUG |
Debug mode | false |
ALLOW_REGISTRATION |
Public merchant signup; disabled by default. | false |
APP_URL |
Application URL | https://example.com |
DB_CONNECTION |
Database driver | mysql |
DB_HOST |
Database host | 127.0.0.1 |
DB_DATABASE |
Database name | upi_gateway |
DB_USERNAME |
Database username | db_user |
DB_PASSWORD |
Database password | Strong password |
QUEUE_CONNECTION |
Queue driver | database |
SESSION_DRIVER |
Session driver | database |
SESSION_ENCRYPT |
Encrypt session payloads at rest in database | true |
APP_PREVIOUS_KEYS |
Comma-separated previous keys for zero-downtime key rotation | base64:... |
Never commit production credentials or API secrets to GitHub.
- Encrypted Session Payloads (
SESSION_ENCRYPT=true): New installations setSESSION_ENCRYPT=truein.env.example. When enabled, session data written to the databasesessionstable is encrypted with AES-256-CBC usingAPP_KEY. This ensures database administrators, storage backups, or query logs cannot read session attributes, CSRF tokens, or authentication states. - Toggling Encryption on Existing Deployments: Enabling
SESSION_ENCRYPT=trueon an existing deployment where sessions were previously stored in plaintext invalidates active sessions on their next HTTP request. Laravel catches decryption failures gracefully without throwing an HTTP 500 error, clearing the unencrypted session so users simply log in again. - Cluster Node Consistency: If deploying across multiple web servers behind a load balancer, all nodes must share the exact same
APP_KEY(andAPP_PREVIOUS_KEYS), otherwise nodes cannot decrypt session cookies or database payloads created by sibling nodes. - Zero-Downtime Key Rotation (
APP_PREVIOUS_KEYS): When rotatingAPP_KEY, append the previous key toAPP_PREVIOUS_KEYSin.env(comma-separated, e.g.APP_PREVIOUS_KEYS=base64:oldKey...). Laravel attempts decryption with the primaryAPP_KEYfirst, falling back to previous keys. On each request, active sessions are automatically re-encrypted with the new primary key upon write. Once all sessions have expired (afterSESSION_LIFETIMEminutes, default 120), remove the old key fromAPP_PREVIOUS_KEYS. - Safe Migration Procedure for Existing Installations:
- Notify active merchants or schedule a maintenance window if you wish to avoid unannounced logouts.
- In your server's
.env, setSESSION_ENCRYPT=true. - Clear and recache configuration:
php artisan config:cache. - Active users will be gracefully redirected to the login screen on their next request; all new sessions will be stored encrypted.
Public /register GET and POST requests return 404 while ALLOW_REGISTRATION=false, and the login page hides its signup link. An administrator who intentionally wants public signup can set ALLOW_REGISTRATION=true in the server's private .env file. If configuration is cached, run php artisan config:cache afterward using the server's PHP CLI. Set it back to false and refresh the cache to close signup. Existing merchant login and administrator merchant controls remain available while signup is closed.
MySQL is required for installation and production runtime. SQLite is used only by the automated test suite.
Example MySQL environment configuration for deployments managed through the command line:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=upi_gateway
DB_USERNAME=db_user
DB_PASSWORD=your_password| Provider | Integration Type | Verification | Status |
|---|---|---|---|
| Paytm for Business | UPI QR | Auto-polling via status endpoint | Unofficial / Experimental |
| Paytm PG | Hosted Checkout | Signed API status check (HMAC SHA-256) | Official API |
| PhonePe PG | Hosted Checkout | Signed OAuth API status check | Official API |
| Generic UPI ID | UPI QR | Manual UTR submission & review | Available |
Integration details & security distinction:
- Official Paytm PG (
paytm_pg): Uses official Paytm Payment Gateway APIs (secure.paytmpayments.com), authenticating requests with merchant keys and validating SHA-256 HMAC checksums. Recommended for all production commercial payment processing. - Paytm for Business (
paytm_business): An unofficial integration that polls Paytm's legacy web status endpoint (/merchant-status/getTxnStatus). It does not verify cryptographic signatures or response checksums, and Paytm provides no SLA or API stability guarantees. Upstream changes by Paytm may degrade or terminate status checks without notice. - Feature Flag (
GATEWAY_ENABLE_PAYTM_BUSINESS): The unofficial Paytm Business integration is governed byGATEWAY_ENABLE_PAYTM_BUSINESSin.env(defaults totruefor backward compatibility). When set tofalse:- New Paytm Business accounts and configuration updates are blocked.
- Existing Paytm Business accounts and encrypted credentials remain securely preserved.
- Ongoing verification checks return a safe disabled/pending status and will not mark orders as
PAID. - No orders or accounts are silently switched to other providers.
- Compliance & Account Advisory: For commercial payment collections, merchants are strongly advised to use registered merchant/current-account VPAs or official Payment Gateway providers (Paytm PG, PhonePe PG). Personal savings-account UPI handles may be subject to bank-policy restrictions, velocity checks, and payment blocks. The gateway does not independently verify VPA ownership or legal account classification (Savings vs. Current).
All merchant API requests require:
X-Api-Key: key_xxx
X-Api-Secret: sec_xxx| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/orders |
Create payment order |
| GET | /api/v1/orders/{id} |
Retrieve order status |
| GET | /api/v1/accounts |
List configured UPI accounts |
Full API documentation is available at /docs when the application is running.
Sandbox credentials use the test_key_... / test_sec_... pair and create internal TEST transactions only. Live and Sandbox credentials, orders, account lists, webhook URLs, and financial totals are isolated.
Request:
curl -X POST https://yourdomain.com/api/v1/orders \
-H "X-Api-Key: key_xxx" \
-H "X-Api-Secret: sec_xxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 199,
"client_order_id": "INV1001"
}'Example Response:
{
"success": true,
"data": {
"order_id": "ORDAB12CD34EF56GH78",
"client_order_id": "INV1001",
"amount": "199.00",
"currency": "INR",
"status": "pending",
"environment": "live",
"test": false,
"payment_url": "https://yourdomain.com/pay/ORDAB12CD34EF56GH78",
"upi_intent": "upi://pay?pa=merchant@upi&am=199.00&tr=ORDAB12CD34EF56GH78",
"expires_at": "2026-01-01T12:15:00+05:30"
}
}The response is illustrative; actual values depend on the application configuration.
Merchants can configure their webhook endpoint through:
Dashboard → API & Webhooks
Webhook events:
order.paidorder.expiredorder.under_revieworder.failed(Sandbox)order.pending(Sandbox)order.timeout(Sandbox)
Each delivery includes X-Signature: t=<unix-seconds>,v1=<hex-hmac>, X-Event-Id, and X-Event. The HMAC-SHA256 input is the timestamp, a period, and the exact raw request body. Use the separate Live or Sandbox webhook signing secret shown in the dashboard for the corresponding environment. Reject timestamps outside a five-minute window. Only timestamped X-Signature is sent and verified; legacy body-only signatures (X-Signature-Legacy) are retired and are neither sent nor accepted.
Delivery is at least once and may arrive out of order. Store the signed body's event_id with a unique constraint and process each event only once. Use event_sequence to avoid applying an older order state after a newer one. A valid signature and recent timestamp alone do not prevent duplicate processing.
$body = file_get_contents('php://input');
$secret = getenv('PAYMENT_WEBHOOK_SECRET'); // Select Live or Sandbox secret for this endpoint.
$header = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
if (!preg_match('/^t=(\d+),v1=([a-f0-9]{64})$/', $header, $matches)
|| abs(time() - (int) $matches[1]) > 300
|| !is_string($secret)
|| $secret === ''
|| !hash_equals(hash_hmac('sha256', $matches[1].'.'.$body, $secret), $matches[2])) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
$eventId = $event['event_id'] ?? null;
// Insert $eventId into a table with a unique constraint before applying effects.
// Treat a duplicate insert as an already processed delivery.
switch ($event['event'] ?? '') {
case 'order.paid':
// Process successful payment
break;
case 'order.expired':
// Process expired payment
break;
}Before accepting production traffic:
- Set
APP_ENV=productionandAPP_DEBUG=false. - Use HTTPS and a valid TLS certificate.
- Use a strong, unique administrator password.
- Keep database credentials and provider secrets private.
- Point the public document root only to Laravel's
public/directory. - Never expose
.env, application source, or private storage files. - Keep
storage/andbootstrap/cache/writable only as required by the application user. Do not use777as a permanent permission setting. - Keep the installation lock enabled after successful setup.
- Configure database backups and periodically verify that they can be restored.
- Configure queue processing and scheduled tasks as required by the deployment.
- Keep the operating system, PHP, and application dependencies updated.
- Verify webhook signatures and handle duplicate webhook deliveries safely.
- Keep session encryption enabled at rest (
SESSION_ENCRYPT=true) in production. - Ensure all cluster nodes share the identical
APP_KEYand maintainAPP_PREVIOUS_KEYSduring key rotation.
Set APP_ENV=production, APP_DEBUG=false, a generated APP_KEY, an HTTPS APP_URL, secure session cookies, and separate Live/Sandbox webhook URLs. Do not include .env, database files, logs, sessions, or development archives in a release package. Take a database backup before running migrations.
The expiry compatibility migration keeps orders.expires_at nullable for forward compatibility. Its rollback intentionally stops with an actionable error when NULL rows exist; it never guesses an expiry or alters payment history. Resolve those rows under an approved data policy, verify them, and then rerun the rollback.
Expired Live orders with supported provider status verification have a configurable 24-hour late-payment recovery window after expires_at. The orders:check-pending scheduler executes in independent, bounded dual phases: Phase 1 verifies due pending orders (bounded by GATEWAY_PENDING_VERIFICATION_BATCH_SIZE, default 100), and Phase 2 independently recovers due expired orders (bounded by GATEWAY_LATE_PAYMENT_BATCH_SIZE, default 100). Both phases use fair round-robin ordering (last_checked_at ASC NULLS FIRST, id ASC) and isolated error boundaries, ensuring a large pending backlog or query failures cannot starve late-payment recovery. Distributed command locking (GATEWAY_SCHEDULER_LOCK_SECONDS=300) prevents overlapping scheduler executions. Only validated provider success bound to the order's original expiry window can change expired orders to paid. Merchants must handle a signed order.paid event even after an earlier order.expired event. Manual UTR matching and Sandbox simulations cannot recover Live payments. Beyond the configured window there are no automatic checks; investigate with the bank/provider. See configuration, provider limitations and recovery behavior.
New order events are persisted transactionally in a durable webhook outbox. The scheduler dispatches up to 100 due events per minute by default. Payloads are immutable and include event_id, event_sequence, and occurred_at; HMAC signing is unchanged. Delivery is at least once and can be out of order: consumers must deduplicate by the signed event ID and prevent older sequences from reverting newer state. Queue outages retain the intent for retry; queued does not mean delivered.
For server configuration, queue worker, scheduler, permissions, and SSL instructions, follow the dedicated cPanel or Ubuntu VPS guide.
The platform supports extending payment verification through provider-specific verifier implementations.
Create:
app/Services/Verifiers/YourProviderVerifier.php
class YourProviderVerifier implements Verifier
{
public function check(Order $order): VerificationResult
{
// Call the provider's official API.
// Return a verified payment result:
// VerificationResult::paid(
// utr: '...',
// txnId: '...'
// );
// Or return a pending result:
// VerificationResult::pending();
}
}Register the provider in PaymentAccount::PROVIDERS:
'your_provider' => [
'label' => 'Your Provider',
'verifier' => YourProviderVerifier::class,
'hosted' => false,
],Use the provider's official API documentation and authentication requirements when implementing a new integration.
Run the application test suite:
composer run testupi-gateway/
│
├── app/
│ ├── Http/
│ ├── Models/
│ ├── Services/
│ │ └── Verifiers/
│ └── Jobs/
│
├── bootstrap/
├── config/
├── database/
├── docs/
│ └── images/
│ └── installation/
├── public/
├── resources/
│ ├── views/
│ ├── css/
│ └── js/
├── routes/
├── screenshots/
├── storage/
├── tests/
├── .env.example
├── artisan
├── composer.json
└── package.json
This is a representative structure; adapt it to the actual repository layout.
Distributed under the MIT License.
See the LICENSE file for details.
Have a question, collaboration idea, business inquiry, or technical discussion? Feel free to get in touch.
| Platform | Details |
|---|---|
| [email protected] | |
| 🌐 Portfolio | mayankdeveloper.site |
| linkedin.com/in/mynk-dev | |
| 🏢 Company | amtechinfo.online |
| @mayankdeveloper | |
| 🐙 GitHub | @mynkdev-21 |
Have an idea? Let's build something great together.
Mayank Singh · Full Stack Developer · Founder @ AmTech INFO
Built with ❤️ for Indian merchants.







