Skip to main content

Overview

Accept cryptocurrency payments for borderless, instant transactions with no foreign exchange fees.

Supported Crypto

USDT (Tether)

Networks

TRC20, ERC20

Confirmation

~10 minutes

Why USDT?

USDT (Tether) is a stablecoin pegged 1:1 to USD: Stable value - no volatility like Bitcoin Low fees - especially on TRC20 Fast transfers - minutes, not days Global - no currency conversion Privacy - no personal info needed

Supported Networks

Recommendation: Use TRC20 for better user experience and lower fees.

How It Works

88Pay integration flow diagram

Create Crypto Payment

curl -X POST "https://api-sandbox.88pay.io/api/transactions/charges" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "x-session-id: YOUR_SESSION" \
  -H "Content-Type: application/json" \
  -d '{
    "flow": "PAYIN",
    "method_code": "USDT",
    "method_category": "CRYPTO",
    "amount": 50,
    "currency": "USD",
    "country": "COL",
    "description": "Order #12345",
    "customer_id": "user_001",
    "notification_url": "https://yoursite.com/webhook",
    "return_url": "https://yoursite.com/success"
  }'
Important: Amount should be in USDT (USD), not local currency. If your product costs 50,000 COP, convert to USD first: amount: 12.50 (approximately)

Response

{
  "status": "Success",
  "code": 200,
  "data": {
    "reference": "IP9BD7CJES6",
    "deposit_address": "TXYZabc123def456ghi789jkl012mno345pqr",
    "network": "TRC20",
    "amount": 50,
    "currency": "USD",
    "qr_code_url": "https://qr.88pay.io/ABC123.png"
  }
}

Customer Experience

1

Receive Address

Customer sees deposit address and QR code
2

Open Wallet

Opens their crypto wallet (Binance, Trust Wallet, etc.)
3

Send USDT

Sends exact USDT amount to addressCritical: Must send exact amount
4

Wait for Confirmations

Transaction appears in ~30 secondsConfirmed after 3-6 blocks (~3-10 minutes)
5

Payment Complete

Webhook sent when confirmedCustomer can return to site

Displaying Crypto Payment

Example UI

function CryptoPayment({ data }) {
  const [status, setStatus] = useState('pending');
  
  // Poll for payment status
  useEffect(() => {
    const interval = setInterval(async () => {
      const status = await checkPaymentStatus(data.reference);
      if (status === 'COMPLETED') {
        setStatus('completed');
        clearInterval(interval);
      }
    }, 10000); // Check every 10 seconds
    
    return () => clearInterval(interval);
  }, []);
  
  return (
    <div className="crypto-payment">
      <h2>Send USDT Payment</h2>
      
      <div className="amount-box">
        <label>Amount:</label>
        <div className="amount">
          {data.amount} USDT
        </div>
        <p className="warning">⚠️ Send EXACT amount</p>
      </div>
      
      <div className="network-badge">
        Network: {data.network}
      </div>
      
      <div className="qr-section">
        <img src={data.qr_code_url} alt="QR Code" />
        <p>Scan with your wallet app</p>
      </div>
      
      <div className="divider">OR</div>
      
      <div className="address-section">
        <label>Deposit Address:</label>
        <div className="address">
          <code>{data.deposit_address}</code>
          <button onClick={() => copy(data.deposit_address)}>
            Copy Address
          </button>
        </div>
      </div>
      
      <div className="instructions">
        <h3>How to pay:</h3>
        <ol>
          <li>Open your crypto wallet</li>
          <li>Select "Send" or "Transfer"</li>
          <li>Choose USDT on {data.network} network</li>
          <li>Paste address or scan QR code</li>
          <li>Enter amount: <strong>{data.amount} USDT</strong></li>
          <li>Confirm and send</li>
        </ol>
      </div>
      
      <div className="status">
        {status === 'pending' && (
          <div className="pending">
            <Spinner />
            <p>Waiting for payment...</p>
            <p className="small">This usually takes 3-10 minutes</p>
          </div>
        )}
        
        {status === 'completed' && (
          <div className="completed">
            <CheckIcon />
            <p>Payment confirmed!</p>
          </div>
        )}
      </div>
      
      <div className="warnings">
        <p>⚠️ Only send USDT on {data.network} network</p>
        <p>⚠️ Sending other coins or wrong network will result in loss of funds</p>
        <p>⚠️ Send exactly {data.amount} USDT</p>
      </div>
    </div>
  );
}

Important Warnings

Critical Information for Customers:Only send USDT - sending BTC, ETH, or other coins will result in permanent lossCorrect network - TRC20 address only accepts TRC20 USDT (not ERC20 or other networks)Exact amount - send precisely the amount shown (e.g., 50.00 USDT, not 49.99 or 50.01)No refunds for wrong coin, wrong network, or wrong amount

Transaction Limits

NetworkMinMaxFee
TRC2010 USDT100,000 USDT~1-2 USDT
ERC2010 USDT100,000 USDT~5-50 USDT (varies)

Confirmation Times

NetworkAppears InConfirmations NeededTotal Time
TRC2030 seconds19 blocks~3-5 minutes
ERC2030 seconds12 blocks~10-20 minutes

Webhook Notifications

    {
      "transaction_status": "PENDING",
      "transaction_reference": "IP9BD7CJES6",
      "transaction_payment_method": "USDT_TRC20"
    }
Meaning: Payment created, awaiting transfer

Best Practices

Poll for transaction status while customer waits
    // Check status every 10 seconds
    const checkStatus = setInterval(async () => {
      const status = await getTransactionStatus(reference);
      
      if (status.transaction_status === 'PROCESSING') {
        updateUI(`Confirmations: ${status.confirmations}/${status.confirmations_required}`);
      }
      
      if (status.transaction_status === 'COMPLETED') {
        clearInterval(checkStatus);
        redirectToSuccess();
      }
    }, 10000);
Make it impossible to miss which network to use
    <div className="network-warning">
      <AlertIcon />
      <h3>IMPORTANT: Use {network} Network Only</h3>
      <p>Sending on other networks will result in loss of funds</p>
    </div>
What if customer sends 49.50 instead of 50?
    // Webhook handler
    if (req.body.transaction_status === 'REJECTED' 
        && req.body.reason === 'partial_payment') {
      const sent = req.body.amount_received;
      const required = req.body.amount_required;
      const remaining = required - sent;
      
      // Offer to complete payment
      await sendEmail({
        to: customer.email,
        subject: 'Complete Your Payment',
        message: `You sent ${sent} USDT. Please send ${remaining} USDT more to complete.`
      });
    }

Testing

Sandbox Behavior

  1. Deposit address provided instantly
  2. Auto-completes after 3 minutes
  3. Webhook sent automatically
  4. No real crypto needed
// Test flow
const payment = await createCryptoPayment({
  amount: 50,
  currency: "USD"
});

console.log('Address:', payment.deposit_address);
console.log('Network:', payment.network);

// Wait 3 minutes
await sleep(180000);

// Webhook arrives with COMPLETED status

Common Issues

Problem: Sent 49 USDT instead of 50Solutions:
  • Accept as partial payment
  • Request remaining balance
  • Offer refund (minus fees)
Prevention:
  • Make amount extremely clear
  • Show exactly what to enter in wallet
  • Provide copy button for amount
Problem: Sent ERC20 to TRC20 address (or vice versa)Result: Funds lost (unrecoverable)Prevention:
  • Show BIG warning about network
  • Color-code network selection
  • Confirm network in UI multiple times
  • Show example of correct wallet selection
Possible causes:
  • Still processing in blockchain
  • Sent to wrong address
  • Low gas fee (ERC20)
Action:
  • Wait 20 minutes
  • Check block explorer with transaction hash
  • Verify address matches exactly
Solution: Provide instructions to buy
    <div className="help-section">
      <h3>Don't have USDT?</h3>
      <p>You can buy USDT on:</p>
      <ul>
        <li><a href="https://binance.com">Binance</a></li>
        <li><a href="https://coinbase.com">Coinbase</a></li>
        <li>Local exchange in your country</li>
      </ul>
    </div>

Next Steps