Building the Brex Accounting API
Brex’s diverse customers use a variety of software tools to run and grow their businesses. About half of all our customers use some form of ERP (Enterprise Resource Planning) application and expect Brex data to seamlessly integrate with their existing productivity tools.
In late 2019, we identified two sizable opportunities:
- Customers demand robust integrations with their existing workforce productivity tools.
- Brex’s channel partners like accounting apps can provide a better customer experience by directly accessing customers’ transactions and statement data, at their permission.
We built the Accounting API to enable ERPs to pull data from Brex through a direct transaction feed, and to allow bookkeeper apps to integrate with Brex for analytics, budgeting, and more.
Intuit’s QuickBooks Online became our first major integration partner for our “Bank Feed” API. QuickBooks is the most popular ERP among our customers, 40% of which are currently leveraging the integration. Fun fact: customers with ERPs linked tend to spend 8x more!
The Accounting API was born during a time where our Cash product was becoming generally available, so it was a unified data pipeline across Card and Cash.
Here’s how the Bank Feed with QuickBooks Online works from the customer’s perspective:
Launching the Accounting API not only helped us close more deals, but also delighted our existing customers who can now dedicate more energy into growing their businesses. Since launching the new Quickbooks integration on the Accounting API, we frequently receive this kind of feedback:
“I just wanted to let you know how much I appreciate your adding Brex to QBO bank feed! This is like winning the lottery, thank you!”
In this post, we will explore the various architectural and design decisions and the process that we took to launch this API.
The Partners’ Perspective
Today, the Accounting API has 3 endpoints:
/accounts, /transactions, /statements
The API has 2 kinds of consumers: partners who choose to integrate with Brex, and end-users visualizing their data. Therefore, we wanted to ensure that the data was coherent from an accounting perspective. For example:
- Statements data is immutable: only closed statements will be returned. The current “open” statement is constantly updated.
- By default, statements are sorted in order of the statement period’s closing date.
- Only posted transactions (not pending) are returned. That being said, we did consider returning pending entries in a separate endpoint to preserve the consistency between changing versus immutable data.
We also built the service to be extensible, future-proof, and partner-agnostic.
Behind the Scenes: Service Architecture
For the core business logic behind the Accounting API, we created a new service called ERP using Protocol Buffers and gRPC in Elixir.
The life cycle of a request:
- The incoming request is handled in a public-facing API service by Phoenix, an Elixir web framework which implements the MVC pattern
- Elixir-grpc is used by the controller in Phoenix to perform a gRPC request to the backend service
- The server gathers the data requested and builds response objects
- When rendering, ExJsonSchema (an Elixir schema validator) validates the data before it is returned
The ERP Service
The role of the ERP service is simply to handle inbound requests, build the response objects by querying upstream services, and return the results as a view.
The ERP service has 3 main components which perform “scatter-gathering” tasks, based on an incoming account id:
- Accounts: Retrieves a customer’s account data and limits
- StatementEntries: Retrieves Card and Cash statement entries
- Statements: Retrieves Card and Cash statements
In the case of Accounts, a request is sent to AccountsServiceProto, which gathers the account details from various services.
We used protobuf-elixir, an Elixir implementation of Google Protocol Buffers. The service and its data objects are defined as follows:
The protobuf-elixir generated stubs allow clients to send requests to our AccountsService with a call like
Data Validation at Runtime
To ensure that the shape of the output returned by the API matches the specs, we used the ExJsonSchema library to validate the data at runtime.
This library allows us to check Elixir maps against a JSON schema. The schema validation can catch cases where required fields are not present, which protobufs can’t enforce (since all fields are optional).
First, we define a schema. For example, the Account schema might look like this:
Once we’re done defining the schema, we use ExJsonSchema.Schema.resolve() to first make sure that the schema definition is valid.
We can then define a ViewValidator module that checks input data against the schema above, using ExJsonSchema.Validator.validate()
Now, the validate_schema() function can be called by the View to make sure the fields match the spec before returning!
Monitoring and Scalability
Datadog is one of our favorite tools for monitoring and searching through logs. Every request in Brex also has a trace_id, which allows us to follow the services and paths of every request.
We also set up monitors for:
- High average latency
- High error rate
- High QPS (queries per second)
We ran a series of load tests using Artillery to check the response time and test the rate limit triggers for the endpoints. Artillery allows us to simulate multiple parallel requests, informing the minimum and maximum response times.
We define load tests in .yaml files, with contextual information such as:
- How many parallel requests to simulate: i.e. 5 users per second, for 10/120/600 seconds
- A set of endpoints to hit, and how much time in between each requests
The framework then runs realistic queries against our endpoints. Results from load testing helped us come up with an upper bound for QPS we can allow per partner, as well as an SLO (service-level objective) for the API’s response time.
Other checks we performed:
- Database checks: make sure all databases being queried have the appropriate indexes
- Data scoping: don’t return data the customer didn’t allow, don’t return another customer’s data
- Script to simulate an initial pull from a client. This pull would retrieve every transaction for a customer in the past year
- A thorough security audit from Brex’s Trust team, who attempted common attacks like authorization bypassing, JWT token manipulation, vulnerability scanning, and more
Rollout & Adoption
The API went into beta in early March 2020. During the process, we listened to feedback from early customers and continued polishing the service, adding frontend interactions and brand new onboarding flows.
The Road Ahead
Our work has just begun! As we continue adding new partners, we have plans to:
- Streamline the onboarding and credential-provisioning process
- Develop a self-serve partner portal
- Build more endpoints to support use cases such as card issuing, setting limits, and onboarding
We’re excited to enable more customers to access Brex data through other platforms they love, save customers time through seamless pipelines, and to bring more services into the Brex ecosystem.
The Team (in alphabetical order)
If you are interested in becoming a partner on our Accounting API, reach out to email@example.com
Want to take on challenges like these? Come join us at Brex!