Custom Payment Flow

View as Markdown

Build a fully customized cryptocurrency payment experience with complete control over the user interface and payment flow. This guide will walk you through creating a payment from scratch using BlockBee's API.

Example of BlockBee's Custom Payment Flow taken from WooCommerce Plugin

Overview

The custom payment flow gives you maximum flexibility to create the exact payment experience you want. You'll handle:

  • Payment creation - Generate payment addresses and amounts
  • User interface - Build your own payment screens
  • Webhook handling - Process the webhooks with the payment confirmations

Setup

Before you can start building custom payment flows, you need to configure your BlockBee account and get your API credentials.

1. Create a BlockBee Account

If you haven't already, sign up for a BlockBee account to get access to the dashboard.

2. Configure Addresses

Set up the cryptocurrency addresses where you want to receive payments at Addresses.

3. Generate API Key

  1. Navigate to the API Keys section in your dashboard
  2. Click "Generate New API Key"
  3. Copy the generated API key - you'll need this for all API requests

Step 1: Create Payment

First, create a new payment by calling the BlockBee API. You'll need your API key and the payment details.

// Create a new payment
const createPayment = async (orderId) => {
  const callbackUrl = encodeURIComponent('https://yoursite.com/webhook?order_id=' + orderId);
  
  const params = new URLSearchParams({
    callback: callbackUrl,
    apikey: 'YOUR_API_KEY',
    address: 'YOUR_WALLET_ADDRESS',
    post: 0,
    json: 0,
    pending: 1,
    multi_token: 0,
    convert: 1
  });
  
  const response = await fetch(`https://api.blockbee.io/btc/create/?${params}`);
  const data = await response.json();
  return data;
};

Check the API Reference to understand all the parameters you can use with this endpoint.

Parameter Notes:

  • callback - Add query parameters to help track payments when receiving webhooks (e.g., ?order_id=123&user_id=456)
  • address - Your wallet address where payments will be forwarded. Can be omitted if set in the Dashboard
  • pending - Set to 1 to receive webhooks for unconfirmed transactions (recommended for better UX)
  • convert - Set to 1 to automatically convert received payments to your preferred currency
  • post - Set to 1 to receive webhooks via POST instead of GET requests (default is GET)
  • json - Set to 1 to receive webhook data in JSON format instead of URL-encoded parameters (works with both GET and POST)
  • multi_token - Only relevant for blockchains with tokens (Ethereum, Solana, Tron). Set to 1 to accept multiple token types

Payment Splitting: To split payments between multiple addresses, use: percentage_1@address_1|percentage_2@address_2 Example: 0.5@ADDRESS_1|0.5@ADDRESS_2 splits 50/50 between two addresses.

Response:

JSON
{
  "address_in": "14PqCsA7KMgseZMPwg6mJy754MtQkrgszu",
  "address_out": "1H6ZZpRmMnrw8ytepV3BYwMjYYnEkWDqVP",
  "callback_url": "https://yoursite.com/webhook?order_id=12345",
  "priority": "default",
  "minimum_transaction_coin": 0.008,
  "status": "success"
}

Step 2: Display Payment Information

Create a payment interface that shows the customer how to complete their payment. You have two main scenarios depending on your use case:

Scenario A: Exchange Deposits (Any Amount)

For exchanges or services accepting any amount above the minimum, fetch the minimum transaction requirements and display them to users.

Scenario B: Ecommerce Payments (Specific Amount)

For stores with fixed prices, convert your fiat amount to cryptocurrency using real-time exchange rates.

Let's implement both scenarios:

// Get ticker information and minimums
const getTickerInfo = async () => {
  const response = await fetch('https://api.blockbee.io/btc/info/?apikey=YOUR_API_KEY');
  const data = await response.json();
  return data;
};

// Convert USD to BTC for ecommerce payments
const convertAmount = async (usdAmount) => {
  const params = new URLSearchParams({
    apikey: 'YOUR_API_KEY',
    value: usdAmount,
    from: 'USD'
  });
  
  const response = await fetch(`https://api.blockbee.io/btc/convert/?${params}`);
  const data = await response.json();
  return data.value_coin; // BTC amount
};

// Get QR code for payment address
const getQRCode = async (address, amount = null) => {
  const params = new URLSearchParams({
    apikey: 'YOUR_API_KEY',
    address: address,
    size: 200
  });
  
  if (amount) {
    params.append('value', amount);
  }
  
  const response = await fetch(`https://api.blockbee.io/btc/qrcode/?${params}`);
  const data = await response.json();
  return data.qr_code; // Base64 image
};

// Display payment information - Exchange Deposits
const displayExchangeDeposit = async (paymentData) => {
  const tickerInfo = await getTickerInfo();
  const qrCode = await getQRCode(paymentData.address_in);
  
  const paymentContainer = document.getElementById('payment-container');
  paymentContainer.innerHTML = `
    <div class="payment-info">
      <h3>Deposit Bitcoin</h3>
      <div class="payment-details">
        <p><strong>Minimum Deposit:</strong> ${tickerInfo.minimum_transaction_coin} BTC</p>
        <p><strong>Send Bitcoin to:</strong></p>
        <div class="address-container">
          <code>${paymentData.address_in}</code>
          <button onclick="copyAddress('${paymentData.address_in}')">Copy</button>
        </div>
        <div class="qr-code">
          <img src="data:image/png;base64,${qrCode}" alt="Payment QR Code" />
        </div>
        <p class="warning">⚠️ Send only amounts above ${tickerInfo.minimum_transaction_coin} BTC</p>
      </div>
      <div class="payment-status">
        <p id="status">⏳ Waiting for deposit...</p>
      </div>
    </div>
  `;
};

// Display payment information - Ecommerce Payment
const displayEcommercePayment = async (paymentData, usdAmount) => {
  const btcAmount = await convertAmount(usdAmount);
  const tickerInfo = await getTickerInfo();
  const minimumAmount = tickerInfo.minimum_transaction_coin;
  
  // Validate minimum amount
  if (btcAmount < minimumAmount) {
    const paymentContainer = document.getElementById('payment-container');
    paymentContainer.innerHTML = `
      <div class="payment-error">
        <h3>❌ Payment Amount Too Low</h3>
        <div class="error-details">
          <p><strong>Your payment amount:</strong> ${btcAmount} BTC</p>
          <p><strong>Minimum required:</strong> ${minimumAmount} BTC</p>
          <p class="warning">⚠️ Payments below the minimum will be lost. Please increase your order amount.</p>
        </div>
      </div>
    `;
    return;
  }
  
  const qrCode = await getQRCode(paymentData.address_in, btcAmount);
  
  const paymentContainer = document.getElementById('payment-container');
  paymentContainer.innerHTML = `
    <div class="payment-info">
      <h3>Complete Your Payment</h3>
      <div class="payment-details">
        <p><strong>Amount:</strong> $${usdAmount} USD (${btcAmount} BTC)</p>
        <p><strong>Send exactly:</strong></p>
        <div class="amount-container">
          <code>${btcAmount} BTC</code>
          <button onclick="copyAmount('${btcAmount}')">Copy Amount</button>
        </div>
        <p><strong>To address:</strong></p>
        <div class="address-container">
          <code>${paymentData.address_in}</code>
          <button onclick="copyAddress('${paymentData.address_in}')">Copy Address</button>
        </div>
        <div class="qr-code">
          <img src="data:image/png;base64,${qrCode}" alt="Payment QR Code" />
          <p><small>QR code includes amount and address</small></p>
        </div>
        <p class="minimum-warning">⚠️ Minimum transaction amount: ${minimumAmount} BTC</p>
      </div>
      <div class="payment-status">
        <p id="status">⏳ Waiting for payment...</p>
      </div>
    </div>
  `;
};

const copyAddress = (address) => {
  navigator.clipboard.writeText(address);
  alert('Address copied to clipboard!');
};

const copyAmount = (amount) => {
  navigator.clipboard.writeText(amount);
  alert('Amount copied to clipboard!');
};

Key API Endpoints Used

The examples above integrate with these BlockBee API endpoints:

  1. GET /{ticker}/info/ - Get minimum transaction amounts and ticker information
  2. GET /{ticker}/convert/ - Convert fiat amounts to cryptocurrency
  3. GET /{ticker}/qrcode/ - Generate QR codes with optional amount embedding

Implementation Notes

For Exchange Deposits:

  • Fetch minimum transaction requirements using the info endpoint
  • Display address-only QR codes (no fixed amount)
  • Always display minimum deposit requirements prominently - this is critical to prevent fund loss
  • Accept any amount above the minimum
  • Warn users that amounts below minimum will be lost

For Ecommerce Payments:

  • Convert fiat prices to cryptocurrency using current exchange rates
  • Generate QR codes that include both address and exact amount
  • Display both fiat and crypto amounts for clarity
  • Provide separate copy buttons for address and amount
  • Verify the converted amount is above the minimum transaction threshold
  • Always display minimum transaction amount prominently - even for fixed amounts, users need to know the minimum

Minimum Transaction Amount Handling:

  • Always fetch and display minimum amounts from the info endpoint for your chosen cryptocurrency
  • Always show minimum requirements in your UI - this is not optional, it prevents fund loss
  • Validate amounts before accepting payments - if a user's payment would be below minimum, show an error
  • Display clear warnings about minimum requirements in your UI
  • For ecommerce payments, ensure your fiat price converts to an amount above the minimum

Step 3: Track Payments

You can track payments using two methods: Webhooks (recommended) or Logs Endpoint (polling). Most applications use webhooks for real-time updates, but the logs endpoint is useful for troubleshooting or as a backup method.

Method 1: Webhooks (Recommended)

Set up webhook endpoints to receive real-time notifications when payments are received. BlockBee sends two types of webhooks:

Webhook Types:

  • Pending - Payment detected in mempool but not yet confirmed (particularly useful for slower blockchains like Bitcoin, Litecoin, and Bitcoin Cash)
  • Confirmed - Payment has received the number of confirmations specified in the confirmations parameter when creating the payment address

For complete webhook field documentation, see Custom Payment Flow Webhooks - includes all fields, examples, and implementation details.

Webhook Implementation:

// Express.js webhook handler - handles both GET and POST
app.all('/webhook', express.json(), (req, res) => {
  // Handle both GET (default) and POST (if post=1 was set)
  const webhookData = req.method === 'GET' ? req.query : req.body;
  const { 
    uuid,
    address_in, 
    address_out, 
    txid_in, 
    txid_out, 
    confirmations, 
    value_coin, 
    value_coin_convert,
    value_forwarded_coin,
    value_forwarded_coin_convert,
    fee_coin,
    coin,
    price,
    pending,
    order_id,
    user_id
  } = webhookData;
  
  // Note: order_id and user_id come from custom parameters you added to callback URL
  
  // Check if we've already processed this transaction
  const alreadyProcessed = checkTransactionInDatabase(uuid);
  
  if (!alreadyProcessed) {
    if (pending === 1) {
      // Payment detected but not confirmed
      console.log(`Pending payment for ${order_id || user_id}: ${value_coin} ${coin.toUpperCase()} to ${address_in}`);
      console.log(`UUID: ${uuid}, Price: $${price}`);
      
      // Store transaction in database with UUID
      storeTransaction({
        uuid: uuid,
        address_in: address_in,
        address_out: address_out,
        txid_in: txid_in,
        amount: value_coin,
        coin: coin,
        price: price,
        status: 'pending',
        value_coin_convert: value_coin_convert,
        processed_at: new Date()
      });
      
      // Notify user (WebSocket, email, etc.)
      notifyUser(address_in, 'pending', {
        uuid: uuid,
        amount: value_coin,
        coin: coin,
        usd_value: value_coin_convert ? JSON.parse(value_coin_convert).USD : null
      });
      
    } else if (pending === 0) {
      // Payment confirmed
      console.log(`Confirmed payment for ${order_id || user_id}: ${value_coin} ${coin.toUpperCase()} to ${address_in}`);
      console.log(`UUID: ${uuid}, Forwarded: ${value_forwarded_coin}, Fee: ${fee_coin}`);
      
      // Update database
      updateTransaction(uuid, {
        txid_out: txid_out,
        confirmations: confirmations,
        value_forwarded_coin: value_forwarded_coin,
        value_forwarded_coin_convert: value_forwarded_coin_convert,
        fee_coin: fee_coin,
        status: 'confirmed',
        confirmed_at: new Date()
      });
      
      // Process order, send confirmation email, etc.
      processSuccessfulPayment(uuid, {
        orderId: order_id,
        userId: user_id,
        amount: value_coin,
        forwarded_amount: value_forwarded_coin,
        fee: fee_coin,
        coin: coin,
        confirmations: confirmations
      });
      
      // Notify user
      notifyUser(address_in, 'confirmed', {
        uuid: uuid,
        amount: value_coin,
        forwarded_amount: value_forwarded_coin,
        coin: coin,
        confirmations: confirmations
      });
    }
  } else {
    console.log(`Duplicate webhook received for UUID: ${uuid}`);
  }
  
  // Always respond with *ok* or HTTP 200 to stop retries
  res.status(200).send('*ok*');
});

Method 2: Logs Endpoint (Polling)

If webhooks aren't suitable for your setup, or as a backup method, you can use the logs endpoint to check payment status. This is particularly useful for troubleshooting webhook issues or implementing cron jobs to monitor payments.

// Check payment status using logs endpoint
const checkPaymentStatus = async (callbackUrl) => {
  const encodedCallback = encodeURIComponent(callbackUrl);
  const params = new URLSearchParams({
    apikey: 'YOUR_API_KEY',
    callback: encodedCallback
  });
  
  const response = await fetch(`https://api.blockbee.io/btc/logs/?${params}`);
  const data = await response.json();
  
  // Check if API request was successful
  if (data.status !== 'success') {
    console.error('API request failed:', data);
    return { error: 'API request failed' };
  }
  
  if (data.callbacks && data.callbacks.length > 0) {
    const processedTransactions = [];
    
    // Process each transaction
    for (const callback of data.callbacks) {
      // Check if transaction is confirmed (pending = 0 in logs)
      const confirmedLog = callback.logs.find(log => log.pending === 0);
      const isConfirmed = confirmedLog !== undefined;
      
      processedTransactions.push({
        uuid: callback.uuid || `${callback.txid_in}-${callback.value_coin}`, // Use UUID or fallback
        txid: callback.txid_in,
        amount: callback.value_coin,
        confirmations: callback.confirmations,
        status: callback.result,
        isConfirmed: isConfirmed,
        lastUpdate: callback.last_update
      });
    }
    
    return { transactions: processedTransactions };
  }
  
  return { transactions: [] };
};

// Process payments with duplicate prevention
const processPayments = async (callbackUrl) => {
  const result = await checkPaymentStatus(callbackUrl);
  
  if (result.error) {
    console.error('Failed to check payment status:', result.error);
    return;
  }
  
  for (const transaction of result.transactions) {
    // Check if we've already processed this transaction UUID
    const alreadyProcessed = await checkTransactionInDatabase(transaction.uuid);
    
    if (!alreadyProcessed) {
      if (transaction.isConfirmed && transaction.status === 'done') {
        console.log(`New confirmed payment: ${transaction.amount} BTC`);
        
        // Store transaction in database with UUID
        await storeTransaction({
          uuid: transaction.uuid,
          txid: transaction.txid,
          amount: transaction.amount,
          confirmations: transaction.confirmations,
          status: 'confirmed',
          processed_at: new Date()
        });
        
        // Process the payment (fulfill order, send confirmation, etc.)
        await processConfirmedPayment(transaction);
        
      } else if (transaction.status === 'pending') {
        console.log(`New pending payment: ${transaction.amount} BTC`);
        
        // Store as pending transaction
        await storeTransaction({
          uuid: transaction.uuid,
          txid: transaction.txid,
          amount: transaction.amount,
          confirmations: transaction.confirmations,
          status: 'pending',
          processed_at: new Date()
        });
        
        // Notify user of pending payment
        await notifyPendingPayment(transaction);
      }
    }
  }
};

// Helper functions (implement based on your database)
const checkTransactionInDatabase = async (uuid) => {
  // Check if transaction UUID exists in your database
  // Return true if exists, false if new
};

const storeTransaction = async (transactionData) => {
  // Store transaction in your database
  // Include the UUID to prevent duplicate processing
};

const processConfirmedPayment = async (transaction) => {
  // Process confirmed payment (fulfill order, send email, etc.)
};

const notifyPendingPayment = async (transaction) => {
  // Notify user of pending payment
};

API Reference

Important Implementation Notes

API Status vs Payment Status:

  • status field indicates API request success (success/error)
  • Payment status is found within the callbacks array and logs entries

Multiple Transactions:

  • Each payment address can receive multiple transactions
  • Each transaction has a unique uuid (or use txid_in + value_coin as fallback)
  • Always store the UUID in your database to prevent duplicate processing

Confirmation Detection:

  • Check logs array within each callback
  • Transaction is confirmed when pending = 0 in any log entry
  • Don't rely solely on result field - check the logs for confirmation status

Duplicate Prevention:

  • Store transaction UUIDs in your database
  • Check if UUID exists before processing any transaction
  • This prevents processing the same transaction multiple times

Payment Status Values

The logs endpoint returns different result values indicating payment status:

  • pending - Transaction is being confirmed by the blockchain
  • sent - Payment forwarded to your address but webhook didn't receive valid *ok* response
  • done - Payment forwarded and webhook sent to your URL with valid *ok* response received

When to Use Each Method

Use Webhooks when:

  • You need real-time payment notifications
  • Your application can receive HTTP requests
  • You want the most efficient solution

Use Logs Endpoint when:

  • Webhooks aren't feasible (firewall restrictions, etc.)
  • You need to troubleshoot webhook issues
  • Implementing backup payment monitoring
  • Running periodic cron jobs to check payment status

Testing Your Integration

Test your payment flow using real cryptocurrency with minimal cost:

1. Use Litecoin for Testing

We recommend using Litecoin (LTC) for testing because:

  • Low transaction fees (typically under $0.01)
  • Fast confirmation times (2.5 minutes average)
  • Low BlockBee fees on small amounts
  • Real blockchain testing without high costs

Simply change your ticker from btc to ltc in your existing code. If your integration works with LTC, it will work with any ticker (btc, eth, trc20/usdt, bep20/usdc, etc.) - the API endpoints and webhook structure are identical across all cryptocurrencies.

2. Test with $2 Worth of LTC

  • Send approximately $2 USD worth of Litecoin to test the complete flow
  • This covers blockchain fees + BlockBee fees with minimal cost
  • You can buy small amounts of LTC on most exchanges
  • Test both pending and confirmed webhook states

3. Test Checklist

  • ✅ Payment creation works with LTC
  • ✅ QR code displays correctly
  • ✅ Address copying functions
  • ✅ Pending webhook received (fast with LTC)
  • ✅ Confirmed webhook received
  • ✅ UI updates correctly
  • ✅ Amount calculations are accurate
  • ✅ Success flow completes

4. Testing Environment Setup

Bash
# Use ngrok for local webhook testing
ngrok http 3000

# Your test webhook URL: https://abc123.ngrok.io/webhook