Skip to main content
A Payment in Payrails is an operation that involves money movements from or to outside the scope of Payrails. This could involve card payments, an external wallet, a bank transfer, any alternative payment methods (APM), etc. There could be many operations involved in a Payment, and each of them has their possible previous and next operations. This means that some operations only make sense if another one happened previously. Also, each operation comes with different types of errors and responses. This is why the best way to describe a Payment is as a collection of operations over time, where the latest operation may define the current status of the Payment. In case this last operation failed for any reason, the status of the payment should not be altered (unless the failure leaves the payment in another status, which in this case should be indicated accordingly). In terms of storage, this approach involves having the following tables:
  • payment = holds the identifier, the most important fields (TBD), and the current status
  • payment_operation = an ordered collection of all the operations that led to the current status (including human interventions and notifications from external providers)
  • payment_operation_log = the complete and raw information for each of those operations, usually involving request and response JSONs for external calls
This structure allows us to have:
  • a fast understanding of where the payment is right now (by querying the payment table)
  • the complete history of how we arrived to that state (by the payment_operation rows related to a payment)
  • durable and auditable logs of what exactly came in and out of our system (in payment_operation_log table for each payment_operation)
This proposed structure is inspired on the Event Sourcing and CQRS microservices patterns. However, because each of the events in the history of a Payment usually involves an interaction with a third-party external service, we cannot leverage all the features proposed by the Event Sourcing pattern, e.g. the ability to recreate a Payment by replaying the events in the queue.

PaymentProviders

Communication with external providers to execute any of the operations should follow the data flow described in this diagram: The logic for interacting with an external provider should be as much encapsulated as possible, leaving the process as:
  • receive a generic PaymentRequest object
  • validate if the object contains all the requirements for the specific provider
  • translate the object to the required parameters for the specific provider
  • load merchant specific configuration (merchantId, credentials, etc.) for calling the provider
  • execute the operation (managing all the communication details, e.g. timeouts, authentication, signature, etc.)
  • interpret the response received to go from provider specific codes to our own generic codes
  • respond with a generic PaymentResponse object

OperationType

There are many possible OperationTypes in our system and even more to come. The first group are the basic financial operations for payments: The next group is related to notification to and from providers. The next group, equally important, is for querying external services about payments. These operations are usually key for reconciliation processes. Next, we have operations related to tokenization of payment instruments. Finally, we have operations related to 3DS and Payer Authentication processes.

OperationResult

Each operation we execute has its own possible result, which will be the selected after mapping the status codes and content of the response from the external provider. We should group the codes to discuss them better. Starting with the happy path: Next, also a type of happy path but with some type of pending action before they can be completed. Let’s discuss the most worrying ones next, the ones where we don’t know what actually happened. Any of these results should be considered as a high priority to alert and analyze as soon as possible. Next, equally important to analyze and optimize if possible, are the connection and timeout related codes. Next, let’s look at the payment rejection results, which depending on the provider and other factors, may go from very specific to completely generic. Next, a special type of the payment rejections, the payment instrument rejections. Next, we have the parameter or configuration errors. These could be similar to the rejections, but could be caused by a problem with formatting, missing required parameters, etc. Last but not least, if something went very wrong during the operation and there was actually an internal server error, we need to indicate it. And, of course, fix it as soon as possible!

PaymentStatus

Once an operation of a specific type (OperationType) was executed and we interpreted its result (OperationResult), we are ready to change the status of the actual payment. In case the operation we executed on the payment wasn’t successful, the status of the payment shouldn’t be changed except for a newly created payment, if authorization failed, we should change the status to Failed. However, if the result of the operation is UNKNOWN, we must change the status of the payment to UNKNOWN and act fast to solve the situation so that we are able to either move the payment back to the previous status or to the new one.

Disclaimer

There may be a RECONCILED status in the future, but discussions are pending on how to implement this as many of the already defined status in the list can be reconciled against the bank reconciliation file.
Last modified on September 23, 2026