Skip to main content

Overview

Mavapay allows your users to buy Bitcoin using Nigerian Naira (NGN). The API provides flexible options for how amounts are specified and how Bitcoin is delivered.
Two Bitcoin Delivery Options:
  • Internal Wallet (autopayout: false): Bitcoin is credited to your Mavapay wallet. The beneficiary field is ignored. You control when and how to deliver Bitcoin to your users.
  • Instant Delivery (autopayout: true): Bitcoin is automatically sent to the provided Lightning invoice immediately after payment is received.

Use Cases

  1. Fixed Fiat Amount: User wants to spend exactly ₦5,000
  2. Fixed Bitcoin Amount: User wants to receive exactly 10,000 sats
  3. Instant Delivery: Bitcoin sent immediately to user’s Lightning wallet

Understanding Payment Currency

The paymentCurrency parameter is crucial for buy Bitcoin flows. It determines what the amount field represents:
  • paymentCurrency: "NGNKOBO" → amount is in Naira (you’re specifying how much to spend)
  • paymentCurrency: "BTCSAT" → amount is in Bitcoin (you’re specifying how much to receive)
This allows you to support both “I want to spend ₦5,000” and “I want to buy 10,000 sats” use cases with the same API.

Integration Flow

Step 1: Create a Quote

Choose one of two approaches based on your use case:

Option A: User Specifies Fiat Amount

When a user wants to spend exactly ₦5,000:
curl -X POST https://api.mavapay.co/api/v1/quote \
  -H "Content-Type: application/json" \
  -H "x-api-key: <your-api-key>" \
  -d '{
    "amount": "500000",
    "sourceCurrency": "NGNKOBO",
    "targetCurrency": "BTCSAT",
    "paymentMethod": "BANKTRANSFER",
    "paymentCurrency": "NGNKOBO",
    "autopayout": false
  }'
The response tells you how much Bitcoin they’ll receive.

Option B: User Specifies Bitcoin Amount

When a user wants to receive exactly 10,000 sats:
curl -X POST https://api.mavapay.co/api/v1/quote \
  -H "Content-Type: application/json" \
  -H "x-api-key: <your-api-key>" \
  -d '{
    "amount": "10000",
    "sourceCurrency": "NGNKOBO",
    "targetCurrency": "BTCSAT",
    "paymentMethod": "BANKTRANSFER",
    "paymentCurrency": "BTCSAT",
    "autopayout": false
  }'
The response tells you how much Naira they need to pay.
See the Create Quote API reference for interactive examples you can test.

Step 2: Display Payment Instructions

Show the user the bank account details from the quote response:
const quote = response.data;

// Display to user
const paymentInstructions = {
  bank: quote.bankName,              // e.g., "GLOBUS BANK"
  accountNumber: quote.ngnBankAccountNumber,  // e.g., "3242273802"
  accountName: quote.ngnAccountName,          // e.g., "Mava Digital Solutions Limited"
  amount: quote.amountInSourceCurrency / 100, // Convert kobo to Naira
  expiresAt: quote.expiry,
  orderId: quote.orderId
};
Example UI:
Please transfer ₦8,445.40 to:

Bank: GLOBUS BANK  
Account: 3242273802
Name: Mava Digital Solutions Limited

Order ID: 43477-4306
Quote expires in: 14:32
Quotes expire after approximately 10 minutes for bank transfers and 5 minutes for lightning invoice payments. Display a countdown timer and prompt users to complete payment quickly.

Step 3: Wait for Payment Confirmation

You’ll receive a payment.received webhook when the user makes the bank transfer:
{
  "event": "payment.received",
  "data": {
    "id": "9830a229-1db0-4a19-9e51-37cca51586f2",
    "amount": 3427,
    "currency": "BTC",
    "type": "DEPOSIT",
    "status": "SUCCESS",
    "orderId": "58772-8830",
    ...
  }
}
See Webhook Examples for complete payload details.

Step 4: Credit User’s Account

You have two options for delivering Bitcoin:

Manual Payout (autopayout: false)

  • Bitcoin is credited to your Mavapay wallet
  • The beneficiary field is ignored when autopayout is false
  • You handle crediting user’s account in your system
  • Later withdraw to user’s wallet using the Withdraw BTC API

Auto Payout (autopayout: true)

  • Bitcoin is automatically sent to the provided Lightning invoice
  • You receive both payment.received and payment.sent webhooks
  • User receives Bitcoin within seconds

Auto Payout with Lightning Invoice

For instant Bitcoin delivery, set autopayout: true and provide a Lightning invoice:
curl -X POST https://api.mavapay.co/api/v1/quote \
  -H "Content-Type: application/json" \
  -H "x-api-key: <your-api-key>" \
  -d '{
    "amount": "208238",
    "sourceCurrency": "NGNKOBO",
    "targetCurrency": "BTCSAT",
    "paymentMethod": "BANKTRANSFER",
    "paymentCurrency": "BTCSAT",
    "autopayout": true,
    "beneficiary": {
      "lnInvoice": "lnbc2082380n1p5fhytm..."
    }
  }'

Requirements for Autopayout

  • autopayout can only be true when paymentCurrency equals the target currency (BTCSAT)
  • A valid beneficiary with Lightning invoice or Lightning address must be provided
  • For Lightning invoices: The invoice amount must match the amount field in the request
  • For Lightning addresses: Amount matching is not required since the invoice is generated on the fly

Webhooks Flow

  1. payment.received - Naira payment confirmed
  2. payment.sent - Bitcoin delivered to invoice
// First webhook
{
  "event": "payment.received",
  "data": {
    "type": "DEPOSIT",
    "currency": "BTC",
    "status": "SUCCESS",
    ...
  }
}

// Second webhook (a few seconds later)
{
  "event": "payment.sent",
  "data": {
    "type": "WITHDRAWAL",
    "currency": "BTC", 
    "status": "SUCCESS",
    ...
  }
}

Implementation Examples

Example 1: Basic Buy Bitcoin (No Autopayout)

async function buyBitcoin(amountNaira) {
  // 1. Create quote
  const quote = await fetch('https://api.mavapay.co/api/v1/quote', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': process.env.MAVAPAY_API_KEY
    },
    body: JSON.stringify({
      amount: (amountNaira * 100).toString(), // Convert to kobo
      sourceCurrency: 'NGNKOBO',
      targetCurrency: 'BTCSAT',
      paymentMethod: 'BANKTRANSFER',
      paymentCurrency: 'NGNKOBO',
      autopayout: false
    })
  });

  const { data } = await quote.json();

  // 2. Store quote and show bank details to user
  await saveQuote(data.id, data.orderId);
  
  return {
    orderId: data.orderId,
    bankName: data.bankName,
    accountNumber: data.ngnBankAccountNumber,
    accountName: data.ngnAccountName,
    amount: data.amountInSourceCurrency / 100,
    bitcoinAmount: data.amountInTargetCurrency,
    expiry: data.expiry
  };
}

// 3. Handle webhook
app.post('/webhook', async (req, res) => {
  const { event, data } = req.body;

  if (event === 'payment.received' && data.currency === 'BTC') {
    // Credit user's BTC balance
    await creditUserBitcoin(data.orderId, data.amount);
  }

  res.json({ received: true });
});

Example 2: Instant Bitcoin Delivery

async function buyBitcoinInstant(satoshiAmount, userLightningInvoice) {
  // Decode invoice to verify amount
  const decoded = await decodeInvoice(userLightningInvoice);
  
  if (decoded.satoshis !== satoshiAmount) {
    throw new Error('Invoice amount does not match requested amount');
  }

  // Create quote with autopayout
  const quote = await fetch('https://api.mavapay.co/api/v1/quote', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': process.env.MAVAPAY_API_KEY
    },
    body: JSON.stringify({
      amount: satoshiAmount.toString(),
      sourceCurrency: 'NGNKOBO',
      targetCurrency: 'BTCSAT',
      paymentMethod: 'BANKTRANSFER',
      paymentCurrency: 'BTCSAT',
      autopayout: true,
      beneficiary: {
        lnInvoice: userLightningInvoice
      }
    })
  });

  const { data } = await quote.json();

  return {
    orderId: data.orderId,
    bankDetails: {
      bank: data.bankName,
      account: data.ngnBankAccountNumber,
      name: data.ngnAccountName
    },
    amountToPay: data.amountInSourceCurrency / 100,
    bitcoinAmount: satoshiAmount,
    deliveryMethod: 'Lightning (Instant)',
    expiry: data.expiry
  };
}

// Handle both webhooks
app.post('/webhook', async (req, res) => {
  const { event, data } = req.body;

  if (event === 'payment.received') {
    await updateOrderStatus(data.orderId, 'payment_received');
    await notifyUser(data.orderId, 'Payment received, sending Bitcoin...');
  }

  if (event === 'payment.sent') {
    await updateOrderStatus(data.orderId, 'completed');
    await notifyUser(data.orderId, 'Bitcoin delivered to your wallet!');
  }

  res.json({ received: true });
});

Important Considerations

Quote Expiration

Quotes are valid for approximately 10 minutes for bank transfers and 5 minutes for lightning invoice payments. If a user pays after expiration:
  • Payment may be refunded
  • Or applied manually by contacting support
  • Always display a clear countdown timer

Exchange Rate Volatility

Exchange rates update frequently. For the best rate:
  • Create a fresh quote each time
  • Don’t cache quotes for long periods
  • Explain to users that rates may change

Amount Validation

When using autopayout with Lightning invoices:
// Always decode and verify invoice amount
const bolt11 = require('bolt11');

function validateInvoice(invoice, expectedSats) {
  const decoded = bolt11.decode(invoice);
  const invoiceAmount = decoded.satoshis;
  
  if (invoiceAmount !== expectedSats) {
    throw new Error(
      `Invoice amount (${invoiceAmount} sats) does not match ` +
      `expected amount (${expectedSats} sats)`
    );
  }
  
  return true;
}

Testing

Test your integration in the staging environment:
https://staging.api.mavapay.co/api/v1
Use the Simulation endpoint to test payment flows without real money:
# After creating a quote
curl -X POST https://staging.api.mavapay.co/api/v1/simulation/pay-in \
  -H "Content-Type: application/json" \
  -H "x-api-key: <your-staging-key>" \
  -d '{
    "currency": "NGN",
    "quoteId": "53a87b44-2ecc-4106-8297-f1fd15d010a9",
    "amount": 844540
  }'

Best Practices

1. Show Real-Time Rates

Create new quotes to show current exchange rates:
// Good: Fresh quote every time
const quote = await createQuote(amount);

// Bad: Cached quote from 10 minutes ago
const quote = getCachedQuote();

2. Display Expiry Prominently

function CountdownTimer({ expiryDate }) {
  const [timeLeft, setTimeLeft] = useState(calculateTimeLeft(expiryDate));
  
  return (
    <div className="alert alert-warning">
      Quote expires in: <strong>{timeLeft}</strong>
      <br />
      <small>Pay quickly to lock in this rate</small>
    </div>
  );
}

3. Handle Webhooks Idempotently

The same webhook may be sent multiple times:
async function handlePaymentReceived(data) {
  const { id, orderId, amount } = data;
  
  // Check if already processed
  const existing = await db.transactions.findOne({ id });
  if (existing) {
    console.log(`Transaction ${id} already processed`);
    return;
  }
  
  // Process and mark as complete
  await db.transactions.insert({ id, orderId, amount, processed: true });
  await creditUserAccount(orderId, amount);
}

4. Provide Status Updates

Keep users informed throughout the process:
const STATUS_MESSAGES = {
  'quote_created': 'Please make payment to the account below',
  'payment_received': 'Payment confirmed! Processing...',
  'bitcoin_sent': 'Bitcoin delivered to your wallet',
  'expired': 'Quote expired. Please create a new order.'
};

5. Handle Errors Gracefully

try {
  const quote = await createQuote(amount);
} catch (error) {
  if (error.message.includes('amount too small')) {
    return 'Minimum purchase is ₦1,000';
  }
  if (error.message.includes('quote expired')) {
    return 'Quote expired. Please try again.';
  }
  return 'Unable to create quote. Please try again.';
}

Supported Currencies

Currently available:
  • NGNKOBO => BTCSAT (Nigerian Naira to Bitcoin)
Coming soon:
  • KESCENT => BTCSAT (Kenyan Shilling to Bitcoin)
  • ZARCENT => BTCSAT (South African Rand to Bitcoin)

Troubleshooting

Quote Creation Fails

Error: “Invalid payment currency”
  • Ensure paymentCurrency is either sourceCurrency or targetCurrency
  • For buying BTC, use NGNKOBO or BTCSAT
Error: “Amount too small”
  • Check minimum amounts in the API response
  • Amounts are in the lowest denomination (kobo for NGN, satoshis for BTC)

Autopayout Not Working

Error: “Autopayout requires payment currency to be target currency”
  • Set paymentCurrency: "BTCSAT" when buying BTC with autopayout
  • Provide a valid Lightning invoice in the beneficiary field
Error: “Invoice amount mismatch”
  • The Lightning invoice amount must exactly match the amount field
  • Decode invoice and verify before creating quote

Payment Not Detected

  • Ensure webhook is registered and publicly accessible
  • Check that user sent the exact amount shown in the quote
  • Verify payment was sent to the correct bank account
  • Contact support with the orderId for manual verification

FAQ

Q: What’s the minimum amount I can buy?
A: Minimum amounts vary. Check the API response for current limits.
Q: How long does autopayout take?
A: Usually within seconds of receiving Naira or Bitcoin payment.
Q: Can I use on-chain addresses instead of Lightning?
A: Currently only Lightning invoices and addresses are supported for autopayout.
Q: What happens if the user pays after the quote expires?
A: The payment may be refunded or processed manually. Contact support with the order ID.
Q: Can I set my own exchange rate markup?
A: Yes, use the customerInternalFee field to add your margin.
Q: How do I handle partial payments?
A: Partial payments are not automatically processed. Contact support for manual handling.