Shopify
QPay V2 payment app for Shopify stores. A standalone Node.js/Express application that provides QPay payment processing, QR code checkout, bank app deep links, and webhook-based payment confirmation.
- GitHub: qpay-sdk/qpay-shopify
- Demo: qpay-shopify-production.up.railway.app
- Version: 1.0.0
- License: MIT
- Marketplace: Shopify App Store (registration pending)
Requirements
| Requirement | Version |
|---|---|
| Node.js | 18.0+ |
| npm | 9.0+ |
| Shopify store | Any plan |
| Public server | For webhook callbacks |
Installation
1. Clone the Repository
git clone https://github.com/qpay-sdk/qpay-shopify.git
cd qpay-shopify2. Install Dependencies
npm installThe app depends on:
express(^4.18.0) — HTTP server and routingejs(^3.1.9) — Template engine for the payment pagedotenv(^16.3.0) — Environment variable management
3. Configure Environment
cp .env.example .envEdit .env with your credentials (see Configuration below).
4. Start the App
# Production
npm start
# Development (auto-reload)
npm run devThe app runs on the configured PORT (default: 3000).
Configuration
All configuration is managed through environment variables in the .env file:
| Variable | Description | Default |
|---|---|---|
SHOPIFY_API_KEY | Your Shopify app API key | — |
SHOPIFY_API_SECRET | Your Shopify app API secret | — |
QPAY_BASE_URL | QPay API endpoint | https://merchant.qpay.mn |
QPAY_USERNAME | QPay merchant username | — |
QPAY_PASSWORD | QPay merchant password | — |
QPAY_INVOICE_CODE | QPay merchant invoice code | — |
QPAY_CALLBACK_URL | Public URL for QPay webhook callbacks | — |
PORT | HTTP server port | 3000 |
Example .env file:
SHOPIFY_API_KEY=your_shopify_api_key
SHOPIFY_API_SECRET=your_shopify_api_secret
QPAY_BASE_URL=https://merchant.qpay.mn
QPAY_USERNAME=your_username
QPAY_PASSWORD=your_password
QPAY_INVOICE_CODE=your_invoice_code
QPAY_CALLBACK_URL=https://your-app-domain.com/webhook/qpay
PORT=3000For sandbox testing, set QPAY_BASE_URL to:
https://merchant-sandbox.qpay.mnHow It Works
Payment Flow
- Initiation: Shopify redirects the customer to your app’s
POST /payment/initiateendpoint with the order ID, amount, and customer email - Invoice Creation: The app authenticates with QPay V2 API and creates an invoice
- Payment Page: The EJS template renders a payment page with:
- A QR code image (base64-encoded PNG)
- Bank app deep links for supported Mongolian banks
- A “Waiting for payment…” status indicator
- Client-side Polling: JavaScript polls
GET /payment/check/:invoiceIdevery 3 seconds - Payment Confirmation: When payment is detected:
- The status message changes to “Payment confirmed!”
- After 1.5 seconds, the customer is redirected back to Shopify’s return URL
- Webhook: QPay also sends a server-side callback to
POST /webhook/qpayfor reliable confirmation
API Endpoints
| Method | Path | Description |
|---|---|---|
POST | /payment/initiate | Creates a QPay invoice and renders the payment page |
GET | /payment/check/:invoiceId | Checks payment status for an invoice (returns { paid: true/false }) |
POST | /webhook/qpay | Receives QPay webhook callbacks |
POST | /gdpr/customers/data_request | Shopify GDPR data request handler |
POST | /gdpr/customers/redact | Shopify GDPR customer data redaction |
POST | /gdpr/shop/redact | Shopify GDPR shop data redaction |
GET | / | Health check endpoint |
Invoice Data Mapping
| QPay Field | Source |
|---|---|
invoice_code | QPAY_INVOICE_CODE env var |
sender_invoice_no | Shopify order ID |
invoice_receiver_code | Customer email |
invoice_description | Shopify Order #<orderId> |
amount | Order amount |
callback_url | QPAY_CALLBACK_URL env var |
Webhook Setup
QPay Webhook
Configure your QPay callback URL to point to:
https://your-app-domain.com/webhook/qpayThe webhook handler receives a JSON POST body with invoice_id, verifies the payment via POST /v2/payment/check, and returns the payment status.
Shopify GDPR Webhooks
The app includes the three mandatory Shopify GDPR endpoints. Since the app does not store customer data persistently, these handlers return acknowledgment responses:
POST /gdpr/customers/data_request— Returns “No customer data stored”POST /gdpr/customers/redact— Returns “No customer data to redact”POST /gdpr/shop/redact— Returns “Shop data redacted”
Customization
Payment Page Template
The payment page is an EJS template located at src/views/payment.ejs. You can customize the design by modifying the inline CSS or adding external stylesheets. The template receives these variables:
| Variable | Type | Description |
|---|---|---|
invoice | object | Full QPay invoice response (contains qr_image, urls, invoice_id) |
orderId | string | The Shopify order ID |
checkUrl | string | URL for polling payment status |
returnUrl | string | URL to redirect to after payment |
QPay Client
The QPay API client (src/qpay-client.js) exposes two methods:
const qpay = require('./qpay-client');
// Create a QPay invoice
const invoice = await qpay.createInvoice({
invoice_code: '...',
sender_invoice_no: '...',
amount: 50000,
callback_url: '...',
});
// Check payment status
const result = await qpay.checkPayment(invoiceId);Token management is handled automatically. The access token is cached in memory and refreshed 30 seconds before expiry.
Adding Custom Routes
Add new Express routes in the src/routes/ directory and register them in src/index.js:
const customRoutes = require('./routes/custom');
app.use('/custom', customRoutes);Troubleshooting
”Cannot POST /payment/initiate” or 404 errors
- Verify the app is running and accessible at the configured port
- Check that the request body includes
orderId,amount, and optionallyemail - Ensure the Express app has JSON body parsing middleware enabled (it is by default)
QR code not displaying
- Check that the QPay credentials in
.envare correct - Verify the
QPAY_BASE_URLis reachable from your server - Check the server console for error messages during invoice creation
Webhook not receiving callbacks
- Ensure
QPAY_CALLBACK_URLis a publicly accessible HTTPS URL - For local development, use a tunneling service (ngrok, localtunnel)
- Verify the webhook endpoint responds to POST requests
Payment polling stuck on “Waiting…”
- Open the browser developer console and check for JavaScript errors
- Verify the
/payment/check/:invoiceIdendpoint returns a valid JSON response - Check that the QPay API is accessible from your server
File Structure
qpay-shopify/
├── src/
│ ├── index.js # Express app entry point, mounts routes
│ ├── config.js # Environment configuration (Shopify + QPay)
│ ├── qpay-client.js # QPay V2 API client (auth, invoice, check)
│ ├── routes/
│ │ ├── payment.js # POST /payment/initiate, GET /payment/check/:id
│ │ ├── webhook.js # POST /webhook/qpay
│ │ └── gdpr.js # Shopify GDPR mandatory endpoints
│ └── views/
│ └── payment.ejs # Payment page template (QR code + bank links)
├── package.json # Dependencies and scripts
├── .env.example # Environment variable template
└── README.mdLinks
- GitHub: https://github.com/qpay-sdk/qpay-shopify
- QPay API Reference: /api-reference
- Shopify App Development: https://shopify.dev/docs/apps