MarkupPay Gateway API
Mobile money payment gateway for African markets. Accept payments, send payouts, issue refunds, and query your account across multiple mobile money networks. All requests are authenticated with an API key and an HMAC-SHA256 signature.
Base URL
https://api-gateway.markuppay.comSandbox lives at https://sandbox.api.markuppay.com with pk_test_ keys.
What’s in here
Payments
3 endpointsCharge a customer's mobile money wallet or card, then track the result to a final state. Card payments run through a hosted 3-D Secure page, so you never handle card numbers. Payments are idempotent on your reference and settle to your account balance.
Payouts & transfers
3 endpointsPay money out of your settlement balance to a mobile money wallet — disbursements, supplier payments, and manual settlements. You specify only the beneficiary.
Refunds
2 endpointsReturn funds from a completed payment to the customer, in full or in part. Refunds move through the same status lifecycle as payments.
Account
2 endpointsRead-only account endpoints: available and pending settlement balances by currency, and the mobile money networks your account can use.
Support
Questions about this API? Reach MarkupPay Support at api-support@markuppay.com.
Quick start
# Charge a customer's mobile money wallet
curl -X POST 'https://api-gateway.markuppay.com/api/v1/payments' \
-H 'X-Api-Key: pk_test_xxx' \
-H 'X-Timestamp: 1738800000' \
-H 'X-Signature: v1=abc123...' \
-H 'Content-Type: application/json' \
-d '{
"amount": 10000,
"currency": "GHS",
"phone": "233241234567",
"network": "MTN",
"reference": "ord_12345"
}'Authentication
Every request carries three headers: X-Api-Key (your public key), X-Timestamp (Unix seconds), and X-Signature (an HMAC-SHA256 signature prefixed with v1=).
The signature is computed over the request method, path, timestamp, and a SHA-256 hash of the body, keyed by your API secret:
HMAC_SHA256(secret, METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + SHA256(BODY))Timestamps more than a few minutes old are rejected, so keep your server clock in sync with NTP. A mismatched signature returns 401 with an INVALID_SIGNATURE error.
Sign a request (Node.js)
const crypto = require('crypto');
function signRequest(method, path, body, apiKey, apiSecret) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const bodyHash = crypto.createHash('sha256')
.update(body || '')
.digest('hex');
const payload = [method, path, timestamp, bodyHash].join('\n');
const signature = crypto.createHmac('sha256', apiSecret)
.update(payload)
.digest('hex');
return {
'X-Api-Key': apiKey,
'X-Timestamp': timestamp,
'X-Signature': 'v1=' + signature,
};
}Conventions
Request and response bodies are JSON encoded in UTF-8. Send Content-Type: application/json on any request that carries a body.
Amounts are integers in minor units — the currency’s smallest unit (pesewas for GHS, cents for USD). Charge GHS 100.00 by sending 10000; GHS 1.50 is 150.
Every response is wrapped in an envelope. Success returns { "success": true, "data": … }; failures return { "success": false, "error": … }. Treat the HTTP status code as the source of truth and read data or error for detail.
Idempotency. Reuse the same reference to safely retry a create request without double-charging — if we have already seen it, we return the original transaction instead of creating a new one.
Status lifecycle. A transaction moves through pending → processing → completed, or ends in failed / cancelled. Treat anything other than a final state as not yet settled. Track results with a callback_url webhook or by polling the transaction by id.
Card payments
Cards are a payment method on the same endpoint as mobile money — send method: "card" to POST /api/v1/payments, along with a customer_email and a return_url. MarkupPay supports Visa and Mastercard.
You never handle card numbers. The card is entered on a MarkupPay-hosted 3-D Secure page, which keeps your integration out of PCI scope. The create response comes back pending with a next_action object.
The 3-D Secure flow
- Create the payment with
method: "card". The response ispendingand carriesnext_action.redirect_url. - Redirect the customer to that URL to authenticate with their bank.
- The customer returns to your
return_url. The card is now charged — but treat the redirect as a hint, not proof. - Confirm the final state from the
payment.completed/payment.failedwebhook, or by pollingGET /api/v1/payments/{id}.
Start a card payment
curl -X POST 'https://api-gateway.markuppay.com/api/v1/payments' \
-H 'X-Api-Key: pk_test_xxx' \
-H 'X-Timestamp: 1738800000' \
-H 'X-Signature: v1=abc123...' \
-H 'Content-Type: application/json' \
-d '{
"amount": 10000,
"currency": "GHS",
"method": "card",
"customer_email": "customer@example.com",
"return_url": "https://example.com/checkout/complete",
"reference": "ord_12345"
}'Response · 200 (redirect the customer)
{
"success": true,
"data": {
"id": "pay_abc123",
"status": "pending",
"method": "card",
"amount": 10000,
"currency": "GHS",
"next_action": {
"type": "redirect",
"redirect_url": "https://checkout.markuppay.com/3ds/pay_abc123"
}
}
}Webhooks
Rather than poll GET /api/v1/payments/{id} for a status change, pass a callback_url when you create a payment or payout. When the transaction reaches a final state, MarkupPay POSTs a WebhookEvent to that URL.
Verify every delivery. Each request carries an X-MarkupPay-Signature header — an HMAC-SHA256 of the raw request body keyed by your webhook signing secret (whsec_…), prefixed with v1=. Reject the request if it does not match.
Respond with any 2xx within a few seconds to acknowledge receipt. Non-2xx responses and timeouts are retried with exponential backoff, so make your handler idempotent — key off the transaction id.
Event types
payment.completedA payment (mobile money or card) succeeded.
payment.failedA payment failed or was cancelled.
payout.completedA transfer/payout succeeded.
payout.failedA transfer/payout failed.
refund.completedA refund was processed.
Webhook payload
{
"event": "payment.completed",
"data": {
"id": "pay_abc123",
"reference": "ord_12345",
"type": "payment",
"status": "completed",
"method": "card",
"amount": 10000,
"currency": "GHS",
"fee": 250,
"net_amount": 9750,
"created_at": "2026-06-25T10:15:00Z"
}
}Verify the signature (Node.js)
const crypto = require('crypto');
function verifyWebhook(rawBody, signatureHeader, signingSecret) {
const expected = 'v1=' + crypto
.createHmac('sha256', signingSecret) // whsec_...
.update(rawBody)
.digest('hex');
const a = Buffer.from(signatureHeader);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Errors
Failures return an ErrorResponse with a machine-readable code and a human-readable message. The most common authentication errors are below.
MISSING_HEADERSHeaders missingOne of the three required auth headers was not sent. Add X-Api-Key, X-Timestamp, and X-Signature.
TIMESTAMP_EXPIREDClock driftThe X-Timestamp is too far from server time. Sync your clock with NTP.
INVALID_SIGNATUREBad signatureThe signature did not match. Recheck the signing string and your API secret.
INSUFFICIENT_PERMISSIONSNo scopeYour key lacks permission for this operation. Request additional scopes.
Error shape
{
"success": false,
"error": {
"code": "INVALID_SIGNATURE",
"message": "Signature validation failed"
}
}List payments
/api/v1/paymentsReturn a paginated list of payments, most recent first. Filter by status with the `status` query parameter.
Query parameters
statusstringoptionalOnly return payments in this state.
limitintegeroptionalMaximum number of records to return (1–100).
offsetintegeroptionalNumber of records to skip for pagination.
Responses
successbooleanoptionaldataPayment[]optionalpaginationPaginationoptionalRequest
curl -X GET 'https://api-gateway.markuppay.com/api/v1/payments' \
-H 'X-Api-Key: pk_test_xxx' \
-H 'X-Timestamp: 1738800000' \
-H 'X-Signature: v1=abc123...' \
-H 'Accept: application/json'Response · 200
{
"success": true,
"data": [
{
"id": "pay_abc123",
"reference": "ord_12345",
"type": "payment",
"status": "completed",
"method": "card",
"amount": 10000,
"currency": "GHS",
"network": "MTN",
"phone": "233241234567",
"card": {
"scheme": "Visa",
"last4": "4242",
"exp_month": "08",
"exp_year": "27"
},
"next_action": {
"type": "redirect",
"redirect_url": "https://checkout.markuppay.com/3ds/pay_abc123"
},
"fee": 250,
"net_amount": 9750,
"customer_name": "Kwame Mensah",
"description": "Order #12345",
"created_at": "2026-06-25T10:15:00Z"
}
],
"pagination": {
"limit": 20,
"offset": 0,
"total": 42
}
}Create a payment
/api/v1/paymentsCharge a customer's mobile money wallet. Returns a payment in the `pending` state; the final result arrives via webhook or by polling the payment by id. Reuse the same `reference` to safely retry without double-charging.
Request body
Payment details.
amountintegerrequiredAmount in the currency's smallest unit (pesewas for GHS, cents for USD).
currencystringrequiredISO 4217 currency code.
methodstringrequiredHow the customer pays. `mobile_money` debits a wallet directly; `card` returns a hosted 3-D Secure page to redirect the customer to.
phonestringoptionalCustomer wallet number in international format. Required for `mobile_money`.
networkstringoptionalMobile money network code. Required for `mobile_money`; resolved from the phone number when omitted.
customer_emailstring · emailoptionalCustomer email. Required for `card` payments — the receipt is sent here.
customer_namestringoptionalCustomer name, shown on the hosted card page.
return_urlstring · urioptionalWhere to send the customer after they complete 3-D Secure. Required for `card` payments.
referencestringrequiredYour unique reference for the transaction. Used for idempotency.
descriptionstringoptionalFree-text description shown to you for reconciliation.
callback_urlstring · urioptionalURL to POST status updates to for this transaction.
Responses
successbooleanoptionaldataPaymentoptionalRequest
curl -X POST 'https://api-gateway.markuppay.com/api/v1/payments' \
-H 'X-Api-Key: pk_test_xxx' \
-H 'X-Timestamp: 1738800000' \
-H 'X-Signature: v1=abc123...' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"amount": 10000,
"currency": "GHS",
"method": "mobile_money",
"phone": "233241234567",
"network": "MTN",
"customer_email": "customer@example.com",
"customer_name": "Kwame Mensah",
"return_url": "https://example.com/checkout/complete",
"reference": "ord_12345",
"description": "Order #12345",
"callback_url": "https://example.com/webhooks/markuppay"
}'Response · 200
{
"success": true,
"data": {
"id": "pay_abc123",
"reference": "ord_12345",
"type": "payment",
"status": "completed",
"method": "card",
"amount": 10000,
"currency": "GHS",
"network": "MTN",
"phone": "233241234567",
"card": {
"scheme": "Visa",
"last4": "4242",
"exp_month": "08",
"exp_year": "27"
},
"next_action": {
"type": "redirect",
"redirect_url": "https://checkout.markuppay.com/3ds/pay_abc123"
},
"fee": 250,
"net_amount": 9750,
"customer_name": "Kwame Mensah",
"description": "Order #12345",
"created_at": "2026-06-25T10:15:00Z"
}
}Get a payment
/api/v1/payments/{id}Fetch a single payment by its id. Poll this endpoint until `status` is `completed` or `failed` when you are not relying on webhooks.
Path parameters
idstringrequiredThe payment id, e.g. `pay_abc123`.
Responses
successbooleanoptionaldataPaymentoptionalRequest
curl -X GET 'https://api-gateway.markuppay.com/api/v1/payments/pay_abc123' \
-H 'X-Api-Key: pk_test_xxx' \
-H 'X-Timestamp: 1738800000' \
-H 'X-Signature: v1=abc123...' \
-H 'Accept: application/json'Response · 200
{
"success": true,
"data": {
"id": "pay_abc123",
"reference": "ord_12345",
"type": "payment",
"status": "completed",
"method": "card",
"amount": 10000,
"currency": "GHS",
"network": "MTN",
"phone": "233241234567",
"card": {
"scheme": "Visa",
"last4": "4242",
"exp_month": "08",
"exp_year": "27"
},
"next_action": {
"type": "redirect",
"redirect_url": "https://checkout.markuppay.com/3ds/pay_abc123"
},
"fee": 250,
"net_amount": 9750,
"customer_name": "Kwame Mensah",
"description": "Order #12345",
"created_at": "2026-06-25T10:15:00Z"
}
}List transfers
/api/v1/transfersReturn a paginated list of payouts, most recent first.
Query parameters
statusstringoptionalOnly return transfers in this state.
limitintegeroptionalMaximum number of records to return (1–100).
offsetintegeroptionalNumber of records to skip for pagination.
Responses
successbooleanoptionaldataTransfer[]optionalpaginationPaginationoptionalRequest
curl -X GET 'https://api-gateway.markuppay.com/api/v1/transfers' \
-H 'X-Api-Key: pk_test_xxx' \
-H 'X-Timestamp: 1738800000' \
-H 'X-Signature: v1=abc123...' \
-H 'Accept: application/json'Response · 200
{
"success": true,
"data": [
{
"id": "txf_abc123",
"reference": "payout_2026_0001",
"type": "payout",
"status": "completed",
"amount": 50000,
"currency": "GHS",
"network": "MTN",
"beneficiary_name": "Ama Owusu",
"beneficiary_account": "233201112222",
"fee": 100,
"net_amount": 49900,
"created_at": "2026-06-25T11:00:00Z"
}
],
"pagination": {
"limit": 20,
"offset": 0,
"total": 42
}
}Create a payout
/api/v1/transfersSend money from your settlement balance to a mobile money wallet. You specify only the beneficiary; the sender is your MarkupPay account. Track the result via webhook or by polling the transfer by id.
Request body
Payout details.
amountintegerrequiredAmount in the currency's smallest unit.
currencystringrequiredISO 4217 currency code.
networkstringrequiredMobile money network code.
beneficiary_namestringrequiredName of the recipient.
beneficiary_accountstringrequiredRecipient wallet number (MSISDN); for bank transfers, the account number.
referencestringrequiredYour unique reference. Used for idempotency.
descriptionstringoptionalFree-text description for your reconciliation.
callback_urlstring · urioptionalURL to POST status updates to for this transaction.
Responses
successbooleanoptionaldataTransferoptionalRequest
curl -X POST 'https://api-gateway.markuppay.com/api/v1/transfers' \
-H 'X-Api-Key: pk_test_xxx' \
-H 'X-Timestamp: 1738800000' \
-H 'X-Signature: v1=abc123...' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"amount": 50000,
"currency": "GHS",
"network": "MTN",
"beneficiary_name": "Ama Owusu",
"beneficiary_account": "233201112222",
"reference": "payout_2026_0001",
"description": "Supplier settlement",
"callback_url": "https://example.com/webhooks/markuppay"
}'Response · 200
{
"success": true,
"data": {
"id": "txf_abc123",
"reference": "payout_2026_0001",
"type": "payout",
"status": "completed",
"amount": 50000,
"currency": "GHS",
"network": "MTN",
"beneficiary_name": "Ama Owusu",
"beneficiary_account": "233201112222",
"fee": 100,
"net_amount": 49900,
"created_at": "2026-06-25T11:00:00Z"
}
}Get a transfer
/api/v1/transfers/{id}Fetch a single payout by its id.
Path parameters
idstringrequiredThe transfer id, e.g. `txf_abc123`.
Responses
successbooleanoptionaldataTransferoptionalRequest
curl -X GET 'https://api-gateway.markuppay.com/api/v1/transfers/txf_abc123' \
-H 'X-Api-Key: pk_test_xxx' \
-H 'X-Timestamp: 1738800000' \
-H 'X-Signature: v1=abc123...' \
-H 'Accept: application/json'Response · 200
{
"success": true,
"data": {
"id": "txf_abc123",
"reference": "payout_2026_0001",
"type": "payout",
"status": "completed",
"amount": 50000,
"currency": "GHS",
"network": "MTN",
"beneficiary_name": "Ama Owusu",
"beneficiary_account": "233201112222",
"fee": 100,
"net_amount": 49900,
"created_at": "2026-06-25T11:00:00Z"
}
}Create a refund
/api/v1/refundsRefund a completed payment, in full or in part. Omit `amount` to refund the full remaining balance of the payment.
Request body
Refund details.
payment_idstringrequiredThe id of the payment to refund.
amountintegeroptionalAmount to refund in minor units. Omit to refund the full remaining balance.
reasonstringoptionalWhy the refund is being issued.
referencestringoptionalYour unique reference. Used for idempotency.
Responses
successbooleanoptionaldataRefundoptionalRequest
curl -X POST 'https://api-gateway.markuppay.com/api/v1/refunds' \
-H 'X-Api-Key: pk_test_xxx' \
-H 'X-Timestamp: 1738800000' \
-H 'X-Signature: v1=abc123...' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"payment_id": "pay_abc123",
"amount": 10000,
"reason": "Customer cancelled order",
"reference": "refund_12345"
}'Response · 200
{
"success": true,
"data": {
"id": "rfnd_abc123",
"payment_id": "pay_abc123",
"reference": "refund_12345",
"type": "refund",
"status": "completed",
"amount": 10000,
"currency": "GHS",
"reason": "Customer cancelled order",
"created_at": "2026-06-26T09:00:00Z"
}
}Get a refund
/api/v1/refunds/{id}Fetch a single refund by its id.
Path parameters
idstringrequiredThe refund id, e.g. `rfnd_abc123`.
Responses
successbooleanoptionaldataRefundoptionalRequest
curl -X GET 'https://api-gateway.markuppay.com/api/v1/refunds/rfnd_abc123' \
-H 'X-Api-Key: pk_test_xxx' \
-H 'X-Timestamp: 1738800000' \
-H 'X-Signature: v1=abc123...' \
-H 'Accept: application/json'Response · 200
{
"success": true,
"data": {
"id": "rfnd_abc123",
"payment_id": "pay_abc123",
"reference": "refund_12345",
"type": "refund",
"status": "completed",
"amount": 10000,
"currency": "GHS",
"reason": "Customer cancelled order",
"created_at": "2026-06-26T09:00:00Z"
}
}Get account balances
/api/v1/balancesReturn your available and pending settlement balances, one entry per currency.
Responses
successbooleanoptionaldataBalance[]optionalRequest
curl -X GET 'https://api-gateway.markuppay.com/api/v1/balances' \
-H 'X-Api-Key: pk_test_xxx' \
-H 'X-Timestamp: 1738800000' \
-H 'X-Signature: v1=abc123...' \
-H 'Accept: application/json'Response · 200
{
"success": true,
"data": [
{
"currency": "GHS",
"available": 1250000,
"pending": 75000
}
]
}Get supported networks
/api/v1/networksList the mobile money networks available to your account and the countries each one serves.
Responses
successbooleanoptionaldataNetwork[]optionalRequest
curl -X GET 'https://api-gateway.markuppay.com/api/v1/networks' \
-H 'X-Api-Key: pk_test_xxx' \
-H 'X-Timestamp: 1738800000' \
-H 'X-Signature: v1=abc123...' \
-H 'Accept: application/json'Response · 200
{
"success": true,
"data": [
{
"code": "MTN",
"name": "MTN Mobile Money",
"countries": [
"GH",
"UG",
"CI",
"CM"
]
}
]
}