WooCommerce Integration Guide โ v1.1 ยท May 2026
Alvan Labs Pay UPI Gateway lets your WooCommerce store accept UPI payments via a unique "paise trick" โ each order gets a distinct amount (e.g. โน1,500.07) so incoming Gmail UPI confirmations can be automatically matched to the right order. No payment gateway account needed โ payments land directly in your UPI ID.
https://payments.alvanlabs.com/api/v1/initiate-pay.upi:// deep-link. Customer is redirected to the UPI payment screen.paid./api/v1/check-payment/{order_id}. When status = paid, WooCommerce order is auto-completed./api/v1/initiate-pay
Step 1 โ Create payment session
Request Body (JSON)
{
"order_id": "WC-1001", // your WooCommerce order ID (string)
"customer_email": "buyer@example.com",
"customer_phone": "9876543210",
"base_amount": 500.00, // order total in โน (float)
"merchant_id": "mch_a3f8d2b1c4e5f6789012ab" // your Merchant Key (string, from admin)
}
Success Response (200)
{
"status": "success",
"order_id": "WC-1001",
"unique_amount": 500.07, // the exact amount customer must pay
"created_at": "2026-05-14T08:44:15",
"expires_at": "2026-05-14T09:44:15", // 1-hour window
"upi_link": "upi://pay?pa=merchant@upi&pn=MerchantName&am=500.07&cu=INR"
}
/api/v1/check-payment/{order_id}?merchant_id={id}
Poll for confirmation
Response
// While waiting:
{ "success": true, "status": "pending" }
// After Gmail match:
{ "success": true, "status": "paid" }
// If expired:
{ "success": true, "status": "expired" }
// Wrong order_id:
{ "success": false, "status": "not_found" }
Poll every 5 seconds for up to 60 minutes. Stop polling once status is paid, expired, or failed.
/api/v1/check-status/{order_id}?merchant_id={id}
Full order details
{
"order_id": "WC-1001",
"status": "paid",
"amount": 500.07,
"created_at": "2026-05-14T08:44:15",
"expires_at": "2026-05-14T09:44:15"
}
Create a file at wp-content/plugins/tilebiz-upi/tilebiz-upi.php
and paste the code below. Then activate it from WordPress โ Plugins.
<?php
/**
* Plugin Name: Alvan Labs Pay UPI Payment Gateway
* Description: Auto-reconciled UPI payments via Alvan Labs Pay Engine.
* Version: 1.0
* Author: Alvan Labs
*/
if (!defined('ABSPATH')) exit;
add_filter('woocommerce_payment_gateways', function($gateways) {
$gateways[] = 'WC_AlvanLabs_UPI';
return $gateways;
});
add_action('plugins_loaded', function() {
if (!class_exists('WC_Payment_Gateway')) return;
class WC_AlvanLabs_UPI extends WC_Payment_Gateway {
public function __construct() {
$this->id = 'tilebiz_upi';
$this->method_title = 'Alvan Labs Pay UPI';
$this->method_description = 'Accept UPI payments with auto-reconciliation via Alvan Labs Pay Engine.';
$this->has_fields = false;
$this->icon = plugin_dir_url(__FILE__) . 'upi-logo.png'; // optional
$this->init_form_fields();
$this->init_settings();
$this->title = $this->get_option('title');
$this->description = $this->get_option('description');
add_action('woocommerce_update_options_payment_gateways_' . $this->id,
[$this, 'process_admin_options']);
add_action('woocommerce_api_tilebiz_upi', [$this, 'poll_result_page']);
}
/* โโ Admin Settings โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
public function init_form_fields() {
$this->form_fields = [
'enabled' => ['title' => 'Enable', 'type' => 'checkbox', 'default' => 'yes'],
'title' => ['title' => 'Title', 'type' => 'text', 'default' => 'Pay via UPI'],
'description' => ['title' => 'Description', 'type' => 'textarea',
'default' => 'Scan QR or use any UPI app โ GPay, PhonePe, Paytm.'],
'api_url' => ['title' => 'TileBiz API URL', 'type' => 'text',
'description' => 'e.g. https://tilebiz.duckdns.org or https://pay.yourdomain.com',
'default' => 'https://payments.alvanlabs.com'],
'merchant_id' => ['title' => 'Merchant Key', 'type' => 'text',
'description' => 'Your Merchant Key from Alvan Labs Pay admin (format: mch_xxxxxxxx...).',
'default' => ''],
];
}
/* โโ Process Payment โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
public function process_payment($order_id) {
$order = wc_get_order($order_id);
$api_url = rtrim($this->get_option('api_url'), '/');
$merchant_id = $this->get_option('merchant_id');
// 1. Create payment session
$response = wp_remote_post("$api_url/api/v1/initiate-pay", [
'headers' => ['Content-Type' => 'application/json'],
'body' => json_encode([
'order_id' => (string) $order_id,
'customer_email' => $order->get_billing_email(),
'customer_phone' => $order->get_billing_phone() ?: '0000000000',
'base_amount' => (float) $order->get_total(),
'merchant_id' => $merchant_id,
]),
'timeout' => 15,
]);
if (is_wp_error($response)) {
wc_add_notice('Payment service unavailable. Please try again.', 'error');
return ['result' => 'fail'];
}
$data = json_decode(wp_remote_retrieve_body($response), true);
if (empty($data['status']) || $data['status'] !== 'success') {
wc_add_notice('Could not create payment session: ' . ($data['detail'] ?? 'unknown error'), 'error');
return ['result' => 'fail'];
}
// Store session data on order
$order->update_meta_data('_tilebiz_unique_amount', $data['unique_amount']);
$order->update_meta_data('_tilebiz_upi_link', $data['upi_link']);
$order->update_meta_data('_tilebiz_expires_at', $data['expires_at']);
$order->save();
// Mark as pending payment
$order->update_status('pending', 'Awaiting UPI payment confirmation.');
WC()->cart->empty_cart();
// Redirect to UPI waiting page
$wait_url = add_query_arg([
'tilebiz_order' => $order_id,
'amount' => $data['unique_amount'],
'upi_link' => urlencode($data['upi_link']),
'expires_at' => urlencode($data['expires_at']),
], wc_get_endpoint_url('order-received', $order_id, wc_get_checkout_url()));
return ['result' => 'success', 'redirect' => $wait_url];
}
/* โโ Waiting / Polling Page โโโโโโโโโโโโโโโโโโโโโโโโ */
public function poll_result_page() {
$order_id = absint($_GET['tilebiz_order'] ?? 0);
$merchant_id = $this->get_option('merchant_id');
$api_url = rtrim($this->get_option('api_url'), '/');
if (!$order_id) {
wp_die('Invalid request.');
}
$poll_url = "$api_url/api/v1/check-payment/{$order_id}?merchant_id={$merchant_id}";
$amount = sanitize_text_field($_GET['amount'] ?? '');
$upi_link = esc_url(urldecode($_GET['upi_link'] ?? ''));
$expires_at = sanitize_text_field(urldecode($_GET['expires_at'] ?? ''));
$order = wc_get_order($order_id);
$success_url = $this->get_return_url($order);
// Inline waiting page โ no extra template needed
echo '<!DOCTYPE html><html><head>';
echo '<meta charset="UTF-8">';
echo '<title>Complete Your UPI Payment</title>';
echo '<meta name="viewport" content="width=device-width,initial-scale=1">';
echo '<style>
body{font-family:sans-serif;background:#f9fafb;display:flex;align-items:center;
justify-content:center;min-height:100vh;margin:0}
.card{background:#fff;border-radius:1.5rem;padding:2.5rem;max-width:420px;
width:100%;text-align:center;box-shadow:0 4px 24px #0001}
h2{font-size:1.4rem;font-weight:700;margin-bottom:0.5rem}
.amount{font-size:2.5rem;font-weight:800;color:#4f46e5;margin:1rem 0}
.note{font-size:0.82rem;color:#6b7280;margin-bottom:1.5rem}
.upi-btn{display:inline-block;background:#16a34a;color:#fff;padding:0.75rem 2rem;
border-radius:999px;font-weight:700;text-decoration:none;font-size:1rem;
margin-bottom:1.5rem}
.status{font-size:0.95rem;color:#6b7280;margin-top:1rem}
.spinner{display:inline-block;width:1rem;height:1rem;border:2px solid #d1d5db;
border-top-color:#4f46e5;border-radius:50%;animation:spin 0.8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.paid{color:#16a34a;font-weight:700;font-size:1.1rem}
.expired{color:#dc2626;font-weight:700}
</style>
</head><body>
<div class="card">
<h2>Complete Your UPI Payment</h2>
<div class="amount">โน' . esc_html($amount) . '</div>
<p class="note">โ Pay the <strong>exact amount shown</strong> โ even the paise matter.</p>
<a href="' . $upi_link . '" class="upi-btn">Pay Now via UPI App</a>
<p class="note">Expires at: ' . esc_html($expires_at) . '</p>
<div class="status" id="st"><span class="spinner"></span> Waiting for payment confirmation...</div>
</div>
<script>
const pollUrl = ' . json_encode($poll_url) . ';
const successUrl= ' . json_encode($success_url) . ';
let tries = 0, maxTries = 720; // 720 ร 5s = 60 min
async function poll() {
if (tries++ > maxTries) {
document.getElementById("st").innerHTML =
"<span class=\'expired\'>โฑ Payment window expired.</span> <a href=\'/\'>Return to shop</a>";
return;
}
try {
const r = await fetch(pollUrl);
const data = await r.json();
if (data.status === "paid") {
document.getElementById("st").innerHTML =
"<span class=\'paid\'>โ Payment confirmed! Redirecting...</span>";
setTimeout(() => location.href = successUrl, 1500);
return;
} else if (data.status === "expired" || data.status === "failed") {
document.getElementById("st").innerHTML =
"<span class=\'expired\'>Payment ' . "'" . '" + data.status + "'. ' Redirected in 3s.</span>";
setTimeout(() => location.href = successUrl, 3000);
return;
}
} catch(e) {}
setTimeout(poll, 5000);
}
poll();
</script>
</body></html>';
exit;
}
}
});
woocommerce_api_tilebiz_upi hook.
For it to work, go to WooCommerce โ Settings โ Advanced โ Webhooks or simply ensure
pretty permalinks are enabled (Settings โ Permalinks โ Post name).
wp-content/plugins/tilebiz-upi/ and paste the PHP code into tilebiz-upi.php.
https://payments.alvanlabs.com) and your Merchant ID (get it from TileBiz admin). Save.
Test before integrating into WooCommerce:
# Current production base URL (use HTTPS after SSL/domain setup)
BASE=https://payments.alvanlabs.com
# Step 1 โ Create payment session
curl -X POST $BASE/api/v1/initiate-pay \
-H "Content-Type: application/json" \
-d '{
"order_id": "TEST-001",
"customer_email": "test@example.com",
"customer_phone": "9876543210",
"base_amount": 100.00,
"merchant_id": "mch_a3f8d2b1c4e5f6789012ab"
}'
# Expected:
# { "status":"success", "unique_amount":100.07, "upi_link":"upi://pay?..." }
# Step 2 โ Poll for status
curl "$BASE/api/v1/check-payment/TEST-001?merchant_id=mch_a3f8d2b1c4e5f6789012ab"
# Expected (before payment):
# { "success":true, "status":"pending" }
# Step 3 โ Full order details
curl "$BASE/api/v1/check-status/TEST-001?merchant_id=mch_a3f8d2b1c4e5f6789012ab"
Customers must pay the unique paise amount shown (e.g. โน500.07 not โน500). Your payment page must make this very clear.
Payment sessions expire after 1 hour. If a customer pays late, the order won't auto-confirm. Handle expired orders manually.
Confirmation emails are parsed every ~15 seconds. Tell customers to wait up to 1 minute after payment for auto-confirmation.
Each order_id must be unique per merchant. WooCommerce order IDs are unique by default โ don't reuse them.
Use HTTPS for the Alvan Labs Pay API URL in production. UPI deep-links may not work from HTTP origins on mobile browsers.
The upi:// link opens the default UPI app. On desktop, consider generating a QR code from the UPI link for scanning.
For API issues, check your Alvan Labs Pay Merchant Dashboard โ Transactions for incoming payments. Contact your Alvan Labs Pay admin with your Merchant ID and the order_id in question.