How to build a wallet with Stripe & Blnk Finance

Process payments with Stripe and track them with your Blnk Core.


5 min read

How to build a wallet with Stripe & Blnk Finance

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:

  1. Users fund their AcmePay wallet via bank transfer or credit/debit card.
  2. At checkout, users choose AcmePay as their payment option.
  3. The purchase amount is deducted from their AcmePay wallet balance.
  4. Users receive 5% cashback, credited directly to their AcmePay wallet.

To build this, you need the following:

  1. A payment provider to handle online payments. We'll use Stripe for this guide
  2. 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.

Simplified money movement map for 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.

Ledgers helps you keep an accurate record of how much belongs to each user in your application.

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.

  1. 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:

    TypeScript
    const response = await blnk.Ledgers.create({
      name: 'Users Ledger',
      meta_data: {
        organization: 'AcmePay',
        description: 'For all users wallet created & managed by Acme.',
      },
    });
    Go
    ledger, resp, err := client.Ledger.Create(blnkgo.CreateLedgerRequest{
      Name: "Users Ledger",
      MetaData: blnkgo.MetaData{
        "organization": "AcmePay",
        "description": "For all users wallet created & managed by Acme.",
      },
    })
    Python
    response = blnk.ledgers.create({
      "name": "Users Ledger",
      "meta_data": {
        "organization": "AcmePay",
        "description": "For all users wallet created & managed by Acme.",
      },
    })
    Java
    ApiResponse<JsonNode> response = blnk.ledgers().create(
      CreateLedger.create()
        .name("Users Ledger")
        .metaData(Map.of("organization", "AcmePay", "description", "For all users wallet created & managed by Acme.")));
  2. Creating the user wallet

    To create the user wallet, we'll do the following:

    1. Create a customer on Stripe, and retrieve the customer id.
    2. Create an identity on Blnk and add the Stripe customer id to its metadata.
    3. Create a balance on Blnk using the identity id from Blnk.
  3. 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" }
    });
  4. 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.

    TypeScript
    async 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;
    }
    Go
    func 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
    }
    Python
    def 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"]
    Java
    ApiResponse<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();
      }
  5. Create a balance for the user

    This is how we store cash balances and record transactions that the user makes within our application.

    TypeScript
    async 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;
    }
    Go
    func 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
    }
    Python
    def 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"]
    Java
    ApiResponse<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:

  1. Get the amount from the customer via the app.
  2. Initiate a Stripe checkout session with customer's email or id and generate a unique reference id.
  3. Listen for webhooks to know when the payment is created or completed.
  4. Create a transaction in the ledger to update your user's balance. Use the same reference id.
  1. 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.

    TypeScript
    async 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;
    }
    Go
    func 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
    }
    Python
    def 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"]
    Java
    String 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'
    });
  2. 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.

    commitPayment
    const 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'));
  3. Update the user balance in your ledger

    If successful, commit the inflight transaction in your ledger to update your user's balance.

    TypeScript
    async 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;
    }
    Go
    func 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
    }
    Python
    def 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"]
    Java
    String 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:

    TypeScript
    async 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;
    }
    Go
    func 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
    }
    Python
    def 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"]
    Java
    String 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:

TypeScript
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;
}
Go
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
}
Python
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"]
Java
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:

TypeScript
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;
}
Go
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
}
Python
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"]
Java
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.

Related articles