A Payment in Payrails is an operation that involves money movements from or 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 its possible previous and next operations. This means that some operations only make sense if another one happened previously. All the possible operations you can execute on a Payment can be found in the Operation Types section of our documentation.

Also, each operation comes with different types of errors and responses. The complete list of result codes for our payment operations can be found in our documentation's Result Codes section.

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, 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 at 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)

For our tech-savvy readers, this structure is inspired by the Event Sourcing and CQRS microservices patterns.

In JSON, the representation of this structure is the following:

  "id": "fe67fc45-ce47-4fc8-a283-22d03ae68b77", //ID of the Payment
  "status": "Failed",
  "history": [
      "id": "7314d8ea-ad96-4564-a954-8fc17584823b", //ID of the PaymentOperation
      "type": "Authorize",
      "result": "INSUFFICIENT_BALANCE",
      "responseCode": "3000"
      "log": [
          "id": "f56051f5-2b41-4f64-8626-d97ba0b9a045", //ID of the PaymentOperationLog
          "request": "{}",
          "response": "{}"

Payment Providers

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