BML Connect Integration
BML Connect is the Bank of Maldives merchant payment platform — the primary way to accept online payments in the Maldives. This skill contains everything needed to integrate with the BML Connect API.
Authentication
Every API call uses a static API key in the Authorization header. No OAuth, no token exchange, no expiry.
Authorization: YOUR_SECRET_API_KEY
Two types of API keys exist per app:
- API Key (secret) — long JWT-like string. Used for all server-to-server calls. Never expose publicly.
- API Key Public —
pk_prefixed. Only for client-side card tokenization (PomeloJS). Not needed for server-side operations.
The merchant also has an Application ID (UUID format) which may be needed for some SDK-based calls.
Environments
| Environment | Base URL | Dashboard |
|---|---|---|
| Production | https://api.merchants.bankofmaldives.com.mv/public | dashboard.merchants.bankofmaldives.com.mv |
| Sandbox (UAT) | https://api.uat.merchants.bankofmaldives.com.mv/public | dashboard.uat.merchants.bankofmaldives.com.mv |
Default to sandbox when writing integration code unless the user specifies production. This prevents accidental real charges during development.
API Endpoints Quick Reference
| Operation | Method | Endpoint |
|---|---|---|
| Create transaction | POST | /public/v2/transactions |
| Get transaction | GET | /public/v2/transactions/{id} |
| Update transaction | PATCH | /public/v2/transactions/{id} |
| Send payment SMS | POST | /public/transactions/{id}/sms |
| Send payment Email | POST | /public/transactions/{id}/email |
| Create customer | POST | /public-customers |
| Get customer | GET | /public-customers/{id} |
| List customers | GET | /public-customers |
| Get customer tokens | GET | /public-customers/{id}/tokens |
| Charge token | POST | /public-customers/charge |
| Create shop | POST | /public/shops |
| Get shops | GET | /public/shops |
| Update shop | PATCH | /public/shops/{id} |
Critical URL quirk: Customer endpoints use /public-customers (dash), not /public/customers (slash). This is the most common integration mistake — getting this wrong produces confusing 404 errors.
Creating a Transaction
This is the most common operation. Send amount in laari (smallest currency unit — 100 laari = MVR 1.00).
POST /public/v2/transactions
{
"amount": 15000,
"currency": "MVR",
"localId": "your-internal-id",
"customerReference": "Human-readable description",
"webhook": "https://your-domain.com/webhooks/bml",
"redirectUrl": "https://your-domain.com/payment/complete"
}
Response contains:
id— BML transaction IDurl— full payment page URL (redirect the customer here or embed in iframe)shortUrl— short URL (useful for SMS)qr.url— QR code image URLstate—QR_CODE_GENERATEDinitially
The localId field is your internal reference — use it to match BML transactions back to your orders/invoices.
Transaction States
| State | Meaning |
|---|---|
QR_CODE_GENERATED | Transaction created, QR ready, awaiting payment |
CONFIRMED | Payment received — money is in |
CANCELLED | Transaction cancelled |
FAILED | Payment failed |
EXPIRED | Transaction timed out |
Only CONFIRMED means money received. Always verify via webhook or polling before marking anything as paid. Never trust client-side redirects alone — a user can manipulate the redirect URL.
Payment Methods Supported
- Card (Visa/Mastercard via MPGS) — customer scans QR or opens link, enters card details
- BML MobilePay — customer scans QR in MobilePay app
- Alipay / WeChat Pay / UnionPay — tourist payments
- Apple Pay / Google Pay — via QR flow
Known limitation: MIB (Maldives Islamic Bank) customers cannot pay through BML Connect. MIB transfers must be handled manually outside of BML Connect. If the user's product serves MIB customers, suggest adding a manual bank transfer option alongside BML Connect.
Webhook Handling
BML sends a POST request to your webhook URL when a transaction state changes. This is the most reliable way to confirm payments.
Webhook Signature Verification
BML sends three headers for signature verification:
X-Signature-NonceX-Signature-TimestampX-Signature
Verify by generating sha256(nonce + timestamp + apiKey) and comparing to the signature header using a timing-safe comparison (to prevent timing attacks):
generated = sha256(nonce + timestamp + your_api_key)
valid = timing_safe_equals(generated, X-Signature)
Use the language-appropriate timing-safe comparison:
- Node.js:
crypto.timingSafeEqual() - Python:
hmac.compare_digest() - PHP:
hash_equals() - Go:
subtle.ConstantTimeCompare()
Webhook Event Types
| Event | When it fires |
|---|---|
NOTIFY_TRANSACTION_CHANGE | Any state change on a transaction |
NOTIFY_TOKENISATION_STATUS | Card saved successfully (tokenization) |
Webhook Payload
The payload includes eventType, state, transactionId, and other transaction details. Only process CONFIRMED state to mark payments as complete.
Important: Your webhook endpoint must be excluded from CSRF protection. BML cannot send a CSRF token. Framework-specific examples:
- Laravel: Add the route to
$exceptinVerifyCsrfTokenmiddleware - Express: Use
express.raw()orexpress.json()on the webhook route specifically - Django: Use
@csrf_exemptdecorator
Webhook Best Practices
- Return 200 immediately, process asynchronously — BML may retry on slow responses
- Make webhook processing idempotent — you may receive the same event multiple times
- Always verify the signature before trusting the payload
- Log the raw payload for debugging
Sending Payment Links
Via SMS
POST /public/transactions/{transactionId}/sms
{
"phoneNumber": "9607771234"
}
Via Email
POST /public/transactions/{transactionId}/email
{
"email": "customer@example.com"
}
Known issue: SMS and Email sending may fail with AWS authentication errors on BML's side. This is a BML infrastructure issue, not your code. If this happens, use the shortUrl from the transaction response and send it yourself via your own SMS/email provider. Always implement this fallback — don't rely solely on BML's SMS/email.
Tokenization (Card-on-File)
BML supports saving customer cards for one-click future payments.
Flow
- Create a customer in BML's system first
- First payment: Create transaction with
tokenizationDetails— BML saves card, returns token - Future payments: Call charge token endpoint with
customerId+tokenId— charged instantly, no card re-entry
Create Customer
POST /public-customers
{
"name": "Customer Name",
"email": "customer@example.com",
"phone": "9607771234"
}
First Payment (Tokenize)
POST /public/v2/transactions
{
"amount": 15000,
"currency": "MVR",
"customerId": "bml-customer-id",
"tokenizationDetails": {
"tokenize": true,
"paymentType": "UNSCHEDULED",
"recurringFrequency": "UNSCHEDULED"
},
"webhook": "https://your-domain.com/webhooks/bml"
}
Charge Saved Card
POST /public-customers/charge
{
"customerId": "bml-customer-id",
"transactionId": "new-transaction-id",
"tokenId": "saved-card-token-id"
}
Note: You must first create a new transaction (to get a transactionId), then charge the token against it. The token charge is a two-step process.
Webhook event NOTIFY_TOKENISATION_STATUS fires when a card is saved successfully.
Money Handling
- Currency: MVR (Maldivian Rufiyaa)
- Smallest unit: laari (100 laari = MVR 1.00)
- Always send amounts to BML in laari (integer). MVR 150.00 = 15000.
- **Never use floating point for mone