How to Build a Peer-to-Peer Payments App

Image of Dimitri Dadiomov
Dimitri DadiomovCEO

Modern Treasury empowers teams to make payment operations simple, scalable, and secure. The “Guides” series walks through representative businesses or payment processes and explains step by step how best to go about building them from scratch.


Increasingly, products are adding various forms of cash transfer beyond a simple credit card payment. In this post, we will walk through the building of a peer-to-peer money transfer app using the Modern Treasury API. The purest examples of this might include Venmo or Square Cash, but more specific versions exist. For example, consider Tally, which helps you manage credit card debt by transferring cash to and from your account.

It is important to note before we start, that this post approaches the engineering and user experience angles only, and that building a business requires many other elements to consider, from risk and compliance to customer service and growth/marketing. The focus of this post is how to make a transaction envisioned by the product real, how to relay it to a bank, and how to successfully manage this at scale.

The User Experience

Let’s start by defining the user experience you want to offer with your app, which we will call Paymo. You have to think about the sign-up flow, the act of sending a payment, and the act of receiving a payment.

  • Sign-up flow: a user comes to our app and creates an account. The app collects basic information like username and password, as well as bank account details. You might ask the user to manually input their bank account and routing number, or use a service like Plaid. More here on authenticating an account.
  • Sending a payment: Using example customers Sidney and Morgan, Sidney selects Morgan, picks an amount of money to transfer, and hits send.
  • Receiving a payment: Morgan receives a notification that Sidney sent them a payment.

Now that we have the basic user experience flow, we can come up with a payment ops architecture and map out the API calls that we might make at every moment.

Payment Ops Architecture

In the US, you are most likely to use the ACH system for these payments. ACH is cheap, ubiquitous, and connected to every domestic bank account.

To move the funds from Sidney’s bank account to Morgan's bank account, you first need to open a bank account for Paymo. Note that to do so, one must be aware of a number of regulations and compliance considerations, and your bank will walk you through those requirements. Only once you have the legal and regulatory structure confirmed can you actually start moving money.

When Sidney pays Morgan, what you are actually doing is:

  1. ACH debit funds from Sidney’s bank account into Paymo’s bank account
  2. Confirm those funds are cleared into the Paymo intermediary bank account
  3. ACH credit funds from the Paymo bank account to Morgan’s bank account
Visual representation of money movement for P2P payment app Paymo: Sidney → Paymo → Morgan
Visual representation of money movement for P2P payment app Paymo.

We’re now ready to move on to the exact API calls.

API Calls and Timings

Let’s walk through this flow step by step.

Using Sidney and Morgan as examples again, say they both sign up for Paymo because Sidney wants to reimburse Morgan for dinner.

Step 1

Sidney signs up for Paymo. Once you’ve collected Sidney’s bank account you will create a counterparty in Modern Treasury:

1curl --request POST \
3  --url \
4  -H 'Content-Type: application/json' \
5  -d '{
6  "name": "Sidney Pyramid",
7  "accounts": [
8      "account_type": "checking",
9      "routing_details": [
10          "routing_number_type": "aba",
11          "routing_number": "121141822"
12      ],
13      "account_details": [
14          "account_number": "123456789"
15  ]}'

Step 2

Morgan must also sign up for Paymo. Once that user is created you must create a counterparty for them in a similar fashion. For each counterparty you create, you receive a response with that counterparty ID and account ID.

Step 3

When Sidney tells the app that they want to send $20 to Morgan, you can ACH debit Sidney’s account for $20. Note the $20 is represented in cents, as 2000:

1curl --request POST \
3  --url \
4  -H 'Content-Type: application/json' \
5  -d '{
6  "type": "ach",
7  "amount": 2000,
8  "direction": "debit",
9  "currency": "USD",
10  "originating_account": "Paymo Bank ID",
11  "receiving_account_id": "Sidney's Bank Account ID",
12  "counterparty_id": "Sidney's Counterparty ID"

What happens behind the scenes is actually quite complicated, but on a basic level the payment can now succeed or fail. If it fails, it will fail with one of the ACH return codes. We’ll assume it succeeds for now.

Step 4

Once the $20 arrives in the company’s intermediary bank account, it can be sent on to Morgan’s bank account. You can do this using the same API call as we did to debit Sidney, but now the direction of the ACH will be reversed and the bank accounts will be different:

1curl --request POST \
3  --url \
4  -H 'Content-Type: application/json' \
5  -d '{
6  "type": "ach",
7  "amount": 2000,
8  "direction": "credit",
9  "currency": "USD",
10  "originating_account": "Paymo Bank ID",
11  "receiving_account_id": "Morgan’s Bank Account ID",
12  "counterparty_id": "Morgan’s Counterparty ID"

Step 5

That’s it! The money is on its way to Morgan.

In addition to the basic ACH instructions, you want to enrich the API calls with metadata and line items so that when someone looks at this in the future, they can understand the context for the payment. For example, you might want to tag each payment with a transfer ID, a request date, and maybe even a session ID for when Sidney logged in to make the transfer.

Finally, you must also be aware and conscious of ACH timings. Because ACH is a batch system, it operates on fixed timelines and payments are processed only on those timelines. Depending on your bank set up the timing could be slightly different.

What Can Go Wrong?

We described the happy path above, but naturally, things can go wrong.

The simplest error would be insufficient funds in Sidney’s bank account. If Sidney wanted to send $20 to Morgan, but only had $10 in their account, the ACH debit we sent to the bank would return with return code R1, the most common return code in the ACH system. Modern Treasury would send you a notification in that event in the form of a webhook or email. Because this is a possibility, you would not want to send $20 to Morgan unless you know that Sidney had $20 to send. Handling each return code with a path to resolution is an important piece of building a robust system.

Users might be confused about a payment or the timing of a payment, and call in. These customer service inquiries often require an investigation to check if, indeed, a payment has gone awry or if everything worked as expected.

To help with that, Modern Treasury’s app stores all the contextual information. Because the original API call contained all the relevant metadata, a Paymo customer service agent might look at this and be able to quickly tell Sidney, “Thank you for calling us, you did, in fact, open the app at 3:25pm to send $20 to Morgan. You described it as funds for dinner.”

As your app grows and succeeds, as we all hope it does, unique cases that happen 1% of the time are happening often enough that you end up dedicating teams to manage basic payment issues, regular accounting reconciliation and bookkeeping, and other one-off investigations around payments. Making sure the team has the right tools, like Modern Treasury, to do this efficiently will save countless hours of otherwise wasted time and frustration.

Try Modern Treasury

See how smooth payment operations can be.

Talk to us


Subscribe to Journal updates

Discover product features and get primers on the payments industry.



Modern Treasury For

Case Studies





Popular Integrations

© Modern Treasury Corp.