Alvan Labs Pay UPI Gateway

WooCommerce Integration Guide โ€” v1.1 ยท May 2026

Merchant Guide
๐Ÿ”Œ Official WooCommerce Plugin
One-click install โ€” no manual code needed. Upload the zip in WP Admin โ†’ Plugins.
โฌ‡ Download Plugin (.zip)

Overview

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.

0%
MDR / Gateway fee
~15s
Auto-confirmation delay
1 hr
Payment session window

Prerequisites

How It Works โ€” Flow

  1. 1
    Customer places order on WooCommerce. Your plugin calls /api/v1/initiate-pay.
  2. 2
    Alvan Labs Pay returns a unique amount (e.g. โ‚น500.07) and a upi:// deep-link. Customer is redirected to the UPI payment screen.
  3. 3
    Customer pays via PhonePe / GPay / Paytm. Their UPI app sends a confirmation email to the merchant's linked Gmail.
  4. 4
    Alvan Labs Pay parses Gmail every ~15 seconds, matches the exact amount to the order, and marks it paid.
  5. 5
    Your plugin polls /api/v1/check-payment/{order_id}. When status = paid, WooCommerce order is auto-completed.

API Reference

POST /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"
}
503 โ€” All payment slots full (99 concurrent orders at same base amount).
404 โ€” Merchant ID not found or not configured.
GET /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.

GET /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"
}

WooCommerce Plugin Code

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;
        }
    }
});
โš  Important: The waiting page uses 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).

Activation Steps

  1. 1
    Create folder wp-content/plugins/tilebiz-upi/ and paste the PHP code into tilebiz-upi.php.
  2. 2
    Go to WordPress Admin โ†’ Plugins โ†’ Activate "Alvan Labs Pay UPI Payment Gateway".
  3. 3
    Go to WooCommerce โ†’ Settings โ†’ Payments โ†’ Alvan Labs Pay UPI โ†’ Manage.
  4. 4
    Set Alvan Labs Pay API URL (production URL: https://payments.alvanlabs.com) and your Merchant ID (get it from TileBiz admin). Save.
  5. 5
    Place a test order on your store and confirm the payment flow end-to-end.

Quick API Test (cURL)

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"

Tips & Gotchas

โœ… Exact Amount is Critical

Customers must pay the unique paise amount shown (e.g. โ‚น500.07 not โ‚น500). Your payment page must make this very clear.

โฑ 1-Hour Window

Payment sessions expire after 1 hour. If a customer pays late, the order won't auto-confirm. Handle expired orders manually.

๐Ÿ“ง Gmail Parsing Latency

Confirmation emails are parsed every ~15 seconds. Tell customers to wait up to 1 minute after payment for auto-confirmation.

๐Ÿ” No Duplicate order_id

Each order_id must be unique per merchant. WooCommerce order IDs are unique by default โ€” don't reuse them.

๐Ÿ”’ HTTPS Required

Use HTTPS for the Alvan Labs Pay API URL in production. UPI deep-links may not work from HTTP origins on mobile browsers.

๐Ÿ“ฑ Mobile UPI Deep-links

The upi:// link opens the default UPI app. On desktop, consider generating a QR code from the UPI link for scanning.

Support

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.