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
- Security: Never expose secret keys in frontend code
- Error Handling: Handle all payment errors gracefully
- Verification: Always verify payment signatures
- Webhooks: Set up webhooks for payment status updates
- Testing: Test thoroughly in test mode before going live
- Logging: Log all payment transactions
- Refunds: Implement refund functionality
- Documentation: Document gateway-specific requirements
Next Steps
- App Structure - Learn app organization
- Hooks - Explore available hooks
- Assets - Manage app assets