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 |
Base URLs
UAT Environment:
https://bnpl-api-uat.fintec.solutions
Production Environment:
https://bnpl-api.fintec.solutions
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-ID
provided after registrationtoken
is regenerated per request (RSA–SHA256, Base64)- All headers are case-sensitive
Request Timing
- Token valid for 5 minutes
- Clock skew tolerance: ±2 minutes
- Rate limit: 30 requests/minute
- Timeout: 30 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" }
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 Collection:
Postman Collection (JSON)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
token
header. Ensurex-Merchant-ID
is 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"}'
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