Skip to main content

Payment Gateway Apps

Payment gateway apps allow you to integrate custom payment processors with O2VEND. This guide covers creating payment gateway apps.

Payment Gateway Overview

Payment gateway apps:

  • Process payments through custom gateways
  • Handle payment callbacks and webhooks
  • Display payment options in checkout
  • Manage payment-specific offers and discounts

Payment Gateway Structure

A payment gateway app requires:

payment-gateway-app/
app.json # App metadata with payment_gateway config
gateway.js # Payment gateway implementation (required)
snippets/
checkout_payment_before.liquid
checkout_payment_methods_before.liquid
assets/
gateway.css
gateway.js

App Metadata

The app.json must include payment gateway configuration:

{
"id": "razorpay-gateway",
"name": "Razorpay Payment Gateway",
"version": "1.0.0",
"description": "Razorpay payment integration",
"hooks": [
"checkout_payment_before",
"checkout_payment_methods_before",
"payment_gateway_offers"
],
"assets": ["gateway.css", "gateway.js"],
"priority": 100,
"payment_gateway": {
"id": "razorpay",
"name": "Razorpay",
"supported_methods": ["card", "netbanking", "upi", "wallet"],
"settings": {
"key_id": {
"type": "text",
"label": "Razorpay Key ID",
"required": true
},
"key_secret": {
"type": "password",
"label": "Razorpay Key Secret",
"required": true
},
"test_mode": {
"type": "checkbox",
"label": "Test Mode",
"default": false
}
}
}
}

Gateway Implementation

The gateway.js file exports the gateway implementation:

/**
* Payment Gateway Implementation
* Exports initialization function and gateway methods
*/

module.exports = {
/**
* Initialize the payment gateway
* @param {Object} config - Gateway configuration from app settings
* @returns {Object} Gateway instance
*/
initialize(config) {
const Razorpay = require('razorpay');

// Initialize Razorpay client
const razorpay = new Razorpay({
key_id: config.key_id,
key_secret: config.key_secret
});

return {
name: 'Razorpay',
id: 'razorpay',

/**
* Process payment
* @param {Object} paymentData - Payment data
* @returns {Promise<Object>} Payment result
*/
async processPayment(paymentData) {
try {
const {
amount,
currency,
order_id,
customer,
notes
} = paymentData;

// Create Razorpay order
const order = await razorpay.orders.create({
amount: amount * 100, // Convert to paise
currency: currency || 'INR',
receipt: order_id,
notes: notes || {}
});

return {
success: true,
order_id: order.id,
amount: order.amount,
currency: order.currency,
key: config.key_id,
// Additional data for frontend
gateway_data: {
order_id: order.id,
amount: order.amount,
currency: order.currency
}
};
} catch (error) {
console.error('Razorpay payment error:', error);
return {
success: false,
error: error.message
};
}
},

/**
* Verify payment
* @param {Object} paymentData - Payment verification data
* @returns {Promise<Object>} Verification result
*/
async verifyPayment(paymentData) {
try {
const crypto = require('crypto');
const {
razorpay_order_id,
razorpay_payment_id,
razorpay_signature
} = paymentData;

// Generate signature
const text = `${razorpay_order_id}|${razorpay_payment_id}`;
const generatedSignature = crypto
.createHmac('sha256', config.key_secret)
.update(text)
.digest('hex');

// Verify signature
if (generatedSignature === razorpay_signature) {
// Fetch payment details
const payment = await razorpay.payments.fetch(razorpay_payment_id);

return {
success: true,
verified: true,
payment_id: payment.id,
order_id: payment.order_id,
amount: payment.amount / 100, // Convert from paise
status: payment.status
};
} else {
return {
success: false,
verified: false,
error: 'Invalid signature'
};
}
} catch (error) {
console.error('Razorpay verification error:', error);
return {
success: false,
error: error.message
};
}
},

/**
* Handle webhook
* @param {Object} webhookData - Webhook payload
* @returns {Promise<Object>} Webhook processing result
*/
async handleWebhook(webhookData) {
try {
const crypto = require('crypto');
const webhookSecret = config.webhook_secret;

// Verify webhook signature
const signature = crypto
.createHmac('sha256', webhookSecret)
.update(JSON.stringify(webhookData))
.digest('hex');

// Process webhook event
if (webhookData.event === 'payment.captured') {
// Payment captured successfully
return {
success: true,
event: webhookData.event,
payment_id: webhookData.payload.payment.entity.id
};
}

return {
success: true,
event: webhookData.event
};
} catch (error) {
console.error('Webhook processing error:', error);
return {
success: false,
error: error.message
};
}
},

/**
* Get payment status
* @param {string} paymentId - Payment ID
* @returns {Promise<Object>} Payment status
*/
async getPaymentStatus(paymentId) {
try {
const payment = await razorpay.payments.fetch(paymentId);

return {
success: true,
status: payment.status,
amount: payment.amount / 100,
currency: payment.currency
};
} catch (error) {
return {
success: false,
error: error.message
};
}
},

/**
* Refund payment
* @param {Object} refundData - Refund data
* @returns {Promise<Object>} Refund result
*/
async refundPayment(refundData) {
try {
const {
payment_id,
amount,
notes
} = refundData;

const refund = await razorpay.payments.refund(payment_id, {
amount: amount ? amount * 100 : undefined, // Convert to paise
notes: notes || {}
});

return {
success: true,
refund_id: refund.id,
amount: refund.amount / 100,
status: refund.status
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
};
}
};

Payment Gateway Hooks

Checkout Payment Hooks

checkout_payment_before

Display payment gateway information before payment section:

{% comment %}
Hook: checkout_payment_before
Display Razorpay information
{% endcomment %}

<div class="razorpay-info">
<h3>Pay with Razorpay</h3>
<p>Secure payment processing</p>
<ul>
<li>Credit/Debit Cards</li>
<li>Net Banking</li>
<li>UPI</li>
<li>Wallets</li>
</ul>
</div>

checkout_payment_methods_before

Add payment method option:

{% comment %}
Hook: checkout_payment_methods_before
Add Razorpay payment option
{% endcomment %}

<div class="payment-method" data-gateway="razorpay">
<input type="radio" name="payment_method" value="razorpay" id="razorpay">
<label for="razorpay">
<img src="/apps/{{ tenant.id }}/razorpay-gateway/assets/razorpay-logo.png" alt="Razorpay">
<span>Razorpay</span>
</label>
</div>

payment_gateway_offers

Display gateway-specific offers:

{% comment %}
Hook: payment_gateway_offers
Display Razorpay offers
{% endcomment %}

{% if cart.total_price > 1000 %}
<div class="razorpay-offer">
<p>Get 5% cashback with Razorpay!</p>
<button onclick="applyRazorpayOffer()">Apply Offer</button>
</div>
{% endif %}

Frontend Integration

Payment Form

Create a payment form in your checkout:

<form id="razorpay-payment-form" data-gateway="razorpay">
<div id="razorpay-checkout"></div>
<button type="submit" id="razorpay-pay-button">Pay with Razorpay</button>
</form>

JavaScript Integration

// assets/gateway.js
(function() {
const RazorpayGateway = {
init: function() {
this.bindEvents();
this.loadRazorpayScript();
},

loadRazorpayScript: function() {
if (window.Razorpay) {
this.setupRazorpay();
return;
}

const script = document.createElement('script');
script.src = 'https://checkout.razorpay.com/v1/checkout.js';
script.onload = () => this.setupRazorpay();
document.head.appendChild(script);
},

setupRazorpay: function() {
const form = document.getElementById('razorpay-payment-form');
if (!form) return;

form.addEventListener('submit', (e) => {
e.preventDefault();
this.processPayment();
});
},

async processPayment() {
try {
// Get payment data from server
const response = await fetch('/api/checkout/payment/razorpay/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
amount: window.cartTotal,
currency: 'INR',
order_id: window.orderId
})
});

const data = await response.json();

if (!data.success) {
throw new Error(data.error);
}

// Initialize Razorpay checkout
const options = {
key: data.key,
amount: data.amount,
currency: data.currency,
name: window.shopName,
description: 'Order Payment',
order_id: data.order_id,
handler: (response) => {
this.verifyPayment(response);
},
prefill: {
name: window.customerName,
email: window.customerEmail,
contact: window.customerPhone
},
theme: {
color: '#000000'
}
};

const razorpay = new Razorpay(options);
razorpay.open();
} catch (error) {
console.error('Payment error:', error);
alert('Payment failed: ' + error.message);
}
},

async verifyPayment(paymentResponse) {
try {
const response = await fetch('/api/checkout/payment/razorpay/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
razorpay_order_id: paymentResponse.razorpay_order_id,
razorpay_payment_id: paymentResponse.razorpay_payment_id,
razorpay_signature: paymentResponse.razorpay_signature
})
});

const data = await response.json();

if (data.success && data.verified) {
// Redirect to order confirmation
window.location.href = `/order-confirmation?payment_id=${data.payment_id}`;
} else {
throw new Error(data.error || 'Payment verification failed');
}
} catch (error) {
console.error('Verification error:', error);
alert('Payment verification failed: ' + error.message);
}
}
};

// Initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => RazorpayGateway.init());
} else {
RazorpayGateway.init();
}
})();

Webhook Handling

Set up webhook endpoint to handle payment events:

// In your server/API
router.post('/webhooks/razorpay', async (req, res) => {
try {
const appService = new AppService();
const gateway = await appService.loadGatewayInstance(
req.tenant.id,
'razorpay-gateway',
appMetadata
);

const result = await gateway.handleWebhook(req.body);

if (result.success) {
// Process webhook event
// Update order status
// Send notifications
}

res.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: error.message });
}
});

Testing Payment Gateways

Test Mode

Enable test mode in app settings:

{
"test_mode": true,
"test_key_id": "rzp_test_...",
"test_key_secret": "test_secret_..."
}

Test Cards

Use test card numbers provided by the payment gateway:

  • Razorpay: Test cards available in dashboard
  • Stripe: Test card numbers (4242 4242 4242 4242)

Best Practices

  1. Security: Never expose secret keys in frontend code
  2. Error Handling: Handle all payment errors gracefully
  3. Verification: Always verify payment signatures
  4. Webhooks: Set up webhooks for payment status updates
  5. Testing: Test thoroughly in test mode before going live
  6. Logging: Log all payment transactions
  7. Refunds: Implement refund functionality
  8. Documentation: Document gateway-specific requirements

Next Steps