By: Gift Uhiene
Real-time payment (RTP) systems enable instant fund transfers between bank accounts, making them essential for modern financial applications. For each transaction, funds and payment information are exchanged and settled within seconds of the transaction initiation. RTP service is continuous 24/7 and available all year round.
Rapyd is a global fintech-as-a-service provider that enables businesses to perform instant fund transfers in local currencies and maintain digital wallets globally. With its extensive API offerings, developers can integrate Rapyd finance capabilities into their applications to effortlessly manage cross-border payments, multicurrency treasury, card issuing, fraud protection, and compliance.
In this tutorial, you’ll learn how to build an RTP-enabled Node.js app using Rapyd and WebSockets, covering setup, API integration, and transaction handling.
You can explore the complete code and follow along with the implementation via GitHub.
Prerequisites
Before getting started, you should have the following:
- A Rapyd account; if you’re signing up for the first time, you’ll need to verify your email and complete the required verification steps
- Node.js installed on your system
- A basic understanding of JavaScript, React.js, and APIs
Getting Rapyd API Credentials
Log in to the Rapyd dashboard and navigate to the Developers > API access control section:
Store these keys securely as you’ll need them to authenticate API requests:
Building a Real-Time Payment Backend
In this section, you’ll build a backend system that integrates the Rapyd API and WebSockets to create a real-time payment flow. The system will allow users to create wallets, initiate transfers, and receive real-time updates on transfer statuses. Here’s what we’ll cover:
- Setting up the Rapyd API client to send secure requests
- Creating a WebSocket server to broadcast real-time updates to clients
- Creating wallet controllers to communicate with Rapyd APIs
- Building the Express server to tie everything together
Project Setup
Clone the starter branch of the project with the command below:
git clone --branch starter --single-branch https://github.com/Rapyd-Samples/rtp-rapyd.git
This is the structure of the backend
directory:
rtp-rapyd/
│── backend/
│ │── node_modules/
│ │── src/
│ │ │── config/
│ │ │ ├── rapydClient.js # Handles API requests to Rapyd
│ │ │── controllers/
│ │ │ ├── walletController.js # Logic for wallet
│ │ │── routes/
│ │ │ ├── walletRoutes.js # Defines API routes for wallet actions
│ │ │── server.js # Main entry point
│ │ │── websocket.js # WebSocket logic for real-time updates
│ │── .env # API keys and secrets
│ │── package.json # Project dependencies
│ │── README.md # Documentation
Installing Dependencies
Navigate to the backend
directory in your terminal and install dependencies:
cd backend
npm install
Then, run the following command in the backend
directory of your terminal to start the server:
npm run dev
Configuring the Rapyd API Client
To interact with Rapyd’s API, you’ll need a client that handles authentication, request signing, and HTTP communication.
Rapyd’s API requires specified header parameters for each request to verify that the requester is an authorized user and to protect against data tampering.
Go to the backend/src/config/rapydClient.js
file and include the code below to set up a Rapyd API client. This will allow you to securely authenticate and make HTTP requests to Rapyd’s API:
require("dotenv").config();
const https = require("https");
const crypto = require("crypto");
const accessKey = process.env.RAPYD_ACCESS_KEY;
const secretKey = process.env.RAPYD_SECRET_KEY;
async function makeRequest(method, urlPath, body = null) {
try {
const httpMethod = method.toUpperCase();
const httpBaseURL = "sandboxapi.rapyd.net";
const salt = generateRandomString(8);
const idempotency = new Date().getTime().toString();
const timestamp = Math.round(new Date().getTime() / 1000);
const signature = sign(httpMethod, urlPath, salt, timestamp, body);
const options = {
hostname: httpBaseURL,
port: 443,
path: urlPath,
method: httpMethod,
headers: {
"Content-Type": "application/json",
salt: salt,
timestamp: timestamp,
signature: signature,
access_key: accessKey,
idempotency: idempotency,
},
};
return await httpRequest(options, body);
} catch (error) {
console.error("Error generating request options", error);
throw error;
}
}
function sign(method, urlPath, salt, timestamp, body) {
try {
let bodyString = body ? JSON.stringify(body) : "";
let toSign =
method.toLowerCase() + urlPath + salt + timestamp + accessKey + secretKey + bodyString;
let hash = crypto.createHmac("sha256", secretKey);
hash.update(toSign);
return Buffer.from(hash.digest("hex")).toString("base64");
} catch (error) {
console.error("Error generating signature", error);
throw error;
}
}
function generateRandomString(size) {
return crypto.randomBytes(size).toString("hex");
}
async function httpRequest(options, body) {
return new Promise((resolve, reject) => {
try {
let bodyString = body ? JSON.stringify(body) : "";
const req = https.request(options, (res) => {
let response = { statusCode: res.statusCode, headers: res.headers, body: "" };
res.on("data", (data) => {
response.body += data;
});
res.on("end", () => {
response.body = response.body ? JSON.parse(response.body) : {};
if (response.statusCode !== 200) return reject(response);
return resolve(response);
});
});
req.on("error", (error) => reject(error));
req.write(bodyString);
req.end();
} catch (err) {
reject(err);
}
});
}
module.exports = { makeRequest };
The code above is from the Node.js example on the Rapyd documentation. The sign
function generates a signature for each request using your accessKey
and secretKey
, ensuring secure communication with the API. To prevent duplicate transactions, every request includes a unique idempotency
key.
Before moving forward, create a .env
file in the root of the backend
directory to store your Rapyd credentials:
RAPYD_ACCESS_KEY=your_access_key
RAPYD_SECRET_KEY=your_secret_key
Setting Up WebSockets
WebSockets enable bidirectional communication between the server and clients, allowing real-time updates without the need for constant polling. Implementing WebSockets in this project will keep the connection open so that when a change happens, the server will immediately update users without them having to refresh the page. To implement this, update backend/src/websocket.js
file with the code below:
const WebSocket = require("ws");
let wss;
exports.setupWebSocketServer = (server) => {
wss = new WebSocket.Server({ noServer: true });
server.on("upgrade", (request, socket, head) => {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit("connection", ws, request);
});
});
wss.on("connection", (ws) => {
console.log("âś… New WebSocket connection");
ws.on("message", (message) => {
console.log("đź“© Received:", message);
ws.send(`Server received: ${message}`);
});
ws.on("close", () => console.log("❌ Client disconnected"));
});
};
exports.notifyUsers = (transaction) => {
if (!wss) return;
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(transaction));
}
});
};
The setupWebSocketServer(server)
function initializes a WebSocket server that handles upgrades from an HTTP server, listens for client connections, and processes incoming messages. The notifyUsers(transaction)
function will broadcast real-time updates to all connected clients by sending transaction data as a JSON string.
Update backend/src/server.js
file with the code below to create a server for the application:
const express = require("express");
const http = require("http");
const cors = require("cors");
const { setupWebSocketServer } = require("./websocket");
const walletRoutes = require("./routes/walletRoutes");
const app = express();
const server = http.createServer(app);
app.use(cors());
app.use(express.json());
app.use("/api/wallet", walletRoutes);
setupWebSocketServer(server);
server.listen(4000, () => console.log("Server running on port 4000"));
Rapyd Wallet: Managing Real-Time Transactions and Fund Transfers
When building a real-time payment system, one of the key components you’ll need is a reliable and flexible wallet system. Rapyd Wallet allows businesses and developers to create, manage, and transfer funds between wallets in real time by acting as a financial hub where money can be moved across borders, currencies, and payment methods.
Some of its key features include:
- Multicurrency support: Each wallet can hold multiple accounts, each with its own currency and balance.
- Custom branding: You can brand the wallets with your own logo and design, making it feel like a native part of your application.
- Client and user wallets: A two-tier structure with client wallets for businesses and user wallets for individuals (such as sellers or employees).
- FX capabilities: Rapyd Wallet integrates with Rapyd’s foreign exchange (FX) features, allowing you to convert currencies at competitive rates.
- Split payments: You can split payments between multiple wallets, which is useful for marketplaces or platforms with shared revenue models.
Building the Wallet Controller
To manage all wallet-related operations, let’s build a wallet controller. This controller will handle interactions with the Rapyd API and centralize the logic for wallet management.
Open backend/src/controllers/walletController.js
and update createWallet
with the code below to implement the function to handle wallet creation and initial funding using Rapyd’s API:
exports.createWallet = async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { first_name, last_name, email, type } = req.body;
const timestamp = Date.now();
const emailPrefix = email.split("@")[0];
const ewalletReferenceId = `${emailPrefix}_${timestamp}`;
// Construct the wallet creation payload
const walletData = {
first_name,
last_name,
ewallet_reference_id: ewalletReferenceId,
email,
type,
contact: {
email,
first_name,
last_name,
contact_type: `${type === "company" ? "business" : "personal"}`,
country: "SG",
address: {
name: `${first_name} ${last_name}`,
line_1: "123 Main Street",
country: "SG",
},
country: "SG",
date_of_birth: "2000-11-22",
metadata: {
merchant_defined: true,
},
},
};
const walletResponse = await makeRequest(
"POST",
"/v1/ewallets",
walletData
);
const walletId = walletResponse.body.data.id;
const fundResponse = await makeRequest("POST", "/v1/account/deposit", {
ewallet: walletId,
currency: "SGD",
amount: 20,
});
res.json({
wallet: walletResponse.body.data,
initial_deposit: fundResponse.body,
});
} catch (error) {
handleError(res, error);
}
};
This code performs the following steps:
- Validates the request using
express-validator
to ensure all required fields are present. - Extracts user details from the request body.
- Generates a unique
ewallet_reference_id
by combining the email prefix and a timestamp. - Constructs a
walletData
object containing user details, contact information, address, and metadata. Some fields, like the country (SG
), are hard-coded but can be customized. - Creates a wallet by sending a POST request to Rapyd’s
/v1/ewallets
endpoint. The response includes awalletId
that uniquely identifies the wallet. - Funds the wallet with 20 SGD by making a deposit request to
/v1/account/deposit
. This step is specific to sandbox mode, as real deposits require actual funding sources in production. - Returns a JSON response with the wallet details and deposit transaction.
Next, in the same file, update the retrieveWallet
function to fetch details about a user wallet:
exports.retrieveWallet = async (req, res) => {
try {
const accountId = req.params.id;
if (!accountId) {
return res.status(400).json({ error: "Account ID is required" });
}
const result = await makeRequest("GET", `/v1/ewallets/${accountId}`);
res.json(result.body.data);
} catch (error) {
handleError(res, error);
}
};
The function above extracts accountId
from the request parameters and makes a GET request to Rapyd’s API to retrieve a wallet’s details.
Transferring Funds Between Wallets
Rapyd allows funds transfers between wallets; if the destination wallet does not have the specified currency, an account for that currency is automatically created. When a transfer is initiated, the status is marked as “PEN” until accepted by the transferee, using the Set Transfer Response API. Unaccepted transfers can, in turn, be canceled by the sender using the same API.
To initiate a peer-to-peer wallet transfer, update the transferFunds
function in the backend/src/controllers/walletController.js
file:
exports.transferFunds = async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { senderWalletId, receiverWalletId, amount } = req.body;
const response = await makeRequest("POST", "/v1/ewallets/transfer", {
source_ewallet: senderWalletId,
destination_ewallet: receiverWalletId,
currency: "SGD",
amount,
});
const transactionId = response.body.data.id;
if (!transactionId) {
return res.status(500).json({ error: "Transaction ID not found." });
}
notifyUsers({
sender: senderWalletId,
receiver: receiverWalletId,
id: transactionId,
currency: "SGD",
amount,
status: "pending",
variant: "TRANSFER_FUNDS",
});
res.json({
message: "Transfer initiated. Waiting for confirmation...",
transactionId,
});
} catch (error) {
handleError(res, error);
}
};
The function sends a request to Rapyd’s /v1/ewallets/transfer
endpoint to transfer funds between wallets, using SGD as the currency. It extracts the transaction ID, notifies all WebSocket clients about the transfer, and responds to the client confirming the request was initiated.
Finally, update the respondToTransfer
function in the same file to enable both the sender and transferee to respond to a transaction:
exports.respondToTransfer = async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { status, id } = req.body;
if (!["accept", "decline", "cancel"].includes(status)) {
return res.status(400).json({
error: "Invalid status. Must be 'accept', 'decline', or 'cancel'.",
});
}
const response = await makeRequest(
"POST",
"/v1/ewallets/transfer/response",
{
id,
status,
}
);
if (response.body.status.status !== "SUCCESS") {
return res.status(500).json({ error: `Failed to ${status} transfer.` });
}
notifyUsers({
sender: response.body.data.source_ewallet_id,
receiver: response.body.data.destination_ewallet_id,
id,
currency: response.body.data.currency_code,
amount: response.body.data.amount,
status:
status === "accept"
? "Accepted"
: status === "decline"
? "Declined"
: status === "cancel"
? "Cancelled"
: status.charAt(0).toUpperCase() + status.slice(1),
variant: "RESPONDS_TO_TRANSFER",
});
res.json({
message: `Transfer ${status}ed successfully.`,
status: response.body.data.status,
});
} catch (error) {
handleError(res, error);
}
};
The function extracts the status
and id
from the request, validates the status
(accept
, decline
, or cancel
), and sends a request to Rapyd’s /v1/ewallets/transfer/response
endpoint to update the transfer. It also notifies the WebSocket clients about the transaction.
Connecting the Frontend
Now that the backend is set up, let’s connect the frontend application to interact with the backend and provide real-time updates to users.
Note: Most of the frontend components are already provided in the starter project, so this example doesn’t focus on building them from scratch. Instead, you’ll concentrate on integrating the frontend with the backend and enabling real-time functionality.
Installing Dependencies
In another terminal, navigate to the frontend
directory and install dependencies:
cd frontend
npm install
Run npm run dev
to run the React.js project.
The createWallet Function
Go to the frontend/src/components/create-wallet-form.jsx
file and update createWallet
with the code below. This function sends a POST request to the backend to create a wallet and stores the wallet ID in local storage:
const createWallet = async (e) => {
e.preventDefault();
setLoading(true);
try {
const response = await axios.post(
"http://localhost:4000/api/wallet/create",
userData
);
localStorage.setItem("ewallet", response.data.wallet.id);
await fetchUser();
} catch (error) {
console.error("Failed to create wallet:", error);
} finally {
setLoading(false);
}
};
The transferFunds Function
Let’s implement the function to transfer funds between wallets. Update transferFunds
in the frontend/src/components/transfer-form.jsx
file. This function sends a POST request to the backend to initiate a transfer between two wallets:
const transferFunds = async (e) => {
e.preventDefault();
setLoading(true);
try {
await axios.post("http://localhost:4000/api/wallet/transfer", {
senderWalletId: walletData.id,
receiverWalletId: receiverWallet,
amount: Number(amount),
});
} catch (error) {
console.error("Failed to transfer funds:", error);
} finally {
setLoading(false);
}
};
The respondToTransfer Function
Finally, let’s add the function to respond to transfer requests. Update respondToTransfer
in the frontend/src/components/transaction-list.jsx
file. This function sends a POST request to the backend to approve or reject a transfer:
const respondToTransfer = async (transactionId, action) => {
setLoading(true);
try {
await fetch("http://localhost:4000/api/wallet/respond-transfer", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: transactionId,
status: action,
}),
});
} catch (error) {
console.error("Failed to respond to transfer:", error);
}
setLoading(false);
};
Final Result
To bring everything together, here’s a demonstration of the app in action. The GIF below showcases the real-time payment features, including:
- Initiating a fund transfer between wallets
- Receiving real-time updates on transaction statuses
- Accepting, declining, or canceling transfers dynamically
With that, you’ve successfully implemented a dynamic, real-time payment system using Rapyd’s API and WebSockets.
Conclusion
In this article, you learned how to build a real-time payment system using the Rapyd API and WebSockets. From setting up the backend to handling wallet creation, wallet-to-wallet transfers, and real-time updates, you now have the tools to create a seamless payment experience for users. By integrating Rapyd’s powerful API and leveraging WebSocket communication, you can ensure that both senders and receivers receive instant feedback on their transactions so your application is dynamic and responsive.
Rapyd’s global payment capabilities make it an excellent choice for developers and businesses looking to implement secure and scalable payment solutions in their applications. Whether you’re building a peer-to-peer payment app, an e-commerce platform, or a financial service, Rapyd provides the infrastructure to handle transactions efficiently. To explore more features and capabilities, visit the Rapyd API documentation and start building your next fintech solution today.
For further learning, check out Rapyd’s developer community resources and sample projects.