1. Introduction
Welcome to our BNPL integration process. This guide provides comprehensive instructions for merchants to integrate with QPay after signing the agreement.
Important: Before starting, ensure you have received your merchant agreement confirmation email.
2. Integration Process Overview
- Agreement Signing: Merchant signs agreement and requests integration.
- Documentation: We share this guide and sample certificates.
- Certificate Generation: Merchant generates and shares certificates following our security standards.
- Environment Setup: We register certificates in UAT and Production environments.
- Access Provision: We provide x-merchant-id, verified phone number, service ID, API documentation, Postman collection, and environment URLs.
Note: The entire process typically takes 1-3 business days after certificate submission.
3. Certificate Generation
Generate your security certificates using the following OpenSSL commands:
# Step 1: Generate Private Key (2048-bit RSA) openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048 # Step 2: Generate Certificate Signing Request openssl req -new -key private_key.pem -out request.csr \ -subj "/C=US/ST=State/L=City/O=YourCompany/CN=yourdomain.com" # Step 3: Generate Public Certificate (Valid for 2 years) openssl x509 -req -in request.csr -signkey private_key.pem -out public_cert.pem \ -days 730 -sha256
Certificate Requirements
- 2048-bit RSA minimum
- SHA-256 hashing algorithm
- 2-year validity period
- Proper subject fields (Country, Organization, CN)
Important Notes
- Keep private key secure
- Do not regenerate unless compromised
- Notify us before certificate expiration
- Test certificates in UAT first
4. After Certificate Submission
After we receive your certificates, you'll receive the following within 2 business days:
Merchant Credentials
- x-merchant-id (e.g. "qpay")
- Verified mobile number for OTP
- Merchant Service ID
Development Resources
- Postman Collection
- Sample Code in 5 languages
- Mock API endpoints
Environment Setup
- UAT access confirmation
- Production onboarding
- IP whitelisting instructions
Pro Tip: Begin testing in our UAT environment immediately after receiving credentials. Production access requires additional verification.
5. API Documentation & Endpoints
Download our comprehensive API documentation:
Download API Docs (PDF)| API | Description | UAT | Production |
|---|---|---|---|
| List Plans | Retrieve available payment plans | /bnpl/mw/list-plans | /bnpl/mw/list-plans |
| List Cards | Get customer's saved payment methods | /bnpl/mw/list-card | /bnpl/mw/list-card |
| Generate OTP | Initiate OTP for transaction | /bnpl/mw/generate-otp | /bnpl/mw/generate-otp |
| Confirm Plan | Finalize payment plan selection | /bnpl/mw/confirm-plan | /bnpl/mw/confirm-plan |
| Refund | Initiate refund for a BNPL transaction | /bnpl/mw/bnpl-refund | /bnpl/mw/bnpl-refund |
Base URLs
UAT Environment:
https://bnpl-api.fintec.solutions
Production Environment:
https://bnpl-api-prod.fintec.solutions
Refund API - Detailed Description
Endpoint: POST {base_url}/bnpl/mw/bnpl-refund
Use this API to initiate a refund for an existing BNPL transaction. The refund amount must match the amount in the original transaction.
Request Payload Structure
{
"senderInfo": {
"lang": 1,
"sender": "{{merchantServiceId}}", // ID of the Merchant's Service
"senderType": "C",
"msgId": "{{msgId}}", // ID of the message, must be unique
"deviceId": "{{deviceId}}", // ID of the Device
"otp": null
},
"refundInfo": {
"amount": {{refundAmount}}, // Amount of refund
"loanId": {{loanId}} // ID of the loan
}
}
Important Notes
- The
msgIdmust be unique for every refund request. Reusing the samemsgIdwill result in error code (Message Is Duplicated). - Refund amount must match the amount in the original loan.
- Ensure the
loanIdcorresponds to a valid, existing loan in the system.
Example Request
curl -X POST "https://bnpl-api.fintec.solutions/bnpl/mw/bnpl-refund" \
-H "Content-Type: application/json" \
-H "x-Merchant-ID: your-merchant-id" \
-H "token: <base64-rsa-sha256-signature>" \
-d '{
"senderInfo": {
"lang": 1,
"sender": "060004100xxxxxxx",
"senderType": "C",
"msgId": "refund-2024-001-uuid-12345",
"deviceId": "454",
"otp": null
},
"refundInfo": {
"amount": "50.00",
"loanId": 12345678
}
}'
Remember to generate the token header by signing the exact JSON payload using your private key with RSA-SHA256 and Base64 encoding.
Expected Response
A successful refund request returns the following response structure:
{
"responseInfo": {
"errorCd": "00",
"desc": "Success",
"statusCode": "1",
"loanId": null,
"failureReasons": null
}
}
6. Request Headers & Authentication
All API requests must include the following headers:
Content-Type: application/json x-Merchant-ID: <your-unique-merchant-identifier> token: <base64-rsa-sha256-signature>
Header Requirements
x-Merchant-IDprovided after registrationtokenis regenerated per request (RSA–SHA256, Base64)- All headers are case-sensitive
Request Timing
- Clock skew tolerance: ±2 minutes
- Rate limit: 30 requests/minute
- Timeout: 60+ seconds
7. Signature Generation
Generate request signatures using your private key with the following exact implementation:
Required Headers
Content-Type: application/json x-Merchant-ID: <your-merchant-id-from-config> token: <generated-signature>
/** * Generates a base64-encoded signature for API requests * @param array $value Request payload * @return string Base64-encoded signature * @throws Exception On signing failure */ public function sign(array $value): string { try { $privateKey = $this->loadMerchantPrivateKey(); // Step 1: Convert JSON to UTF-8 bytes $utf8Bytes = json_encode($value, JSON_UNESCAPED_UNICODE); // Step 2: Sign using RSA private key with SHA256 $signature = ''; $success = openssl_sign($utf8Bytes, $signature, $privateKey, OPENSSL_ALGO_SHA256); if (!$success) { $error = openssl_error_string(); throw new Exception('Failed to sign hash: ' . $error); } openssl_free_key($privateKey); // Step 3: Encode result into Base64 string $base64Signature = base64_encode($signature); Log::info('QPay Security Service - Data signed successfully', [ 'algorithm' => OPENSSL_ALGO_SHA256, 'data_length' => strlen($utf8Bytes), 'signature_length' => strlen($signature), 'base64_length' => strlen($base64Signature) ]); return $base64Signature; } catch (Exception $e) { Log::error('QPay Security Service - Sign failed', [ 'error' => $e->getMessage() ]); throw new Exception('Failed to sign data: ' . $e->getMessage()); } }
Request Example (JSON)
Basic{
"senderInfo": {
"lang": 1,
"sender": "00968********",
"senderType": "M",
"msgId": "uniqueMSG",
"deviceId": "454"
},
"amount": "amount",
"messageId": "merchant-service-id"
}
Sign the exact JSON string (same order/whitespace) to avoid signature mismatches.
Request Example with Hashed OTP
Hashed OTP
OTP: 123456 → SHA-256 (Base64):
jZae727K08KaOmKSgOaGzww/XVqGr/PKEgIMkjrcbJI=
{
"senderInfo": {
"lang": 1,
"sender": "00968********",
"senderType": "M",
"deviceId": "454",
"msgId": "uniqueMSGid",
"otp": "jZae727K08KaOmKSgOaGzww/XVqGr/PKEgIMkjrcbJI="
}
}
Hash the plaintext OTP with SHA-256 and encode in Base64. Use the same string for signing and sending.
Expected Signature (Sample)
j0CZl8+Sj9mDZgTkQZ+MzKNF1o7u4rwiXouxmsYXbUKwYWfWdI/xkiLQkuCO+jWXs+TmgNX1dtulVPvMTJxT5AI0b9vqJAr9iDMYNjv4N1XQPVQvSyTe50eczcdNmLan/6vy371gc32Q+5ERgAI41Is29VKDlyEIkkjZV8lDp6MLrACDo8/N8nvoVdHQpTY0EEbRyMw/X9ri+5JKVu2JejNMeIEewcapsflCcpOFitBiITPXiltRjP4fpRtwajgKIC1vo3nvYaC2xe6Y0Mx+X4WBPpV7t+aJm4NQdMADVaFjuB82BNMV53vNnB28e5vFgbczPkM7swnDfiRxtyf1Uw==
Sample only. Your signature changes with the exact JSON and private key.
8. Testing & Go-Live Checklist
Download our comprehensive Postman Collections:
Quick Signing & Hashing (Base64)
Use these browser tools for ad-hoc validation (for production, sign server-side using your private key):
- Prepare your JSON request (see “Request Example” above) as a single string without extra spaces/line breaks.
- Open RSA Signing Tool, choose algorithm RSASSA-PKCS1-v1_5 with SHA-256, paste your private key (PEM) locally, paste the JSON string as the message, and ensure the output is Base64.
- Copy the Base64 signature and set it as the
tokenheader. Ensurex-Merchant-IDis set correctly. - (Optional) Use the SHA-256 Hashing Tool to compute the digest of your JSON string to compare against server-side logs.
cURL Example
curl -X POST "https://bnpl-api-uat.fintec.solutions/bnpl/mw/confirm-plan" \
-H "Content-Type: application/json" \
-H "x-Merchant-ID: <your-merchant-id>" \
-H "token: <base64-rsa-sha256-signature>" \
-d '{"senderInfo":{"lang":1,"sender":"00968********","senderType":"M","msgId":"uniqueMSG","deviceId":"454"},"amount":"1.000", "messageId": "0600041004001440"}'
Replace placeholders with your real values. Keep JSON exactly the same when generating the signature and when sending the request.
Integration Testing Steps
- UAT Environment Setup: Configure your system with UAT credentials
- Basic Connectivity: Test authentication and token generation
- API Validation: Verify all endpoints with sample data
- Error Handling: Test with invalid inputs and error conditions
- End-to-End Flow: Complete full transaction lifecycle
- Performance Testing: Verify under expected load
Go-Live Requirements
Mandatory Items
- Successful UAT testing report
- Production certificates submitted
- IP addresses whitelisted
- Fallback mechanism implemented
Recommended Checks
- Load testing completed
- Monitoring configured
- Team training conducted
- Support contacts verified
Common Pitfalls: wrong x-Merchant-ID value, JSON reformatting after signing, expired token, or clock drift > 2 minutes.
9. Error Codes & Troubleshooting
Below is a subset of common errors. Refer to the full API document for the complete list.
| Code | HTTP | Message | How to Resolve |
|---|---|---|---|
| INVALID_SIGNATURE | 401 | Signature verification failed | Sign the exact JSON; ensure Base64 output; use the correct private key; check clock skew. |
| MISSING_HEADER | 400 | Required header is missing | Include x-Merchant-ID, Content-Type, and token headers. |
| TOKEN_EXPIRED | 401 | Token is older than allowed window | Re-generate signature within 5 minutes of the request. |
| REQUEST_VALIDATION | 400 | Payload failed validation | Fix field formats/required fields; avoid conflicting parameters. |
| RATE_LIMIT_EXCEEDED | 429 | Too many requests | Throttle to ≤ 30 requests/minute. |
| UNAUTHORIZED_MERCHANT | 403 | Merchant not allowed for this operation | Check merchant status, IP whitelist, and environment (UAT vs PROD). |
| OTP_INVALID | 400 | Invalid/expired OTP | Request a new OTP and retry within the valid time window. |
| PLAN_NOT_AVAILABLE | 409 | Selected plan no longer available | Re-query List Plans and re-select. |
| 46 (Message Is Duplicated) | — | Message Is Duplicated |
Ensure msgId is unique for every request (e.g., UUIDv4 or
a timestamp+nonce). Do not reuse the same msgId across requests.
|
| 49 (Invalid Credentials) | — | Invalid Credentials |
Verify x-Merchant-ID, regenerate token (RSA-SHA256, Base64) over the exact JSON,
check key/cert pairing and environment (UAT vs PROD), and confirm token is within 5 minutes.
|
Provider Error Payload Examples
Duplicate msgId used in more than one request:
{
"responseInfo": {
"errorCd": "46",
"desc": "Message Is Duplicated",
"statusCode": "3"
}
}
Problem with signing or x-Merchant-ID:
{
"responseInfo": {
"errorCd": "49",
"desc": "Invalid Credentials",
"statusCode": "3"
}
}
10. Support & Contact Information
Technical Support
-
Email Support
alwarith.s@fintec.solutions
mohammed.alajmi@fintec.solutions
Response time: 2 business hours
-
Emergency Phone
+968 2416 2616
Integration Assistance
-
Office Hours
Sunday-Thursday, 9AM-5PM GST
Dedicated integration support
Support Guidelines
- Always include your x-merchant-id in communications
- For API issues, provide request/response samples
- Business inquiries should use separate channels