Collect Bank Accounts
When building an e-commerce application, subscription service, payroll solution, or just need to enable one-time purchases via direct debt (pay by bank) or fund transfers, one of the critical requirements is to collect and store bank accounts information securely. However, bank routing numbers paired with account numbers, are considered sensitive data and need to be captured and stored following strict security requirements.
In this guide, we will set up Basis Theory SDKs to capture Bank Accounts in a frontend, Web or Mobile application, and securely store the data as tokens in Basis Theory Platform, NACHA preferred partner for data tokenization and encryption. Given this guide is followed step by step, you can rest assured that you are complying to world's class security and privacy standards.
Getting Started
To get started, you will need to create a Basis Theory Account and a TEST Tenant.
Creating a Public Application
Next you will need a Public Application using our NACHA-compliant template Collect Bank Data
. Click here to create one.
This will create an application with the following Access Controls:
- Permissions:
token:create
,token:update
- Containers:
/bank/
- Transform:
mask
Configuring Basis Theory Elements
Basis Theory Elements are available for the following technologies. Click below for detailed instructions on how to install and configure them.
Adding Text Elements
Once installed and configured, add the Text Elements to your application. This will enable users to type in their bank account information in your form, while ensuring your systems never touch the data.
- JavaScript
- React
- iOS
- Android
<div id="routingNumber" style="width: 100%;"></div>
<div id="accountNumber" style="width: 100%;"></div>
import { BasisTheory } from '@basis-theory/basis-theory-js';
let bt;
let routingNumberElement;
let accountNumberElement;
async function init() {
bt = await new BasisTheory().init('<API_KEY>', { elements: true });
// Creates Elements instances
routingNumberElement = bt.createElement('text', {
targetId: 'myRoutingNumber', // (custom) used for tracking validation errors
placeholder: 'Routing Number',
mask: [
/\d/u,
/\d/u,
/\d/u,
/\d/u,
' ',
/\d/u,
/\d/u,
/\d/u,
/\d/u,
' ',
/\d/u,
],
transform: /\s/u, // strip out spaces from mask above before tokenization
});
accountNumberElement = bt.createElement('text', {
targetId: 'myAccountNumber',
placeholder: 'Account Number',
});
// Mounts Elements in the DOM in parallel
await Promise.all([
routingNumberElement.mount('#routingNumber'),
accountNumberElement.mount('#accountNumber'),
]);
}
init();
import React, { useRef } from 'react';
import {
BasisTheoryProvider,
TextElement,
useBasisTheory
} from '@basis-theory/basis-theory-react';
export default function App() {
const { bt } = useBasisTheory('<API_KEY>', { elements: true });
// Refs to get access to the Elements instance
const routingNumberRef = useRef(null);
const accountNumberRef = useRef(null);
return (
<BasisTheoryProvider bt={bt}>
<TextElement
id="myRoutingNumber"
placeholder="Routing Number"
ref={routingNumberRef}
mask={[
/\d/u,
/\d/u,
/\d/u,
/\d/u,
' ',
/\d/u,
/\d/u,
/\d/u,
/\d/u,
' ',
/\d/u
]}
transform={/\s/u}
/>
<TextElement
id="myAccountNumber"
placeholder="Account Number"
ref={accountNumberRef}
/>
</BasisTheoryProvider>
);
}
import Foundation
import UIKit
import BasisTheoryElements
import Combine
class ViewController: UIViewController {
@IBOutlet weak var routingNumberTextField: TextElementUITextField!
@IBOutlet weak var accountNumberTextField: TextElementUITextField!
override func viewDidLoad() {
super.viewDidLoad()
let regexDigit = try! NSRegularExpression(pattern: "\\d")
let routingNumberMask = [
regexDigit,
regexDigit,
regexDigit,
regexDigit,
" ",
regexDigit,
regexDigit,
regexDigit,
regexDigit,
" ",
regexDigit
] as [Any]
let transformMatcher = try! NSRegularExpression(pattern: "\\D") // Regex to remove non-digit
let routingNumberRegex = try! NSRegularExpression(pattern: "^\\d{9}$"); // Regex to validate routing number after transform
let routingNumberOptions = TextElementOptions(mask: routingNumberMask, transform: ElementTransform(matcher: transformMatcher), validation: routingNumberRegex)
try! routingNumberTextField.setConfig(options: routingNumberOptions)
routingNumberTextField.placeholder = "Routing Number"
accountNumberTextField.placeholder = "Account Number"
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.basistheory.android.view.TextElement
android:id="@+id/routingNumber"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Routing Number" />
<com.basistheory.android.view.TextElement
android:id="@+id/accountNumber"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Account Number"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
import com.basistheory.android.view.TextElement
import com.basistheory.android.service.BasisTheoryElements
class MainActivity : AppCompatActivity() {
private lateinit var routingNumberElement: TextElement
private lateinit var accountNumberElement: TextElement
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
routingNumberElement = findViewById(R.id.routingNumber)
accountNumberElement = findViewById(R.id.accountNumber)
val digitRegex = Regex("""\d""")
routingNumberElement.inputType = InputType.NUMBER
routingNumberElement.mask = ElementMask(
listOf(
digitRegex,
digitRegex,
digitRegex,
digitRegex,
" ",
digitRegex,
digitRegex,
digitRegex,
digitRegex,
" ",
digitRegex
)
)
// Regex to remove spaces before tokenization
routingNumberElement.transform = RegexReplaceElementTransform(
regex = Regex("\\D"),
replacement = ""
)
}
}
Storing Bank Accounts
Now that you are securely capturing the bank account data in your user-facing application(s), it is time to store it in your Basis Theory Tenant.
To do this, we will invoke the Create Token endpoint from the SDK, passing the Text Elements as data points in the payload. This will securely create a bank
token by transferring the bank information from the frontend Elements to Basis Theory, where the data will be strongly encrypted and stored in a compliant environment.
Add a submit function along with a button to trigger it:
- JavaScript
- React
- iOS
- Android
<div id="cardNumber"></div>
<div style="display: flex;">
<div id="cardExpirationDate" style="width: 100%;"></div>
<div id="cardVerificationCode" style="width: 100%;"></div>
</div>
<button id="submit">Submit</button>
import { BasisTheory } from '@basis-theory/basis-theory-js';
let bt;
let routingNumberElement;
let accountNumberElement;
async function init () {
...
document.getElementById("submit").addEventListener("click", submit);
}
async function submit () {
try {
const token = await bt.tokens.create({
type: 'bank',
data: {
routing_number: routingNumberElement,
account_number: accountNumberElement,
}
});
// store token.id in your database
} catch (error) {
console.error(error);
}
}
init();
import React, { useRef, useState } from 'react';
import {
BasisTheoryProvider,
TextElement,
useBasisTheory,
} from '@basis-theory/basis-theory-react';
export default function App() {
const { bt } = useBasisTheory('<API_KEY>', { elements: true });
// Refs to get access to the Elements instance
const routingNumberRef = useRef(null);
const accountNumberRef = useRef(null);
const submit = async () => {
try {
const token = await bt?.tokens.create({
type: 'bank',
data: {
routing_number: routingNumberRef.current,
account_number: accountNumberRef.current,
}
});
// store token.id in your database
} catch (error) {
console.error(error);
}
}
return (
<BasisTheoryProvider bt={bt}>
...
<button onClick={submit}>Submit</button>
</BasisTheoryProvider>
);
}
Add a new UIButton
to your Main.storyboard
and create a new Action for it called submit
.
import Foundation
import UIKit
import BasisTheoryElements
import Combine
class ViewController: UIViewController {
@IBOutlet weak var routingNumberTextField: TextElementUITextField!
@IBOutlet weak var accountNumberTextField: TextElementUITextField!
@IBAction func submit(_ sender: Any) {
let body: [String: Any] = [
"type": "bank",
"data": [
"routing_number": self.routingNumberTextField,
"account_number": self.accountNumberTextField
]
]
BasisTheoryElements.tokenize(body: body, apiKey: "<API_KEY>") { token, error in
guard error == nil else {
print(error)
return
}
// store token.id in your database
}
}
override func viewDidLoad() { ... }
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
...
<Button
android:id="@+id/submit_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:backgroundTint="#00A4BA"
android:text="Submit" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
import com.basistheory.android.view.TextElement
import com.basistheory.android.service.BasisTheoryElements
class MainActivity : AppCompatActivity() {
private lateinit var cardNumberElement: CardNumberElement
private lateinit var cardExpirationDateElement: CardExpirationDateElement
private lateinit var cardVerificationCodeElement: CardVerificationCodeElement
private lateinit var button: Button;
private val bt = BasisTheoryElements.builder()
.apiKey("<API_KEY>")
.build()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
routingNumberElement = findViewById(R.id.routingNumber)
accountNumberElement = findViewById(R.id.accountNumber)
...
button = findViewById(R.id.submit_button)
button.setOnClickListener {
submit()
}
}
private fun submit() {
val token = runBlocking {
bt.tokenize(object {
val type = "bank"
val data = object {
val routing_number = routingNumberElement
val account_number = accountNumberElement
}
})
}
// store token.id in your database
}
}
<API_KEY>
with the Public API Key you
created in the Creating a Public Application step.The created bank
token object contains the non-sensitive information about the tokenized card, which follows the Bank Token specification:
{
"id": "f910b9aa-a4a6-4f24-9ec4-2de1a5731d0b",
"type": "bank",
"tenantId": "4aee08b9-5557-474b-a120-252e01fc7b0f",
"data": {
"routing_number": "021000021",
"account_number": "XXXXX3123"
},
"createdBy": "f5c44560-8433-4dcc-b67f-53594c397a5e",
"createdAt": "2023-10-26T14:27:10.6126956+00:00",
"mask": {
"routingNumber": "{{ data.routing_number }}",
"accountNumber": "{{ data.account_number | reveal_last: 4 }}"
},
"searchIndexes": [],
"containers": [
"/bank/high/"
]
}
You can safely store the token's primary key id
in your database to link it with the appropriate checkout, user profile, subscription, or any other record that requires association with the bank details.
Notice how the bank account_number
has been masked before it is returned to your application. This default behavior prevents your application having to deal with the sensitive part of the bank details. Click here to learn more about masking and see how to customize this behavior.
Customizing Tokens
The steps so far cover most cases when you need to collect bank accounts in your frontend and store them in a secure location. However, in some scenarios you may need to customize your bank tokens for specific business needs or technical requirements.
Click here to learn more about Basis Theory Token capabilities.
Deduplication
Companies often find it necessary to uniquely identify bank accounts flowing through their systems for various reasons, which may include: preventing fraudulent transactions, detecting abuse, building consumer profiles or streamlining payment processing for a better user experience.
By leveraging token fingerprinting, developers can recognize the tokenized data in a customizable fashion, without having to interact with the plaintext data. For bank details, it is common to index in both Routing Number and Account Number, but in some cases additional consumer details may also be considered.
When making the tokenization request to store the bank data, pass a fingerprint expression to instruct Basis Theory to calculate the fingerprint for the sensitive data field:
- JavaScript
- React
- iOS
- Android
async function submit () {
try {
const token = await bt.tokens.create({
type: 'bank',
data: {
routing_number: routingNumberElement,
account_number: accountNumberElement,
},
fingerprintExpression: '{{ data.account_number }}|{{ data.routing_number }}',
});
} catch (error) {
console.error(error);
}
}
export default function App() {
const submit = async () => {
try {
const token = await bt?.tokens.create({
type: 'bank',
data: {
routing_number: routingNumberElement,
account_number: accountNumberElement,
},
fingerprintExpression: '{{ data.account_number }}|{{ data.routing_number }}',
});
} catch (error) {
console.error(error);
}
}
return (...);
}
class ViewController: UIViewController {
@IBAction func submit(_ sender: Any) {
let body: [String: Any] = [
"type": "bank",
"data": [
"routing_number": self.routingNumberTextField,
"account_number": self.accountNumberTextField
],
"fingerprint_expression": "{{ data.account_number }}|{{ data.routing_number }}"
]
BasisTheoryElements.tokenize(body: body, apiKey: "<API_KEY>") { token, error in
guard error == nil else {
print(error)
return
}
}
}
}
class MainActivity : AppCompatActivity() {
private fun submit() {
val token = runBlocking {
bt.tokens.create(object {
val type = "bank"
val data = object {
val routing_number = routingNumberElement
val account_number = accountNumberElement
}
val fingerprint_expression = "{{ data.account_number }}|{{ data.routing_number }}"
})
}
}
}
The new tokens should now have a fingerprint:
{
"id": "f910b9aa-a4a6-4f24-9ec4-2de1a5731d0b",
"type": "bank",
"tenantId": "4aee08b9-5557-474b-a120-252e01fc7b0f",
"data": {
"routing_number": "021000021",
"account_number": "XXXXX3123"
},
"createdBy": "f5c44560-8433-4dcc-b67f-53594c397a5e",
"createdAt": "2023-10-26T14:27:10.6126956+00:00",
"fingerprint": "CC2XvyoohnqecEq4r3FtXv6MdCx4TbaW1UUTdCCN5MNL",
"fingerprint_expression": "{{ data.number }}",
"mask": {
"routingNumber": "{{ data.routing_number }}",
"accountNumber": "{{ data.account_number | reveal_last: 4 }}"
},
"searchIndexes": [],
"containers": [
"/bank/high/"
]
}
If you want to prevent creation of a duplicate token based on the distinguishable fingerprint, add the flag below:
- JavaScript
- React
- iOS
- Android
async function submit () {
try {
const token = await bt.tokens.create({
type: 'bank',
data: {
routing_number: routingNumberElement,
account_number: accountNumberElement,
},
fingerprintExpression: '{{ data.account_number }}|{{ data.routing_number }}',
deduplicateToken: true,
});
} catch (error) {
console.error(error);
}
}
export default function App() {
const submit = async () => {
try {
const token = await bt?.tokens.create({
type: 'bank',
data: {
routing_number: routingNumberElement,
account_number: accountNumberElement,
},
fingerprintExpression: '{{ data.account_number }}|{{ data.routing_number }}',
deduplicateToken: true,
});
} catch (error) {
console.error(error);
}
}
return (...);
}
class ViewController: UIViewController {
@IBAction func submit(_ sender: Any) {
let body: [String: Any] = [
"type": "bank",
"data": [
"routing_number": self.routingNumberTextField,
"account_number": self.accountNumberTextField
],
"fingerprint_expression": "{{ data.account_number }}|{{ data.routing_number }}"
"deduplicate_token": true
]
BasisTheoryElements.tokenize(body: body, apiKey: "<API_KEY>") { token, error in
guard error == nil else {
print(error)
return
}
}
}
}
class MainActivity : AppCompatActivity() {
private fun submit() {
val token = runBlocking {
bt.tokens.create(object {
val type = "bank"
val data = object {
val routing_number = routingNumberElement
val account_number = accountNumberElement
}
val fingerprint_expression = "{{ data.account_number }}|{{ data.routing_number }}"
val deduplicate_token = true
})
}
}
}
By doing the above, you are instructing Basis Theory to return the existing token if it is found to have the same fingerprint. Click here to learn more about token deduplication.
Conclusion
The best practices prescribed in this guide ensure that your user-facing applications can collect sensitive bank account information securely, while protected. The token.id
obtained at the end of the Storing Bank Accounts section is a synthetic replacement for the sensitive data and can be safely stored in your database, or transmitted through your systems, meeting security and privacy requirements while reducing the risk of exposure in case of data breaches.
For next steps, take a look at the following guides to proceed taking the most value of your secured bank tokens: