Dropme Payment Gateway
Dropme's Payment Gateway API lets you accept mobile money payments (MTN MoMo, Orange Money) from your own applications — without managing PawaPay credentials or a callback infrastructure. Dropme acts as the single certified intermediary and fans out payment results to your webhook endpoint.
The API is REST-based, returns JSON, and uses Bearer token authentication. Every endpoint is versioned under /api/v1/.
REST + JSON
Standard HTTP methods, predictable response shapes
Bearer Auth
Scoped API keys — never share credentials with clients
Webhook delivery
Terminal results POSTed to your callback URL
Base URL
All API calls are made to the following base URL:
Sandbox and production environments are distinguished by the API key prefix (dm_test_ vs dm_live_), not by a different base URL.
Authentication
Include your API key as a Bearer token in the Authorization header of every request.
curl https://dropme.cm/api/v1/payments/mobile-money/initiate \
-H "Authorization: Bearer dm_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"phone":"237670000000","amount":5000,"provider":"MTN_MOMO_CMR","currency":"XAF","country":"CMR","callbackUrl":"https://yourapp.com/webhooks/payment"}'API key types
| Prefix | Environment | Note |
|---|---|---|
| dm_live_... | Production | Real money movement — guard carefully |
| dm_test_... | Sandbox | No real transactions — safe for development |
DROPME_API_KEY) and access it only server-side.Quick Start
Accept your first mobile money payment in under 5 minutes.
Get your API key
In the Dropme admin, go to Integrations → Payment Gateway and create a client. Your API key is shown once — copy it to a secure vault.
Initiate a payment
POST to /api/v1/payments/mobile-money/initiate with the payer's phone, amount, and your callback URL.
Handle the callback
Dropme POSTs the terminal result (COMPLETED, FAILED, etc.) to your callbackUrl. Respond with HTTP 200 immediately, then process asynchronously.
const axios = require('axios');
const DROPME_KEY = 'dm_live_your_api_key_here';
const BASE_URL = 'https://dropme.cm';
async function initiatePayment({ phone, amount, provider, reference, callbackUrl }) {
const { data } = await axios.post(
`${BASE_URL}/api/v1/payments/mobile-money/initiate`,
{
phone,
amount,
currency: 'XAF',
country: 'CMR',
provider, // e.g. 'MTN_MOMO_CMR' or 'ORANGE_CMR'
externalReference: reference,
callbackUrl,
},
{ headers: { Authorization: `Bearer ${DROPME_KEY}` } }
);
return data; // { depositId, status: 'PENDING', externalReference }
}
// Usage
initiatePayment({
phone: '237670000000',
amount: 5000,
provider: 'MTN_MOMO_CMR',
reference: 'order_abc123',
callbackUrl: 'https://yourapp.com/webhooks/payment',
}).then(console.log);Initiate a Payment
Triggers a mobile money deposit request via PawaPay. Returns immediately with a depositId. The final result is delivered asynchronously to your callbackUrl.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
| phone | string | required | E.164 formatted phone number including country code. e.g. '237670000000' |
| amount | number | required | Payment amount as a positive integer (no decimals for XAF). |
| provider | string | required | Mobile money provider code. See Providers table below. |
| currency | string | optional | ISO 4217 currency code. Defaults to 'XAF'. |
| country | string | optional | ISO 3166-1 alpha-3 country code. Defaults to 'CMR'. |
| callbackUrl | string | optional | HTTPS URL where the terminal result will be POSTed. Falls back to your client's default callback URL. |
| externalReference | string | optional | Your own order or transaction ID. Echoed back in the webhook payload. |
Supported providers
| provider value | Network | Country |
|---|---|---|
| MTN_MOMO_CMR | MTN MoMo | Cameroon (CMR) |
| ORANGE_CMR | Orange Money | Cameroon (CMR) |
| MTN_MOMO_CIV | MTN MoMo | Côte d'Ivoire (CIV) |
| ORANGE_CIV | Orange Money | Côte d'Ivoire (CIV) |
| MTN_MOMO_GHA | MTN MoMo | Ghana (GHA) |
Success response 202 Accepted
{
"depositId": "3e4fc1a0-8b2d-4f5a-9c1e-7a6b8d0e2f3c",
"status": "PENDING",
"externalReference": "order_abc123",
"message": "Payment initiated. You will receive a callback when complete."
}Code examples
const axios = require('axios');
const DROPME_KEY = 'dm_live_your_api_key_here';
const BASE_URL = 'https://dropme.cm';
async function initiatePayment({ phone, amount, provider, reference, callbackUrl }) {
const { data } = await axios.post(
`${BASE_URL}/api/v1/payments/mobile-money/initiate`,
{
phone,
amount,
currency: 'XAF',
country: 'CMR',
provider, // e.g. 'MTN_MOMO_CMR' or 'ORANGE_CMR'
externalReference: reference,
callbackUrl,
},
{ headers: { Authorization: `Bearer ${DROPME_KEY}` } }
);
return data; // { depositId, status: 'PENDING', externalReference }
}
// Usage
initiatePayment({
phone: '237670000000',
amount: 5000,
provider: 'MTN_MOMO_CMR',
reference: 'order_abc123',
callbackUrl: 'https://yourapp.com/webhooks/payment',
}).then(console.log);Check Payment Status
Poll the current status of a payment. Use this as a fallback if your webhook endpoint is temporarily unavailable. Prefer webhook delivery for real-time updates.
Path parameter
| Parameter | Type | Required | Description |
|---|---|---|---|
| depositId | string (UUID) | required | The depositId returned from the initiate endpoint. |
Success response 200 OK
{
"payment": {
"id": "...",
"deposit_id": "3e4fc1a0-8b2d-4f5a-9c1e-7a6b8d0e2f3c",
"external_reference": "order_abc123",
"phone": "237670000000",
"provider": "MTN_MOMO_CMR",
"amount": "5000",
"currency": "XAF",
"status": "COMPLETED",
"callback_status": "delivered",
"created_at": "2025-06-15T07:00:00.000Z",
"updated_at": "2025-06-15T07:02:14.000Z"
}
}Code examples
async function checkPayment(depositId) {
const { data } = await axios.get(
`${BASE_URL}/api/v1/payments/mobile-money/${depositId}`,
{ headers: { Authorization: `Bearer ${DROPME_KEY}` } }
);
return data.payment; // { depositId, status, amount, currency, phone, ... }
}
const payment = await checkPayment('3e4fc1a0-...');
if (payment.status === 'COMPLETED') {
// Fulfil order
}Webhooks
When a payment reaches a terminal state, Dropme sends a POST request to your registered callback URL. Callbacks are only sent for terminal statuses — never for intermediate states like PENDING.
Payload schema
{
"depositId": "3e4fc1a0-8b2d-4f5a-9c1e-7a6b8d0e2f3c",
"status": "COMPLETED", // or FAILED, CANCELED, TIMEOUT, REJECTED
"amount": "5000",
"currency": "XAF",
"providerTransactionId": "PP-...", // PawaPay reference, may be null on failure
"payer": { "type": "MSISDN", "address": { "value": "237670000000" } },
"timestamp": "2025-06-15T07:02:14.000Z"
}Terminal statuses
| Status | Meaning | Action |
|---|---|---|
| COMPLETED | Payment succeeded, funds transferred | Fulfil order / credit account |
| FAILED | Payment failed at the operator level | Notify user, release holds |
| CANCELED | Payment was explicitly cancelled | Notify user |
| TIMEOUT | Payer did not approve within the time limit | Prompt user to retry |
| REJECTED | Operator rejected the request | Log and notify support |
Best practices
- •Respond with HTTP 200 immediately before processing. Dropme marks delivery as failed after 10 seconds.
- •Make your webhook handler idempotent — the same depositId may be delivered more than once.
- •Process the actual business logic asynchronously (background job / queue).
- •Verify the depositId exists in your own database before acting on it.
- •Dropme retries failed deliveries with exponential backoff.
Handler examples
// Express.js webhook handler
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhooks/payment', (req, res) => {
// Always respond 200 immediately — retries use exponential backoff
res.status(200).json({ received: true });
const { depositId, status, amount, currency, externalReference } = req.body;
if (status === 'COMPLETED') {
// Fulfil the order or credit the user
fulfillOrder(externalReference, { depositId, amount, currency });
} else if (['FAILED', 'CANCELED', 'TIMEOUT', 'REJECTED'].includes(status)) {
// Notify the user and release any holds
handleFailedPayment(externalReference, status);
}
// 'PENDING' callbacks are NOT sent — only terminal statuses are delivered
});
app.listen(3000);Error Reference
All error responses follow a consistent JSON shape:
{
"error": "Human-readable error message"
}HTTP status codes
| Code | Meaning | Common cause |
|---|---|---|
| 200 / 202 | Success | Request processed successfully |
| 400 | Bad Request | Missing required fields or invalid values |
| 401 | Unauthorized | Missing, malformed, or invalid API key |
| 403 | Forbidden | API key suspended or scope not permitted |
| 404 | Not Found | depositId does not belong to this client |
| 422 | Unprocessable | PawaPay rejected the deposit (e.g. invalid provider) |
| 429 | Rate Limited | Exceeded requests/minute for this API key |
| 500 | Internal Error | Upstream PawaPay error or platform issue. Retry with backoff. |
Rate Limits & Testing
Each API key has a configurable rate limit set by the Dropme admin (default: 60 requests/minute). Exceeding the limit returns 429 Too Many Requests.
Testing in sandbox
Use a dm_test_ key for all development. Sandbox keys hit the PawaPay sandbox environment — no real money moves. Your webhook endpoint will still be called with the real callback payload, so you can test your full integration end-to-end.
SDK / library support
No official SDK is required — the API is a plain REST interface. Any HTTP client works. The code examples on this page are ready to copy directly into your project.