July 6, 2023
October 12, 2024
Developers
12 Minutes, 51 Seconds

Technical deep dive: Payouts at Stitch

At Stitch, we recognise the necessity of a robust payouts mechanism and have engineered a system that is both agile and secure. Here, we shine a spotlight on how we’ve built Payouts at Stitch. 

Taufeeq Razak, Full Stack Engineer
Share this article
Technical Deep Dive: Payouts at Stitch

In the constantly evolving landscape of digital finance, fluidity and efficiency are not just buzzwords but essential gears that drive the economic engine. Payouts, often unsung, play a cardinal role as the sinews connecting businesses, customers and financial institutions. From enabling withdrawals and handling refunds, to ensuring merchants, vendors and suppliers receive their dues, payouts are indispensable.

At Stitch, we recognise the necessity of a robust payouts mechanism and have engineered a system that is both agile and secure. Here we’ll shine a spotlight on how we’ve built Payouts at Stitch.

The intricacies of payouts

In the Stitch milieu, a payout is a series of operations that enables funds to move from a merchant’s bank account to a user's account, or to that of another business. Picture a wallet-based application where today, customers can effortlessly deposit money via Stitch's Pay by bank product, or by Card or Cash. The challenge begins when a user wishes to withdraw money. This is where Payouts come into play.

As an end-to-end payment services provider, Stitch handles the entire spectrum of money movement – from receiving to disbursing and reconciling funds. Payouts is the bedrock that enables us to move money from a merchant to a customer, supplier or anyone else.

The mechanics of Payouts

The Money Movement Engine: the conductor at Stitch

The Stitch architecture is built upon a Monorepo structure that consolidates the codebase into a single repository. Within this system, the Money Movement Engine comprises a series of services that make payouts possible. It not only communicates with external services but also provides an abstract layer to handle bank-specific complexities.

Core responsibilities of the Money Movement Engine:

  • Coordinating with external banks: The Money Movement Engine is entrusted with the task of securely relaying instructions to external banks, orchestrating the movement of money between accounts
  • Tracking accounts and balances: The Money Movement Engine also takes on the mantle of a vigilant accountant, keeping a close eye on account balances, thus ensuring an accurate ledger of funds and the state of accounts

Defining The Money Movement Engine

The Money Movement Engine encapsulates an API interface and workers that run on a Bull MQ queue. These components collectively handle a range of tasks essential for managing the movement of money. The API interface is the internal point of contact to issue payout instructions, whereas the workers, operating in the background, handle tasks such as batching instructions, creating payout instruction files and parsing statement files.  

The payouts process explained

Consider the wallet-based use case mentioned earlier, where a user wants to withdraw money. Here’s how the flow works:

1. Request initiation: The merchant initiates the withdrawal by sending a request through the public-facing GraphQL API
2. Instruction to Money Movement Engine: The API processes the request and issues an instruction to the Money Movement Engine to move the specified amount of funds
3. Validation and grouping: The Money Movement Engine validates the instruction to ensure it's legitimate. It then groups it with other payout instructions, allowing for efficient batch processing
4. Batch Payout Instruction File creation: All the grouped instructions are converted into a Batch Payout Instruction File. This file is essentially the medium of communication with the bank, containing all the instructions to move money from source accounts to destination accounts. For batch payout instruction files, the information exchanged includes details of the source account where money needs to be transferred, as well as the destination account where the money needs to land. This includes the source account number and branch code, destination account number and branch code and amount to be transferred 
5. Uploading file to bank’s server: The Batch Payout Instruction File is then securely uploaded to the bank’s server through host-to-host connections. This informs the bank of the transactions that need to be processed

Navigating financial waters: Host-to-host connections

In the versatile and dynamic world of banking, initiating the movement of money necessitates bank-specific integrations. In South Africa, host-to-host integrations reign supreme. However, like different dialects, each bank has its unique file formats and integration methods.

The Money Movement Engine, therefore, has to be an adroit linguist, able to translate complex bank-specific intricacies into an easily consumable interface.

Host-to-host integration in essence is an exchange of instructions and information through file transfers. However, not all files are created equal. Depending on the bank, these files can come in various formats, such as MT940, XML, and others. Due to the various natures that these files may come in, each partner bank requires custom integration work which can often take months of custom development time. However, the Money Movement Engine is well suited to handle various file formats, as this was a key consideration in design of the platform.

The common types of files exchanged include:

  • Batch Payout Instruction Files: These contain instructions to move money from a source account to a destination account
  • Reply files: These are files that are sent back after an instruction file is sent, indicating acceptance or rejection of payout instructions
  • Statement files: These contain granular details of each transaction in a particular account and general details about the account, such as running balances

Crafting the code: file formats and conversions in detail

Diving into the specifics, let’s examine the characteristics of these files and unravel the intricacies involved in converting and handling different file formats.

Batch Payout Instruction Files

Let’s examine a format analogous to what one of our partner banks might utilise:

000A2019062500012ABCCompany 000123450000
060A04Z0012190625190625190625000456789123ABCBatch XX
060A1023100607091234567Z0010045678106007009123456710000012345620190625880
Sample1 Salary John Doe ABCDept
060A1223100607091234567Z001004569012310060709123456100000123452019062510000
Sample2 Contra Jane Doe XYZDept
060A1023100607091234567Z0010045611201567745123456710000098765420190625880
Sample3 Salary Alice Johnson DEFDept
060A1223100607091234567Z001004562120156774512345671000009876542019062510000
Sample4 Contra Bob Smith GHIDept

This sample file commences with a header row, followed by a series of detail rows. The header is likely to comprise the file date, an identifier and other constants. The detail rows, denoting payout instructions, include pertinent details like account numbers and descriptions. Fixed-width formats are often preferred for their simplicity in processing.

Enabling seamless file generation with TypeScript schemas

Having examined the example of a batch payout instruction file, it is clear that these files carry a wealth of structured information. To handle this efficiently and programmatically, let's delve into how we can leverage TypeScript schemas to automate and streamline the process of file generation.

Defining our TypeScript schema

To generate files in the above format, we can define a schema using TypeScript. This schema outlines the structure of the file, specifying the type and length of each field. With this schema in place, we can efficiently generate batch payout instruction files that conform to the desired format.

Here's an example of what our TypeScript schema might look like:

import { FieldConfig } from './types';

export type UserHeaderField =
| 'Record identifier'
| 'Data set status'
| 'Bankserv record identifier'
| 'Bankserv user code'
| 'Bankserv creation date'
| 'Bankserv purge date'
| 'First action date'
| 'Last action date'
| 'First sequence number'
| 'User generation number'
| 'Type of service'
| 'Accepted report'
| 'Account type correct'
| 'Filler 1';

export const userHeader: FieldConfig<UserHeaderField>[] = [
{
name: 'Record identifier',
dataType: 'numeric',
length: 3,
comments: `The Record Identifier identifies the type of
transactions.
},
{
name: 'Data set status',
dataType: 'alpha',
length: 1,
comments: `The status of the record: "T" - Test data, "L" - Live data.`,
},
// ...rest of the userHeader line item
];

export type StandardTransactionField =
| 'Record identifier'
| 'Data set status'
| 'Bankserv record identifier'
| 'User branch'
| 'User nominated account'
| 'User code'
| 'User sequence number'
| 'Homing branch'
| 'Homing account number'
| 'Type of account'
| 'Amount'
| 'Action date'
| 'Entry class'
| 'Tax code'
| 'Filler 1'
| 'User reference'
| 'Homing account name'
| 'Non-standard homing account number'
| 'Filler 2'
| 'Homing institution'
| 'Filler 3';

export const standardTransaction: FieldConfig<StandardTransactionField>[] = [
{
name: 'Record identifier',
dataType: 'numeric',
length: 3,
comments: `The Record Identifier identifies the type of transactions.`,
},
{
name: 'User branch',
dataType: 'numeric',
length: 6,
comments: `Bank Branch code of the Electronic Banking Suite
User's nominated account.`,
},
{
name: 'User nominated account',
dataType: 'numeric',
length: 11,
comments: `Electronic Banking Suite User's nominated
account number.`,
},
{
name: 'Homing branch',
dataType: 'numeric',
length: 6,
comments: `Homing account Branch number.`,
},
{
name: 'Homing account number',
dataType: 'numeric',
length: 11,
comments: `The beneficiary's account number.`,
},
{
name: 'Type of account',
dataType: 'numeric',
length: 1,
comments: `Identifies the homing account type.`,
},
{
name: 'Amount',
dataType: 'numeric',
length: 11,
comments: `Monetary value of the Transaction.`,
},
// ..rest of the standardTransaction line item
];

This TypeScript schema serves as a blueprint for the structure of the batch payout instruction files. By defining the data types, lengths and other properties of each field, the schema ensures that the generated files adhere to the required standard.

Automating File Generation

With the schema defined, we can write scripts that map data from a database or another source into the fixed-width file format. This ensures the files are created in the structure outlined by the schema. Additionally, having a schema acts as documentation, which is invaluable for new team members or when integrating with systems that need to understand the file structure.

Below is an example of how a shared function is used to construct a line, depending on the configuration defined in the schema.

export const constructLine = (
fieldConfigs: FieldConfig[],
values: Record<string, number | string>
) =>
fieldConfigs.reduce(
(line: string, field) => line + formatLine(field, padField(field, values[field.name])),
''
);

To tie it all together, this is how a standard transaction line can be constructed using the function above.

function constructStandardTransactionLine(standardTxn: StandardTransactionRecord): string {
return constructLine(sharedSpec.standardTransaction, {
'Record identifier': standardTxn.recordIdentifier,
'Data set status': standardTxn.datasetStatus,
'Bankserv record identifier': standardTxn.bankservRecordIdentifier,
'User branch': standardTxn.userBranchCode,
'User nominated account': standardTxn.userNominatedAccount,
'User code': standardTxn.bankservUserCode,
'User sequence number': standardTxn.userSequenceNumber,
'Homing branch': standardTxn.homingBranch,
'Homing account number': standardTxn.homingAccountNumber,
'Type of account': standardTxn.homingAccountType,
Amount: toAmount(standardTxn.amount),
'Action date': formatDateToSixCharacters(standardTxn.actionDate),
'Entry class': standardTxn.entryClass,
'Tax code': standardTxn.taxCode,
'Filler 1': '',
'User reference': generateTransactionRecordUserReference(standardTxn.userReference),
'Homing account name': standardTxn.homingAccountName,
'Non-standard homing account number': standardTxn.nonStandardHomingAccountNumber,
'Filler 2': '',
'Homing institution': standardTxn.homingInstitution,
'Filler 3': '',
} as Partial<Record<sharedSpec.StandardTransactionField, any>>);
}

This function takes a standardTransaction object as an argument and uses the constructLine function to create a string that represents a single transaction line. The fields and values from the standardTransaction object are formatted according to the configuration defined in the schema.

By employing TypeScript schemas and automating file generation with scripts, we can ensure that batch payout instruction files are generated accurately and efficiently.

Bridging systems

The Money Movement Engine harnesses these files to bridge the modern financial ecosystem with traditional banks. It employs a TypeScript (TS) schema to convert internal TS objects into corresponding line items for batch file payout instructions.

Additionally, custom parsing logic is woven into The Money Movement Engine, enabling it to convert each line item in a statement into a corresponding action within the Stitch infrastructure. This could be anything from updating transaction statuses to adjusting account balances and issuing payment confirmations to merchants.

For the seamless exchange of statement files, an SFTP to blob-storage connection is employed to efficiently transfer files between Stitch’s servers and banking partners.

Bridging these systems effectively is essential, but when dealing with large volumes of transactions, it’s crucial that the system can scale and manage the flow without any hiccups.

Ensuring fluidity: managing high-volume transactions

Ensuring scalability is a critical aspect of any payouts system. To achieve scalability, Stitch employs various strategies. One such strategy is bundling multiple payout instructions within each batch payout instruction file. This allows for more efficient processing and reduces the number of requests sent to the server.

Additionally, statement files, which can contain millions of line items, are processed in a distributed, parallel manner and parsed in chunks. This ensures optimal performance and scalability as it allows for multiple processors to work simultaneously and handle the load.

Conclusion

Payouts, often overlooked, are vital components in the vast expanse of financial infrastructure. They form the conduits that facilitate the smooth flow of funds. At Stitch, The Money Movement Engine is designed with the recognition of the role payouts play in the broader financial ecosystem. While it’s essential to have a well-structured system, what is paramount is the ability to efficiently and securely address the real-world challenges faced by businesses and individuals.

The technical snippets highlight one piece of a complex system like The Money Movement Engine. This kind of systematic approach ensures that payouts are not only managed effectively, but also handled with precision and adherence to the highest standards.

The Stitch Money Movement Engine serves as a bridge, ensuring that whether it is the joy of a customer completing a purchase, a merchant receiving timely payment or a seamless funds transfer, the pulse of economic activity continues unhindered.

As the landscape of digital finance evolves, the significance of reliable and efficient payouts will only grow. Through the implementation of well-thought-out schemas and automated file-generation techniques, Stitch ensures accuracy and scalability, which are pivotal in handling complex financial transactions. The Money Movement Engine is just one of the many gears in the larger machinery, working towards a future where financial transactions will be even more accessible, secure and efficient for all.

Leverage API-powered payouts and refunds Request a demo