MPMarkupPay
MarkupPay · Mobile money gateway

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.

v1
Version
10
Endpoints
4
Groups
22
Schemas

Base URL

https://api-gateway.markuppay.com

Sandbox lives at https://sandbox.api.markuppay.com with pk_test_ keys.

What’s in here

Payments

3 endpoints

Charge 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 endpoints

Pay money out of your settlement balance to a mobile money wallet — disbursements, supplier payments, and manual settlements. You specify only the beneficiary.

Refunds

2 endpoints

Return funds from a completed payment to the customer, in full or in part. Refunds move through the same status lifecycle as payments.

Account

2 endpoints

Read-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"
  }'
Getting started

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,
  };
}
Getting started

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.

Payments

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

  1. Create the payment with method: "card". The response is pending and carries next_action.redirect_url.
  2. Redirect the customer to that URL to authenticate with their bank.
  3. The customer returns to your return_url. The card is now charged — but treat the redirect as a hint, not proof.
  4. Confirm the final state from the payment.completed / payment.failed webhook, or by polling GET /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"
    }
  }
}
Getting started

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.completed

A payment (mobile money or card) succeeded.

payment.failed

A payment failed or was cancelled.

payout.completed

A transfer/payout succeeded.

payout.failed

A transfer/payout failed.

refund.completed

A 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);
}
Getting started

Errors

Failures return an ErrorResponse with a machine-readable code and a human-readable message. The most common authentication errors are below.

MISSING_HEADERSHeaders missing

One of the three required auth headers was not sent. Add X-Api-Key, X-Timestamp, and X-Signature.

TIMESTAMP_EXPIREDClock drift

The X-Timestamp is too far from server time. Sync your clock with NTP.

INVALID_SIGNATUREBad signature

The signature did not match. Recheck the signing string and your API secret.

INSUFFICIENT_PERMISSIONSNo scope

Your key lacks permission for this operation. Request additional scopes.

Error shape

{
  "success": false,
  "error": {
    "code": "INVALID_SIGNATURE",
    "message": "Signature validation failed"
  }
}
Payments

List payments

listPayments
GET/api/v1/payments

Return a paginated list of payments, most recent first. Filter by status with the `status` query parameter.

Query parameters

statusstringoptional

Only return payments in this state.

enumpending · processing · completed · failed · cancelled
limitintegeroptional

Maximum number of records to return (1–100).

default20minimum1maximum100
offsetintegeroptional

Number of records to skip for pagination.

default0minimum0

Responses

content-typeapplication/json
successbooleanoptional
dataPayment[]optional
paginationPaginationoptional

Request

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
  }
}
Payments

Create a payment

createPayment
POST/api/v1/payments

Charge 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.

content-typeapplication/json
amountintegerrequired

Amount in the currency's smallest unit (pesewas for GHS, cents for USD).

currencystringrequired

ISO 4217 currency code.

methodstringrequired

How the customer pays. `mobile_money` debits a wallet directly; `card` returns a hosted 3-D Secure page to redirect the customer to.

default"mobile_money"enummobile_money · card
phonestringoptional

Customer wallet number in international format. Required for `mobile_money`.

networkstringoptional

Mobile money network code. Required for `mobile_money`; resolved from the phone number when omitted.

enumMTN · TELECEL · AIRTELTIGO · AIRTEL · ORANGE
customer_emailstring · emailoptional

Customer email. Required for `card` payments — the receipt is sent here.

customer_namestringoptional

Customer name, shown on the hosted card page.

return_urlstring · urioptional

Where to send the customer after they complete 3-D Secure. Required for `card` payments.

referencestringrequired

Your unique reference for the transaction. Used for idempotency.

descriptionstringoptional

Free-text description shown to you for reconciliation.

callback_urlstring · urioptional

URL to POST status updates to for this transaction.

Responses

content-typeapplication/json
successbooleanoptional
dataPaymentoptional

Request

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"
  }
}
Payments

Get a payment

getPayment
GET/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

idstringrequired

The payment id, e.g. `pay_abc123`.

example"pay_abc123"

Responses

content-typeapplication/json
successbooleanoptional
dataPaymentoptional

Request

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"
  }
}
Payouts & transfers

List transfers

listTransfers
GET/api/v1/transfers

Return a paginated list of payouts, most recent first.

Query parameters

statusstringoptional

Only return transfers in this state.

enumpending · processing · completed · failed
limitintegeroptional

Maximum number of records to return (1–100).

default20minimum1maximum100
offsetintegeroptional

Number of records to skip for pagination.

default0minimum0

Responses

content-typeapplication/json
successbooleanoptional
dataTransfer[]optional
paginationPaginationoptional

Request

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
  }
}
Payouts & transfers

Create a payout

createTransfer
POST/api/v1/transfers

Send 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.

content-typeapplication/json
amountintegerrequired

Amount in the currency's smallest unit.

currencystringrequired

ISO 4217 currency code.

networkstringrequired

Mobile money network code.

enumMTN · TELECEL · AIRTELTIGO · AIRTEL · ORANGE
beneficiary_namestringrequired

Name of the recipient.

beneficiary_accountstringrequired

Recipient wallet number (MSISDN); for bank transfers, the account number.

referencestringrequired

Your unique reference. Used for idempotency.

descriptionstringoptional

Free-text description for your reconciliation.

callback_urlstring · urioptional

URL to POST status updates to for this transaction.

Responses

content-typeapplication/json
successbooleanoptional
dataTransferoptional

Request

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"
  }
}
Payouts & transfers

Get a transfer

getTransfer
GET/api/v1/transfers/{id}

Fetch a single payout by its id.

Path parameters

idstringrequired

The transfer id, e.g. `txf_abc123`.

example"txf_abc123"

Responses

content-typeapplication/json
successbooleanoptional
dataTransferoptional

Request

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"
  }
}
Refunds

Create a refund

createRefund
POST/api/v1/refunds

Refund a completed payment, in full or in part. Omit `amount` to refund the full remaining balance of the payment.

Request body

Refund details.

content-typeapplication/json
payment_idstringrequired

The id of the payment to refund.

amountintegeroptional

Amount to refund in minor units. Omit to refund the full remaining balance.

reasonstringoptional

Why the refund is being issued.

referencestringoptional

Your unique reference. Used for idempotency.

Responses

content-typeapplication/json
successbooleanoptional
dataRefundoptional

Request

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"
  }
}
Refunds

Get a refund

getRefund
GET/api/v1/refunds/{id}

Fetch a single refund by its id.

Path parameters

idstringrequired

The refund id, e.g. `rfnd_abc123`.

example"rfnd_abc123"

Responses

content-typeapplication/json
successbooleanoptional
dataRefundoptional

Request

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"
  }
}
Account

Get account balances

getBalances
GET/api/v1/balances

Return your available and pending settlement balances, one entry per currency.

Responses

content-typeapplication/json
successbooleanoptional
dataBalance[]optional

Request

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
    }
  ]
}
Account

Get supported networks

getNetworks
GET/api/v1/networks

List the mobile money networks available to your account and the countries each one serves.

Responses

content-typeapplication/json
successbooleanoptional
dataNetwork[]optional

Request

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"
      ]
    }
  ]
}