How to build a wallet with Stripe & Blnk Finance
Process payments with Stripe and track them with your Blnk Core.
5 min read
Wallets are one of the most popular fintech products in the world today. It serves a wide range of use cases from traditional fintech apps like CashApp to other app categories like eCommerce (Shopify), travel (Booking.com), gaming (Call of Duty Mobile), etc.
To use a wallet, you typically add money from a bank account or credit card before making transactions in the app.
In this guide, we’ll show you how to build a wallet product step-by-step using Blnk Finance, with Stripe as your payment provider, to create a smooth and reliable experience.
The user experience
Let’s create a wallet feature for an eCommerce app using AcmePay, a payment method that offers 5% cashback on checkout purchases. When users sign up, an AcmePay wallet is automatically created for them. Here’s how it works:
- Users fund their AcmePay wallet via bank transfer or credit/debit card.
- At checkout, users choose AcmePay as their payment option.
- The purchase amount is deducted from their AcmePay wallet balance.
- Users receive 5% cashback, credited directly to their AcmePay wallet.
To build this, you need the following:
- A payment provider to handle online payments. We'll use Stripe for this guide
- A double-entry ledger to accurately track transactions and wallet balances.
Money movement map
First, start by determining how money moves in our system for each transaction — wallet funding, making a purchase, and receiving cashback. This provides a bird's eye view of the flow of funds within AcmePay.
Next, we set up our ledger architecture. Our ledger is how we know how much each user has in our system because Stripe will only show the total amount deposited across all users in our system.
1. Implementing our wallet
Now that we've defined our user experience and money movement map, let's dive into building our wallet. To follow this guide, you need to have a live instance of Blnk Core and a Stripe account.
Set up your users ledger
You need to create a ledger to keep track of all balances created for users in AcmePay. This is the only ledger we need to create for this guide.
To set up our Users Ledger, make a request to the Create Ledger endpoint as follows:
TypeScriptconst response = await blnk.Ledgers.create({ name: 'Users Ledger', meta_data: { organization: 'AcmePay', description: 'For all users wallet created & managed by Acme.', }, });Goledger, resp, err := client.Ledger.Create(blnkgo.CreateLedgerRequest{ Name: "Users Ledger", MetaData: blnkgo.MetaData{ "organization": "AcmePay", "description": "For all users wallet created & managed by Acme.", }, })Pythonresponse = blnk.ledgers.create({ "name": "Users Ledger", "meta_data": { "organization": "AcmePay", "description": "For all users wallet created & managed by Acme.", }, })JavaApiResponse<JsonNode> response = blnk.ledgers().create( CreateLedger.create() .name("Users Ledger") .metaData(Map.of("organization", "AcmePay", "description", "For all users wallet created & managed by Acme.")));Creating the user wallet
To create the user wallet, we'll do the following:
- Create a customer on Stripe, and retrieve the customer id.
- Create an identity on Blnk and add the Stripe customer id to its metadata.
- Create a balance on Blnk using the identity id from Blnk.
Create a customer on Stripe
This helps us identify the user when they fund their wallet via Stripe checkout.
const stripe = require('stripe')('sk_test_51Q8vs7Rr ... Eh00okDxEs1L'); const customer = await stripe.customers.create({ name: "Charles Xavier", email: "charlesxavier@example.com", metadata: { "userId": "123" } });Create an identity on Blnk
This helps us identify the user within our ledger. Once linked to a balance, all transactions performed by the balance can be traced to a user.
TypeScriptasync function createIdentity(stripeCreatedId: string, user: {firstName: string; lastName: string; email: string}) { const response = await blnk.Identities.create({ first_name: user.firstName, last_name: user.lastName, email: user.email, meta_data: { stripe_id: stripeCreatedId, }, }); console.log('Customer Identity created:', response.data.identity_id); return response.data.identity_id; }Gofunc createIdentity(stripeCreatedID string, user User) { identity, resp, err := client.Identity.Create(blnkgo.Identity{ FirstName: "User.firstName", LastName: "User.lastName", Email: "User.email", MetaData: blnkgo.MetaData{ "stripe_id": "stripe_created_id", }, }) fmt.Println("Customer Identity created:", identity.IdentityID) return identity.IdentityID, nil }Pythondef create_identity(stripe_created_id, user): response = blnk.identity.create({ "first_name": user["first_name"], "last_name": user["last_name"], "email": user["email"], "meta_data": { "stripe_id": stripe_created_id, }, }) print("Customer Identity created:", response.data["identity_id"]) return response.data["identity_id"]JavaApiResponse<JsonNode> createIdentity(String stripeCreatedId, User user) { ApiResponse<JsonNode> response = blnk.identity().create( CreateIdentity.create() .firstName("User.firstName") .lastName("User.lastName") .email("User.email") .metaData(Map.of("stripe_id", "stripe_created_id"))); System.out.println("Customer Identity created: " + response.data().get("identity_id").asText()); return response.data().get("identity_id").asText(); }Create a balance for the user
This is how we store cash balances and record transactions that the user makes within our application.
TypeScriptasync function createMainWallet(ledgerId: string, identityId: string, currency: string) { const response = await blnk.LedgerBalances.create({ ledger_id: ledgerId, identity_id: identityId, currency, meta_data: { wallet_type: 'main', }, }); console.log('Main Wallet created:', response.data.balance_id); return response.data.balance_id; }Gofunc createMainWallet(ledgerID, identityID, currency string) { balance, resp, err := client.LedgerBalance.Create(blnkgo.CreateLedgerBalanceRequest{ LedgerID: "ledgerId", IdentityID: "identityId", Currency: "currency", MetaData: blnkgo.MetaData{ "wallet_type": "main", }, }) fmt.Println("Main Wallet created:", balance.BalanceID) return balance.BalanceID, nil }Pythondef create_main_wallet(ledger_id, identity_id, currency): response = blnk.ledger_balances.create({ "ledger_id": ledger_id, "identity_id": identity_id, "currency": currency, "meta_data": { "wallet_type": "main", }, }) print("Main Wallet created:", response.data["balance_id"]) return response.data["balance_id"]JavaApiResponse<JsonNode> createMainWallet(String ledgerId, String identityId, String currency) { ApiResponse<JsonNode> response = blnk.ledgerBalances().create( CreateLedgerBalance.create() .ledgerId("ledgerId") .identityId("identityId") .currency("currency") .metaData(Map.of("wallet_type", "main"))); System.out.println("Main Wallet created: " + response.data().get("balance_id").asText()); return response.data().get("balance_id").asText(); }
2. Wallet funding
To handle wallet top-up, we will:
- Get the amount from the customer via the app.
- Initiate a Stripe checkout session with customer's email or id and generate a unique reference id.
- Listen for webhooks to know when the payment is created or completed.
- Create a transaction in the ledger to update your user's balance. Use the same reference id.
Create a new Stripe checkout payment
Start by creating an inflight transaction to initiate the payment in your ledger. This creates the transaction but doesn't apply it to the balances until payment is confirmed by Stripe.
Make sure to generate a unique reference id for each new transaction in your app.
TypeScriptasync function walletFunding(reference, amount, currency, customerBalanceId, stripeSessionId, paymentMethod) { const response = await blnk.Transactions.create({ amount, precision: 100, currency, reference, source: '@Stripe', destination: customerBalanceId, allow_overdraft: true, inflight: true, description: 'Topup via Stripe', meta_data: { payment_method: paymentMethod, stripe_session_id: stripeSessionId, }, }); console.log('Transaction created:', response.data.transaction_id); return response.data.transaction_id; }Gofunc walletFunding(reference string, amount float64, currency string, customerBalanceId string, stripeSessionId string, paymentMethod string) { transaction, resp, err := client.Transaction.Create( blnkgo.CreateTransactionRequest{ ParentTransaction: blnkgo.ParentTransaction{ Amount: amount, Precision: 100, Currency: currency, Reference: reference, Source: "@Stripe", Destination: customerBalanceId, Description: "Topup via Stripe", MetaData: blnkgo.MetaData{ "payment_method": paymentMethod, "stripe_session_id": stripeSessionId, }, }, AllowOverdraft: true, Inflight: true, }, ) fmt.Println("Transaction created:", transaction.TransactionID) return transaction.TransactionID, nil }Pythondef wallet_funding(reference, amount, currency, customerBalanceId, stripeSessionId, paymentMethod): response = blnk.transactions.create({ "amount": amount, "precision": 100, "currency": currency, "reference": reference, "source": "@Stripe", "destination": customerBalanceId, "allow_overdraft": True, "inflight": True, "description": "Topup via Stripe", "meta_data": { "payment_method": paymentMethod, "stripe_session_id": stripeSessionId, }, }) print("Transaction created:", response.data["transaction_id"]) return response.data["transaction_id"]JavaString walletFunding(String reference, Number amount, String currency, String customerBalanceId, String stripeSessionId, String paymentMethod) { ApiResponse<JsonNode> response = blnk.transactions().create( CreateTransactions.create() .amount(amount) .precision(100) .currency(currency) .reference(reference) .source("@Stripe") .destination(customerBalanceId) .allowOverdraft(true) .inflight(true) .description("Topup via Stripe") .metaData(Map.of("payment_method", paymentMethod, "stripe_session_id", stripeSessionId))); System.out.println("Transaction created: " + response.data().get("transaction_id").asText()); return response.data().get("transaction_id").asText(); }Next, create a Stripe amount using the same reference and adding the transaction and balance id to the metadata:
const stripe = require('stripe')('sk_test_...'); const session = await stripe.checkout.sessions.create({ payment_method_types: ['card'], line_items: [ { price_data: { currency: 'usd', product_data: { name: 'Top-up', }, unit_amount: 2000, }, quantity: 1, }, ], metadata: { blnk_balance_id: "bln_ 123", blnk_transaction_id: transactionId }, mode: 'payment', success_url: 'https://yourdomain.com/success', cancel_url: 'https://yourdomain.com/cancel', client_reference_id: 'ref_acmepay-001' });Listen for webhooks to know when the payment is captured
Set up a webhook handler to listen for events from Stripe. Then use these events to handle updating the status of the inflight transaction in your ledger.
If successful, the inflight transaction is committed. If failed or expired, it is voided.
commitPaymentconst express = require('express'); const app = express(); const stripe = require('stripe')('sk_test_51Q8v ... MnEh00okDxEs1L'); const endpointSecret = 'whsec_b653c958ed ... 578088ea2'; app.post('/webhook', express.raw({ type: 'application/json' }), (request, response) => { const sig = request.headers['stripe-signature']; let event; try { event = stripe.webhooks.constructEvent(request.body, sig, endpointSecret); } catch (err) { response.status(400).send(`Webhook Error: ${err.message}`); return; } const session = event.data.object; console.log('Checkout session completed:', session.id); if (event.type === 'checkout.session.completed') { function commitPayment(session.metadata.blnk_transaction_id) } else if (event.type === 'checkout.session.async_payment_failed' || 'checkout.session.expired') { function voidPayment(session.metadata.blnk_transaction_id) } response.status(200).end(); }); app.listen(4242, () => console.log('Running on port 4242'));Update the user balance in your ledger
If successful, commit the inflight transaction in your ledger to update your user's balance.
TypeScriptasync function commitPayment(transactionId: string) { const response = await blnk.Transactions.updateStatus( transactionId, { status: 'commit' }, ); console.log('Transaction committed:', response.data.transaction_id); return response.data.transaction_id; }Gofunc commitPayment(transactionID string) { transaction, resp, err := client.Transaction.Update( transactionID, blnkgo.UpdateStatus{ Status: blnkgo.InflightStatusCommit, }, ) fmt.Println("Transaction committed:", transaction.TransactionID) return transaction.TransactionID, nil }Pythondef commit_payment(transaction_id): response = blnk.transactions.update_status( transaction_id, {"status": "commit"}, ) print("Transaction committed:", response.data["transaction_id"]) return response.data["transaction_id"]JavaString commitPayment(String transactionId) { ApiResponse<JsonNode> response = blnk.transactions().updateStatus( transactionId, UpdateTransactionStatus.create() .status("commit")); System.out.println("Transaction committed: " + response.data().get("transaction_id").asText()); return response.data().get("transaction_id").asText(); }If failed, void the transaction instead:
TypeScriptasync function voidPayment(transactionId: string) { const response = await blnk.Transactions.updateStatus( transactionId, { status: 'void' }, ); console.log('Transaction voided:', response.data.transaction_id); return response.data.transaction_id; }Gofunc voidPayment(transactionID string) { transaction, resp, err := client.Transaction.Update( transactionID, blnkgo.UpdateStatus{ Status: blnkgo.InflightStatusVoid, }, ) fmt.Println("Transaction voided:", transaction.TransactionID) return transaction.TransactionID, nil }Pythondef void_payment(transaction_id): response = blnk.transactions.update_status( transaction_id, {"status": "void"}, ) print("Transaction voided:", response.data["transaction_id"]) return response.data["transaction_id"]JavaString voidPayment(String transactionId) { ApiResponse<JsonNode> response = blnk.transactions().updateStatus( transactionId, UpdateTransactionStatus.create() .status("void")); System.out.println("Transaction voided: " + response.data().get("transaction_id").asText()); return response.data().get("transaction_id").asText(); }
3. Making a purchase
According to our flow of funds, when our user makes a purchase, we record a transaction from the user balance to an internal Revenue balance in our ledger.
To implement this, make a request to the Record Transaction endpoint as follows:
async function walletFunding(reference, purchaseAmount, currency, customerBalanceId) {
const response = await blnk.Transactions.create({
amount: purchaseAmount,
precision: 100,
currency,
reference,
source: customerBalanceId,
destination: '@Revenue',
description: 'Payment for online purchase',
meta_data: {
payment_method: 'AcmePay',
},
});
console.log('Transaction created:', response.data.transaction_id);
return response.data.transaction_id;
} func walletFunding(reference string, purchaseAmount float64, currency string, customerBalanceId string) {
transaction, resp, err := client.Transaction.Create(
blnkgo.CreateTransactionRequest{
ParentTransaction: blnkgo.ParentTransaction{
Amount: purchaseAmount,
Precision: 100,
Currency: currency,
Reference: reference,
Source: customerBalanceId,
Destination: "@Revenue",
Description: "Payment for online purchase",
MetaData: blnkgo.MetaData{
"payment_method": "AcmePay",
},
},
},
)
fmt.Println("Transaction created:", transaction.TransactionID)
return transaction.TransactionID, nil
} def wallet_funding(reference, purchaseAmount, currency, customerBalanceId):
response = blnk.transactions.create({
"amount": purchaseAmount,
"precision": 100,
"currency": currency,
"reference": reference,
"source": customerBalanceId,
"destination": "@Revenue",
"description": "Payment for online purchase",
"meta_data": {
"payment_method": "AcmePay",
},
})
print("Transaction created:", response.data["transaction_id"])
return response.data["transaction_id"] String walletFunding(String reference, Number purchaseAmount, String currency, String customerBalanceId) {
ApiResponse<JsonNode> response = blnk.transactions().create(
CreateTransactions.create()
.amount(purchaseAmount)
.precision(100)
.currency(currency)
.reference(reference)
.source(customerBalanceId)
.destination("@Revenue")
.description("Payment for online purchase")
.metaData(Map.of("payment_method", "AcmePay")));
System.out.println("Transaction created: " + response.data().get("transaction_id").asText());
return response.data().get("transaction_id").asText();
} 4. Crediting user cashback
Once the purchase has been completed, we record another transaction to credit the customer with their cashback reward from an internal Cashback balance in our ledger:
async function creditCashback(reference, cashbackAmount, currency, customerBalanceId) {
const response = await blnk.Transactions.create({
amount: cashbackAmount,
precision: 100,
currency,
reference,
source: '@CashBack',
destination: customerBalanceId,
description: 'Cashback reward',
});
console.log('Cashback credited:', response.data.transaction_id);
return response.data.transaction_id;
} func creditCashback(reference string, cashbackAmount float64, currency string, customerBalanceId string) {
transaction, resp, err := client.Transaction.Create(
blnkgo.CreateTransactionRequest{
ParentTransaction: blnkgo.ParentTransaction{
Amount: cashbackAmount,
Precision: 100,
Currency: currency,
Reference: reference,
Source: "@CashBack",
Destination: customerBalanceId,
Description: "Cashback reward",
},
},
)
fmt.Println("Cashback credited:", transaction.TransactionID)
return transaction.TransactionID, nil
} def credit_cashback(reference, cashbackAmount, currency, customerBalanceId):
response = blnk.transactions.create({
"amount": cashbackAmount,
"precision": 100,
"currency": currency,
"reference": reference,
"source": "@CashBack",
"destination": customerBalanceId,
"description": "Cashback reward",
})
print("Cashback credited:", response.data["transaction_id"])
return response.data["transaction_id"] String creditCashback(String reference, Number cashbackAmount, String currency, String customerBalanceId) {
ApiResponse<JsonNode> response = blnk.transactions().create(
CreateTransactions.create()
.amount(cashbackAmount)
.precision(100)
.currency(currency)
.reference(reference)
.source("@CashBack")
.destination(customerBalanceId)
.description("Cashback reward"));
System.out.println("Cashback credited: " + response.data().get("transaction_id").asText());
return response.data().get("transaction_id").asText();
} Conclusion
This guide covers the key steps for managing a wallet, but there are still several important challenges and considerations you’ll need to address when building your wallet product.
For example, you may need to correct an error in your transactions. Since Blnk is immutable, it is impossible to edit a transaction that has been created. The only way to do it is to first refund the transaction, and then post the correct record.
Other considerations include, performing some checks before completing a transaction, handling Stripe refunds, managing wallet withdrawals, and collaborating with other teams like Customer Support for access to your users' financial data.
Getting started
This article outlined a straightforward way to designing a wallet product, however it doesn't cover all of the unique complexities that different wallet products may involve.
If you have questions about how to build your specific use case with Blnk, feel free to reach out to us via Support or contact Sales.