The Bank Account Validation Service (the-BAVS)
Goal
This document proposes The Bank Account Validation Service, a vendor-agnostic service solving the Bank-and-Account validation business needs of Ebury and enabling the deprecation of a key component of the FXS monolithic.
Disclaimer: There was already a component by the name of BAVS (conceived in May 2021 and deployed in production in May 2022). Despite its name, this existing component was tied to the ApplyFinancial API, it was built on top of ECS and exposed a REST API. The migration of all clients of ApplyFinancial to BAVS is far from complete and the majority of ApplyFinancial clients continue to use the FXS API for validation. For the purposes of this document we will not refer to such component anymore, as it was effectively deprecated in October 2024.
Problem Description
The Bank/Account validation needs of Ebury are fulfilled by a component (ebury_validate) of the FXS monolithic leading to these issues:
* ebury_validate contributed to the unsustainable monolithic nature of FXS; a very specific responsibility is fitted within FXS along with a myriad of other responsibilities.
-
The current implementation is heavily tied to the ApplyFinancial apis: all Ebury clients of this api need to set AF credentials, deal with request/response objects defined not by Ebury but by ApplyFinancial (and as a consequence all clients need boilerplate code to extract/transform the response fields as needed)
-
Choosing a different vendor/provider in the future requires changing every single existing client at Ebury, requiring huge amounts of repetitive and error-prone work.
Background
Over 65% of payment rejections are attributed to invalid account details, identifiers and bank codes (based on SWIFT stats), as such Ebury currently has three core needs in regards to (pre)validation/screening of banks and accounts that interact with the array of services that Ebury offers:
- Validation of a Bank
- Validation of an individual account hosted by a Bank
- Retrieving the Financial Payment Networks for a Bank
There are different vendors that provide these capabilities, most notably LexisNexis (née ApplyFinancial) and SWIFT. Ebury has chosen LexisNexis as its provider and currently exposes a small wrapper on top of the AF api as a component of the FXSuite monolithic: ebury_validate.
As part of the Ebury 2.0 architecture roadmap, FXS is to be decommissioned and decomposed into domain-specific microservices where cross-domain communication happens through an event-driven model of communication. As such the component of FXS that deals with validations (aka ebury_validate) needs to be split away from FXS.
Currently, BOS and EBO communicate with FXSuite when they need to validate bank/accounts or retrieve the payment networks of a bank. FXSuite plays the role of a gateway to the ApplyFinancial provider:

Solution
The solution to the above is the-BAVS: a vendor-agnostic service for all bank-and-account validation needs at Ebury.
the-BAVS offers: - clean and easy-to-use apis with request/response objects defined by Ebury existing usecases and with definitions that are independent of any specific vendor - the-BAVS clients have to focus only on the business-needs at hand (eg: pre-validation of a bank branch specified by a SWIFT code) without having to worry about the backing-vendor behind the validation - the-BAVS enables Ebury to easily change vendors in the near future (should the need arise) with minimal and isolated changes without requiring a single change to client code. - Finally, at a lower-level, the-BAVS provides request-validation, clear error semantics, TTL-backed caching, and bulk apis for efficiency purposes.
Once the-BAVS is deployed all requests for bank/account validation will be directed to it.
the-BAVS will expose a vendor-agnostic API modeled on Ebury’s business needs, this will isolate clients from changes on the provider’s API and in the hypothetical scenario that we change providers clients will remain unaffected.
Note: SWIFT is only shown to illustrate the vendor-agnostic trait of the-BAVS.
The following shows a view at a lower level.

- the-BAVS will expose a vendor-agnostic interface for the validation APIs.
- For every third-party data provider (ApplyFinancial, Swift, etc) there will be a gateway component fully-contained in the-BAVS. the-BAVS will ship with the ApplyFinancial gateway from the beginning.
- For every gateway, an adapter will be created to transform the requests/responses from the gateway in order to fit the vendor-agnostic interface.
the-BAVS will be a self-contained microservice, all gateways/adapters will belong in the same repository. Despite this, the design of the Gateways components must make them easy to extract as independent services if that is needed in the future.
Note: Since the immediate clients of the-BAVS (FXS, BOS) are legacy and do not yet support an event-driven model of communication, the-BAVS will expose a synchronous API to such clients. As Ebury makes further progress towards an event-driven architecture the need for an async model of communication with clients will naturally appear and at that point the-BAVS can start communicating via events.
the-BAVS Service Definition
The service definition along with the request/response objects are defined with Protocol Buffers and they are compatible with all existing clients at Ebury, that is, all existing fields used at Ebury are guaranteed to be returned by the-BAVS.
Technical limitation of gRPC: Our k8s ingress is currently not set up to externally expose HTTP/2 traffic from gRPC services. Since legacy clients, such as BOS, are not on k8s yet, in the meantime this service will expose a REST api. Once the ingress limitation is lifted we can serve direct gRPC traffic.
We performed a thorough study of all existing clients of AF at Ebury to ensure compatibility. We will also show how a different vendor from the one currently used by Ebury is able to implement the same responses.
Bank Validation
service BankAccountValidationService {
rpc ValidateBank(ValidateBankRequest)
returns (ValidateBankResponse) {}
}
The request/response objects are defined as follows:
message ValidateBankRequest {
BankIdentifier bank_id = 1;
}
message ValidateBankResponse {
BankValidationStatus status = 1;
// If the validation failed due to invalid bank ids
// the following fields are null
optional BankDetails bank_details = 2;
optional string swift_code = 3;
}
Note that both request and response abstract away any vendor-specific implementation details. Existing client code at Ebury today has to check for different error codes from AF and contains custom exceptions to deal with each scenario. That is now handled by the BankValidationStatus field. Note that additional fields (in these and the messages covered below) can easily be added in a backwards compatible manner thanks to the encoding used by protocol buffers.
Banks, being a central concept at Ebury, are uniquely identified by the BankIdentifier message:
message BankIdentifier {
oneof bank_identifier {
BankFromNationalCode national_code = 1;
BankFromSWIFTCode swift_code = 2;
// In the future we can include additional ways to uniquely identify
// a bank in the world (eg: a country-specific encoding of banks)
}
}
message BankFromNationalCode {
string country_code = 1;
string bank_national_id = 2;
}
message BankFromSWIFTCode {
string swift_code = 1;
}
At a high-level this definition is what Ebury is already using, albeit its current usage is informal and error-prone (eg: code is constantly checking which combination of parameters out of swift code, country code and national id was provided). The new definition is cleaner, reduces the space for errors and is easy to extend.
The other key piece of information needed about banks are branch details:
message BankDetails {
message BankAddress {
string country_code = 1;
string city = 2;
string postal_code = 3;
string street_name = 4;
}
string full_name = 1;
BankAddress address = 2;
// If the bank belongs to the SWIFT network this field
// will be set to its SWIFT code in the network.
optional string swift_code = 3;
// If the bank is Australian this field will contain
// the Bank State Branch (BSB) australian code.
optional string bsb_code = 4;
// Again, we can easily customize the bank details to include
// branch-specific details:
// BankAddress head_office_details;
// BankAddress branch_details; // branch extracted from SWIFT code
}
Financial Payment Networks
The other core usecase for Ebury is fetching the payment networks a bank is a member of. As of today, it specifically checks for the SWIFT, FPS (Fast Payments) and E1S1 (Euro1/Step1) networks.
The following definitions cover this:
service BankAccountValidationService {
// ... continuation
rpc GetBankPaymentNetworks(GetBankPaymentNetworksRequest)
returns (GetBankPaymentNetworksResponse) {}
}
message GetBankPaymentNetworksRequest {
BankIdentifier bank_id = 1;
}
message GetBankPaymentNetworksResponse {
BankValidationStatus bank_validation_status = 1;
repeated FinancialPaymentNetwork networks_supported = 2;
}
enum FinancialPaymentNetwork {
UNSPECIFIED = 0;
FPS = 1; // Faster Payments Service (UK)
E1S1 = 2; // Euro1/Step1 (EBA Clearing)
COMPE = 3; // Brasil Cheque Clearing
// Others as needed
}
The API is, again, clean and derived from Ebury business needs without any implementation detail from the vendor leaking to the clients. In addition, if Ebury starts focusing on additional payment networks the changes to this definition are straightforward.
Bank Account Validation
service BankAccountValidationService {
// .. continuation
rpc ValidateBankAccount(ValidateBankAccountRequest)
returns (ValidateBankAccountResponse) {}
}
message ValidateBankAccountRequest {
BankAccountID account_id = 1;
}
message ValidateBankAccountResponse {
BankAccountValidationStatus status = 1;
// If the validation succeeded the following field is set
optional BankDetails bank_details = 2;
}
The way a bank account is defined is either through an IBAN or a national ID, which follows standard nomenclature in the finance industry, again, not tied to a specific vendor:
message BankAccountID {
message NationalAccountID {
BankFromNationalCode bank_id = 1;
string account_id = 2;
}
oneof bank_identifier {
string iban = 1;
NationalAccountID national_id = 2;
}
}
Finally, a bulk validation api is also provided which suits Ebury scripting/batching needs:
service BankAccountValidationService {
// .. continuation
rpc BulkValidateBankAccounts(BulkValidateBankAccountsRequest)
returns (BulkValidateBankAccountsResponse) {}
}
Note that our current provider, ApplyFinancial, does not have rate-limiting in place. As of this writing, the bulk sizes that Ebury sends for validation are in the range of [1, 26].
Implementing the service interface with a different vendor (SWIFT)
As mentioned earlier, the request/response definitions not only fulfill all existing clients of Ebury today but are also independent of the vendors chosen for the implementation. Here we briefly show how a different vendor, SWIFT, can implement the same response.
SWIFT provides the Pre-Validation API for Banks (see section ‘Validates the instructed institution as financial institution’ in https://developer.swift.com/reference). That api takes as input a JSON object that is compatible with the ValidateBankRequest message and its response object is a superset of the ValidateBankResponse message.
SWIFT can also implement the GetBankPaymentNetworks api: one of its products is Bank Directory Plus which maps Banks to the Payment Networks they belong to.
In the end, a hypothetical change of vendor will not require a change in the service definition, thus all clients would remain unaffected by such a change and the migration would only require adding a lightweight gateway on top of the new vendor and integrating it in the-BAVS.
Alternatives
Instead of creating a new service that abstracts away vendor-details we could move on with the existing component that acts as a gateway to ApplyFinancial and migrate all existing clients to it. That alternative was explored in this proposal but based on the feedback received we decided to pursue this RFC which is a step forward in the bigger FXS-decommission initiative and doesn't pile up technical debt.
Security Impact
The service is expected to be deployed in a private network and communication between the-BAVS and its clients will be secured with TLS.
All requests to the-BAVS must be authenticated.
the-BAVS service does not store or handle any sensitive information.
Performance Impact
No performance impact is expected in the client applications that will consume this new service or in the new service itself.
As the-BAVS will be replacing a component in FXSuite (ebury_validate), the traffic should remain the same.
Developer Impact
As part of migrating to the-BAVS, client code of FXSuite's ebury_validate will have to use the generated gRPC client library to talk to the-BAVS. Instead of manipulating the JSON response that they receive from FXS today they will have to interact with the deserialised protocol buffer message for the corresponding response.
Legacy apps such as BOS, that run on python2.7, can still have gRPC clients (on versions grpcio==1.39.0) that can communicate with the modern server gRPC (on version grpcio==1.50.0) used by BAVS.
By design, the-BAVS responses are backwards compatible with those returned by FXS so the client code changes should be straightforward.
Data Contracts/Sources
The 'the-BAVS Service Definition' section already defines the request/response formats and uses the optional specifier whenever a given field is returned in a best-effort basis.
Deployment
the-BAVS will follow the guidelines for gRPC deployment on K8S and will follow the existing CI/CD practices. As a brand new service there won't be deployment-impact on other services and the-BAVS is expected to be a 24/7 service with monitoring/alerting practices based on the corresponding blueprint.
Dependencies
the-BAVS will depend on an external vendor who gives access to a repository of bank/accounts for validation purposes. Today that dependency already exists with ApplyFinancial.
Depending on a market study, Ebury might decide to switch providers (SWIFT is a likely alternative, and some conversations have already taken place, see this sales deck for more info). Since we have already shown in 'Implementing the service interface with a different vendor' that the-BAVS API is vendor-agnostic such change wouldn't affect the-BAVS clients.