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
- Identity Management API
- Cash Deposit API
- Webhook Endpoints (optional)
- Deposit Location Search API (optional)
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
Request Parameters
Parameter | Type | Required | Description |
---|---|---|---|
referenceId | string | Yes | Your unique identifier for this user |
email | string | Yes | User's validated email address |
phone | string | Yes | User's validated phone number (e.g., "+15555551234") |
hasExistingIdv | boolean | Yes | Whether KYC has been previously completed |
idv | object | Yes | Identity 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
Parameter | Type | Required | Description |
---|---|---|---|
identityId | string | Yes | The unique identifier of the customer making the deposit |
amount | object | No | The amount and currency to deposit. Either amount or amountOut must be populated. |
amountOut | object | No | An alternative to amount which specifies how much $MOVEUSD must be minted into the deposit wallet after fees |
wallet | object | Yes | The target wallet where $MOVEUSD will be minted |
deviceIpAddress | object | No | IP address of the user device must be provided for cash deposits |
deviceLocation | object | Yes | Location 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 imagebarcode_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
Parameter | Type | Required | Description |
---|---|---|---|
latitude | number | Yes* | Customer's latitude coordinate |
longitude | number | Yes* | Customer's longitude coordinate |
zipcode | string | Yes* | Customer's ZIP code (alternative to lat/long) |
retailerId | string | No | Filter 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
Path Parameters
Parameter | Type | Required | Description |
---|---|---|---|
id | string | Yes | The 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
Updated about 1 month ago