Cash Deposits

Implement a complete cash deposit flow that allows your customers to deposit cash at retail locations.

This guide provides step-by-step instructions for integrating the CFX cash deposit functionality into your application. By following this guide, you'll enable your customers to deposit cash at 30,000+ retail locations nationwide.

Overview

What You'll Build

A complete cash deposit flow that includes:

  • Creating deposit requests with a specified amount
  • Generating and displaying barcodes for in-store scanning
  • Tracking deposit status through webhooks
  • Crediting customer accounts when deposits complete
  • Handling edge cases like expirations and cancellations

Key Integration Points

Prerequisites

Before starting this integration, ensure you have:

  • A CFX developer account with sandbox API credentials

  • Ability to display/render barcode images in your application

  • Optional: A Webhook endpoint set up to receive deposit notifications
    Webhook configuration guide →

Implementation Steps

Step 1: Register User Identity

Before creating a cash deposit request, you must first register the user's identity in the CFX system. This is required to associate deposits with a specific user and to ensure compliance with regulatory requirements.

API Endpoint

POST /v1/identity

Register Identity API Spec →

Request Parameters

ParameterTypeRequiredDescription
referenceIdstringYesYour unique identifier for this user
emailstringYesUser's validated email address
phonestringYesUser's validated phone number (e.g., "+15555551234")
hasExistingIdvbooleanYesWhether KYC has been previously completed
idvobjectYesIdentity verification details

JavaScript Example

// Register user identity
async function registerUserIdentity(userData) {
  try {
    const response = await fetch('https://sandbox-api.moveusd.com/v1/identity', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-KEY': process.env.MOVEUSD_API_KEY,
        'X-API-SECRET': process.env.MOVEUSD_API_SECRET
      },
      body: JSON.stringify({
        referenceId: userData.referenceId,
        email: userData.email,
        phone: userData.phone,
        hasExistingIdv: false, // Set to true if user already completed KYC
        idv: {
          // For new users who need to complete IDV
          countryOfResidence: 'US'
        }
      })
    });

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(`API Error: ${errorData.message}`);
    }

    return await response.json();
  } catch (error) {
    console.error('Failed to register user identity:', error);
    throw error;
  }
}

// Example usage
try {
  const identity = await registerUserIdentity({
    referenceId: 'user123',
    email: '[email protected]',
    phone: '+15555551234'
  });
  
  // Store the identity ID for future deposit requests
  const identityId = identity.id;
  console.log(`User registered with identity ID: ${identityId}`);
  
  // Note: The user will need to complete identity verification if they haven't already
  if (identity.status === 'REQUIRES_IDV') {
    // Provide user with a link to complete verification
    console.log('User needs to complete identity verification');
  }
  
} catch (error) {
  showErrorMessage('Unable to register user identity. Please try again.');
}

Step 2: Create a Cash Deposit Request

The next step is to create a cash deposit request when a customer wants to deposit cash at a retail location.

API Endpoint

POST /v1/deposit/cash

Create Cash Deposit API Spec →

Request Parameters

ParameterTypeRequiredDescription
identityIdstringYesThe unique identifier of the customer making the deposit
amountobjectNoThe amount and currency to deposit. Either amount or amountOut must be populated.
amountOutobjectNoAn alternative to amount which specifies how much $MOVEUSD must be minted into the deposit wallet after fees
walletobjectYesThe target wallet where $MOVEUSD will be minted
deviceIpAddressobjectNoIP address of the user device must be provided for cash deposits
deviceLocationobjectYesLocation of the user device must be provided for cash deposits

Response

Upon successful creation, the API returns the deposit request details including a barcode for in-store scanning.

JavaScript Example

// Create a cash deposit request
async function createCashDeposit(identityId, amountUsd) {
  try {
    const response = await fetch('https://sandbox-api.moveusd.com/v1/deposit/cash', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-KEY': process.env.MOVEUSD_API_KEY,
        'X-API-SECRET': process.env.MOVEUSD_API_SECRET
      },
      body: JSON.stringify({
        identityId: identityId,
        amount: {
          amount: 100,
          currency: "USD",
        },
        // other fields as required
      })
    });

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(`API Error: ${errorData.message}`);
    }

    return await response.json();
  } catch (error) {
    console.error('Failed to create cash deposit:', error);
    throw error;
  }
}

// Example usage
try {
  const depositRequest = await createCashDeposit('id_1234567890', 50); // $50 deposit
  
  // Store the deposit request ID for future reference
  saveDepositRequestId(depositRequest.id);
  
  // Display the barcode to the customer
  displayBarcode(depositRequest.barcode, depositRequest.barcode_number);
  
  // Show deposit details to the customer
  displayDepositDetails({
    amount: depositRequest.amount / 100, // Convert from cents
    fee: depositRequest.fee / 100, // Convert from cents
    expiresAt: new Date(depositRequest.expires_at)
  });
} catch (error) {
  showErrorMessage('Unable to create deposit request. Please try again.');
}

Step 3: Display the Barcode to the Customer

Once you have created a deposit request, you need to display the barcode to the customer so they can present it at a retail location.

Implementation Details

The API response includes:

  • barcode: A base64-encoded PNG image
  • barcode_number: The numeric representation of the barcode

You should display both the barcode image and the numeric representation to ensure the cashier can manually enter the code if scanning fails.

Important Information to Display

  • Deposit amount (if prescribed)
  • Fee amount (if applicable)
  • Expiration time
  • Instructions for completing the deposit at a retail location

React Example

// CashDepositBarcode.jsx
import React, { useState, useEffect } from 'react';
import { format } from 'date-fns';

const CashDepositBarcode = ({ depositRequest }) => {
  const [timeRemaining, setTimeRemaining] = useState('');
  
  // Calculate and update time remaining until expiration
  useEffect(() => {
    const updateTimeRemaining = () => {
      const now = new Date();
      const expiresAt = new Date(depositRequest.expires_at);
      const diffMs = expiresAt - now;
      
      if (diffMs <= 0) {
        setTimeRemaining('Expired');
        return;
      }
      
      const hours = Math.floor(diffMs / (1000 * 60 * 60));
      const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
      setTimeRemaining(`${hours}h ${minutes}m`);
    };
    
    updateTimeRemaining();
    const interval = setInterval(updateTimeRemaining, 60000); // Update every minute
    
    return () => clearInterval(interval);
  }, [depositRequest.expires_at]);
  
  return (
    <div className="cash-deposit-barcode">
      <h2>Cash Deposit Barcode</h2>
      
      <div className="barcode-container">
        <img 
          src={depositRequest.barcode} 
          alt="Cash Deposit Barcode" 
          className="barcode-image"
        />
        <div className="barcode-number">
          <span>{depositRequest.barcode_number}</span>
          <p className="barcode-help">If scanning fails, cashier can enter this number</p>
        </div>
      </div>
      
      <div className="deposit-details">
        <div className="detail-row">
          <span>Deposit Amount:</span>
          <span className="amount">${(depositRequest.amount / 100).toFixed(2)}</span>
        </div>
        
        {depositRequest.fee > 0 && (
          <div className="detail-row">
            <span>Service Fee:</span>
            <span className="fee">${(depositRequest.fee / 100).toFixed(2)}</span>
          </div>
        )}
        
        <div className="detail-row">
          <span>Total to Pay:</span>
          <span className="total">${((depositRequest.amount + depositRequest.fee) / 100).toFixed(2)}</span>
        </div>
        
        <div className="detail-row expiration">
          <span>Expires in:</span>
          <span className="time-remaining">{timeRemaining}</span>
          <p className="expiration-date">
            {format(new Date(depositRequest.expires_at), 'MMM d, yyyy h:mm a')}
          </p>
        </div>
      </div>
      
      <div className="instructions">
        <h3>How to Complete Your Deposit</h3>
        <ol>
          <li>Visit any participating retail location (Walmart, 7-Eleven, etc.)</li>
          <li>Show this barcode to the cashier</li>
          <li>Hand the cashier the cash amount shown above</li>
          <li>Keep your receipt until deposit is confirmed</li>
          <li>Your account will be credited within minutes</li>
        </ol>
      </div>
      
      <button className="find-locations-btn" onClick={onFindLocations}>
        Find Nearby Locations
      </button>
    </div>
  );
};

export default CashDepositBarcode;

Step 4: Implement the Location Finder

To improve the customer experience, you can implement a location finder to help customers find nearby retail locations where they can make their cash deposit.

API Endpoint

GET /v1/deposit/cash/location

Search Cash Deposit Locations API Spec →

Request Parameters

ParameterTypeRequiredDescription
latitudenumberYes*Customer's latitude coordinate
longitudenumberYes*Customer's longitude coordinate
zipcodestringYes*Customer's ZIP code (alternative to lat/long)
retailerIdstringNoFilter by retailer name (e.g., "WALMART", "7ELEVEN")
  • Either latitude/longitude pair OR zip_code is required

Response

The API returns a list of nearby retail locations with their details, including address, distance, and operating hours.

JavaScript Example

// Function to find nearby retail locations
async function findNearbyLocations(params) {
  try {
    // Build query string from params
    const queryParams = new URLSearchParams();
    
    if (params.latitude && params.longitude) {
      queryParams.append('latitude', params.latitude);
      queryParams.append('longitude', params.longitude);
    } else if (params.zipCode) {
      queryParams.append('zip_code', params.zipCode);
    } else {
      throw new Error('Either latitude/longitude or zip_code is required');
    }
    
    if (params.radius) queryParams.append('radius', params.radius);
    if (params.limit) queryParams.append('limit', params.limit);
    if (params.retailer) queryParams.append('retailer', params.retailer);
    
    const url = `https://sandbox-api.moveusd.com/v1/deposit/cash/location?${queryParams.toString()}`;
    
    const response = await fetch(url, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        'X-API-KEY': process.env.MOVEUSD_API_KEY,
        'X-API-SECRET': process.env.MOVEUSD_API_SECRET
      }
    });

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(`API Error: ${errorData.message}`);
    }

    return await response.json();
  } catch (error) {
    console.error('Failed to find nearby locations:', error);
    throw error;
  }
}

// Example usage with browser geolocation
function showNearbyLocations() {
  if (navigator.geolocation) {
    navigator.geolocation.getCurrentPosition(
      // Success callback
      async (position) => {
        try {
          const locations = await findNearbyLocations({
            latitude: position.coords.latitude,
            longitude: position.coords.longitude,
            radius: 5, // 5 miles
            limit: 10
          });
          
          displayLocationsOnMap(locations.locations);
          displayLocationsList(locations.locations);
        } catch (error) {
          showErrorMessage('Unable to find nearby locations. Please try again.');
        }
      },
      // Error callback
      (error) => {
        console.error('Geolocation error:', error);
        // Fall back to ZIP code entry
        promptForZipCode();
      }
    );
  } else {
    // Browser doesn't support geolocation
    promptForZipCode();
  }
}

// Function to display locations in a list
function displayLocationsList(locations) {
  const listContainer = document.getElementById('locations-list');
  listContainer.innerHTML = '';
  
  if (locations.length === 0) {
    listContainer.innerHTML = '<p>No locations found in your area.</p>';
    return;
  }
  
  locations.forEach(location => {
    const locationElement = document.createElement('div');
    locationElement.className = 'location-item';
    
    locationElement.innerHTML = `
      <h3>${location.retailer_name}</h3>
      <p class="address">
        ${location.address}<br>
        ${location.city}, ${location.state} ${location.zip_code}
      </p>
      <p class="distance">${location.distance.toFixed(1)} miles away</p>
      <p class="hours">${formatHours(location.hours)}</p>
      <button class="directions-btn" onclick="openDirections('${encodeURIComponent(location.address + ', ' + location.city + ', ' + location.state + ' ' + location.zip_code)}')">
        Get Directions
      </button>
    `;
    
    listContainer.appendChild(locationElement);
  });
}

// Helper function to format operating hours
function formatHours(hours) {
  if (!hours) return 'Hours not available';
  
  // Format hours based on your needs
  return hours;
}

// Open directions in maps application/website
function openDirections(address) {
  const mapsUrl = `https://maps.google.com/maps?q=${address}`;
  window.open(mapsUrl, '_blank');
}

Step 5: Set Up Webhook Handler for Deposit Status Updates

To receive real-time updates on the status of cash deposits, you need to implement a webhook handler that processes deposit status notifications from MoveUSD.

Webhook Events

Your webhook endpoint will receive events as the cash deposit is processed. Details regarding these events are docuemented at:

Deposit Status Updated Webhook

Implementation Requirements

Your webhook handler should:

  • Verify the webhook signature to ensure it's from CFX (if configured
  • Process events based on their type and status
  • Update your database with the latest deposit status
  • Trigger appropriate notifications to the customer
  • Handle any business logic based on deposit completion or failure

Webhook Verification

Note that CFX can configure webhooks to utilise an API key (which you will provide as part of onboarding) and/or sign messages with an HMAC signature to ensure the webhook is authentic.

Node.js (Express) Example

// webhook-handler.js
const express = require('express');
const crypto = require('crypto');
const bodyParser = require('body-parser');

const app = express();

// Use raw body parser for webhook signature verification
app.use('/webhooks/moveusd', 
  bodyParser.json({
    verify: (req, res, buf) => {
      req.rawBody = buf.toString();
    }
  })
);

// Verify webhook signature
function verifyWebhookSignature(req) {
  const signature = req.headers['x-moveusd-signature'];
  const webhookSecret = process.env.MOVEUSD_WEBHOOK_SECRET;
  
  if (!signature || !webhookSecret) {
    return false;
  }
  
  const hmac = crypto.createHmac('sha256', webhookSecret);
  const expectedSignature = hmac.update(req.rawBody).digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Webhook handler for MoveUSD events
app.post('/webhooks/moveusd', async (req, res) => {
  try {
    // Verify webhook signature
    if (!verifyWebhookSignature(req)) {
      console.error('Invalid webhook signature');
      return res.status(401).send('Invalid signature');
    }
    
    const event = req.body;
    
    // Acknowledge receipt of the webhook immediately
    // This prevents MoveUSD from retrying the webhook
    res.status(200).send('Webhook received');
    
    // Process the event based on type
    switch (event.event) {
      case 'deposit.created':
        await handleDepositCreated(event.data);
        break;
        
      case 'deposit.in_progress':
        await handleDepositInProgress(event.data);
        break;
        
      case 'deposit.completed':
        await handleDepositCompleted(event.data);
        break;
        
      case 'deposit.cancelled':
        await handleDepositCancelled(event.data);
        break;
        
      case 'deposit.expired':
        await handleDepositExpired(event.data);
        break;
        
      case 'deposit.failed':
        await handleDepositFailed(event.data);
        break;
        
      default:
        console.log(`Unhandled event type: ${event.event}`);
    }
  } catch (error) {
    console.error('Error processing webhook:', error);
    // We've already sent a 200 response, so we just log the error
  }
});

// Handler functions for different event types
async function handleDepositCreated(data) {
  console.log('Deposit created:', data.id);
  // Update deposit status in your database
  await updateDepositStatus(data.id, 'PENDING');
}

async function handleDepositInProgress(data) {
  console.log('Deposit in progress:', data.id);
  // Update deposit status in your database
  await updateDepositStatus(data.id, 'IN_PROGRESS');
  
  // Notify the customer that their barcode has been scanned
  await sendCustomerNotification(
    data.identity_id,
    'deposit_in_progress',
    'Your cash deposit is being processed at the retail location.'
  );
}

async function handleDepositCompleted(data) {
  console.log('Deposit completed:', data.id);
  
  try {
    // Update deposit status in your database
    await updateDepositStatus(data.id, 'COMPLETED');
    
    // Credit the customer's account balance
    await creditCustomerAccount(
      data.identity_id,
      data.amount,
      {
        deposit_id: data.id,
        deposit_type: 'CASH',
        retailer: data.retailer
      }
    );
    
    // Notify the customer of successful deposit
    await sendCustomerNotification(
      data.identity_id,
      'deposit_success',
      `Your cash deposit of $${(data.amount / 100).toFixed(2)} has been successfully processed and added to your account.`
    );
    
    // Record the transaction for reporting
    await recordCompletedDeposit(data);
  } catch (error) {
    console.error('Error processing completed deposit:', error);
    // Implement retry or manual review process
    await flagDepositForReview(data.id, error.message);
  }
}

async function handleDepositCancelled(data) {
  console.log('Deposit cancelled:', data.id);
  // Update deposit status in your database
  await updateDepositStatus(data.id, 'CANCELLED');
  
  // Notify the customer
  await sendCustomerNotification(
    data.identity_id,
    'deposit_cancelled',
    'Your cash deposit request has been cancelled.'
  );
}

async function handleDepositExpired(data) {
  console.log('Deposit expired:', data.id);
  // Update deposit status in your database
  await updateDepositStatus(data.id, 'EXPIRED');
  
  // Notify the customer
  await sendCustomerNotification(
    data.identity_id,
    'deposit_expired',
    'Your cash deposit request has expired. Please create a new deposit request if you still wish to deposit cash.'
  );
}

async function handleDepositFailed(data) {
  console.log('Deposit failed:', data.id, data.failure_reason);
  // Update deposit status in your database
  await updateDepositStatus(data.id, 'FAILED', data.failure_reason);
  
  // Notify the customer
  await sendCustomerNotification(
    data.identity_id,
    'deposit_failed',
    `Your cash deposit could not be processed. Reason: ${data.failure_reason}`
  );
  
  // Flag for customer support if needed
  await flagDepositForReview(data.id, data.failure_reason);
}

// Helper functions (implementation depends on your specific app)
async function updateDepositStatus(depositId, status, failureReason = null) {
  // Update deposit record in your database
  // Implementation depends on your database and ORM
}

async function creditCustomerAccount(identityId, amount, metadata) {
  // Add the deposited amount to the customer's account balance
  // Implementation depends on your account management system
}

async function sendCustomerNotification(identityId, type, message) {
  // Send push notification, email, or in-app message to the customer
  // Implementation depends on your notification system
}

async function recordCompletedDeposit(data) {
  // Record the deposit for accounting and reporting purposes
  // Implementation depends on your reporting system
}

async function flagDepositForReview(depositId, reason) {
  // Flag deposit for manual review by customer support
  // Implementation depends on your support workflow
}

// Start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Webhook server listening on port ${PORT}`);
});

Step 5: Implement Deposit Cancellation (Optional)

In some cases, customers may want to cancel a pending deposit request. Implementing a cancellation feature provides flexibility and improves the customer experience.

API Endpoint

POST /v1/deposit/cash/{id}/cancel

Cancel Cash Deposit Request

Path Parameters

ParameterTypeRequiredDescription
idstringYesThe ID of the deposit request to cancel

Response

The API returns the updated deposit request with a status of "CANCELLED".

Important Notes

  • Only deposits with a status of "PENDING" can be cancelled
  • Once a barcode has been scanned at a retail location (status "IN_PROGRESS"), the deposit cannot be cancelled

JavaScript Example

// Function to cancel a cash deposit request
async function cancelCashDeposit(depositId) {
  try {
    const response = await fetch(`https://sandbox-api.moveusd.com/v1/deposit/cash/${depositId}/cancel`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-KEY': process.env.MOVEUSD_API_KEY,
        'X-API-SECRET': process.env.MOVEUSD_API_SECRET
      }
    });

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(`API Error: ${errorData.message}`);
    }

    return await response.json();
  } catch (error) {
    console.error('Failed to cancel cash deposit:', error);
    throw error;
  }
}

// Example usage with confirmation dialog
function showCancellationConfirmation(depositId) {
  // Display a confirmation dialog to the user
  if (confirm('Are you sure you want to cancel this deposit request?')) {
    cancelDepositRequest(depositId);
  }
}

async function cancelDepositRequest(depositId) {
  try {
    // Show loading indicator
    showLoadingSpinner();
    
    // Call the API to cancel the deposit
    const result = await cancelCashDeposit(depositId);
    
    // Hide loading indicator
    hideLoadingSpinner();
    
    // Update the UI to show the deposit is cancelled
    updateDepositStatusUI(depositId, 'CANCELLED');
    
    // Show success message
    showSuccessMessage('Your deposit request has been cancelled.');
    
    // Optionally, redirect to another page
    // window.location.href = '/deposits';
  } catch (error) {
    // Hide loading indicator
    hideLoadingSpinner();
    
    // Show error message based on the type of error
    if (error.message.includes('already in progress')) {
      showErrorMessage('This deposit cannot be cancelled because it is already being processed at a retail location.');
    } else if (error.message.includes('already completed')) {
      showErrorMessage('This deposit cannot be cancelled because it has already been completed.');
    } else {
      showErrorMessage('Unable to cancel deposit request. Please try again or contact support.');
    }
  }
}

// UI helper functions
function showLoadingSpinner() {
  // Show loading spinner implementation
}

function hideLoadingSpinner() {
  // Hide loading spinner implementation
}

function updateDepositStatusUI(depositId, status) {
  // Update UI to reflect new deposit status
  const statusElement = document.querySelector(`#deposit-${depositId} .status`);
  if (statusElement) {
    statusElement.textContent = status;
    statusElement.className = `status status-${status.toLowerCase()}`;
  }
  
  // If the deposit is cancelled, update other UI elements
  if (status === 'CANCELLED') {
    // Hide the barcode
    const barcodeElement = document.querySelector(`#deposit-${depositId} .barcode-container`);
    if (barcodeElement) {
      barcodeElement.style.display = 'none';
    }
    
    // Show cancelled message
    const messageElement = document.querySelector(`#deposit-${depositId} .message`);
    if (messageElement) {
      messageElement.textContent = 'This deposit request has been cancelled.';
      messageElement.className = 'message message-cancelled';
    }
    
    // Hide cancel button
    const cancelButton = document.querySelector(`#deposit-${depositId} .cancel-btn`);
    if (cancelButton) {
      cancelButton.style.display = 'none';
    }
    
    // Show create new button
    const newDepositButton = document.querySelector(`#deposit-${depositId} .new-deposit-btn`);
    if (newDepositButton) {
      newDepositButton.style.display = 'block';
    }
  }
}

function showSuccessMessage(message) {
  // Show success message implementation
}

function showErrorMessage(message) {
  // Show error message implementation
}

Testing Your Integration

Before going live with your cash deposit integration, thoroughly test it in the sandbox environment:

1. Test Deposit Creation

  • Create deposit requests with various amounts
  • Verify barcode generation and display
  • Test with and without optional parameters

2. Test Webhook Processing

  • Verify your webhook handler correctly processes each event
  • Test error handling and recovery scenarios

3. Test Location Finder

  • Test with different coordinates and ZIP codes
  • Test edge cases like remote locations with a few retailers

4. Test Deposit Cancellation

  • Test cancelling deposits in different states
  • Verify proper error handling for invalid cancellations
  • Test UI updates after cancellation

5. End-to-End Testing

  • Simulate complete deposit flows from creation to completion
  • Test integration with your account balance system
  • Verify customer notifications at each step

Best Practices

Security

  • Implement webhook API keys or event signatures to prevent fraud
  • Implement proper access controls within your application
  • Store deposit records securely and in compliance with regulations
  • Avoid storing sensitive customer information

User Experience

  • Optimize barcode display for both web and mobile devices
  • Provide clear, step-by-step instructions for completing deposits
  • Provide a link to the CFX hosted 'how-to' video on cash deposits
  • Send timely notifications at each stage of the deposit process
  • Make sure barcodes are easily scannable (proper size and resolution)

Technical Implementation

  • Implement proper error handling and retry mechanisms
  • Design your database schema to efficiently track deposit status
  • Set up monitoring and alerting for webhook failures

Business Logic

  • Clearly communicate fees to customers before they make a deposit
  • Implement proper accounting for deposits in your financial systems
  • Consider offering incentives for larger deposits to optimize fee structure