Transactional reliability

In a payment API, it is crucially important to ensure reliable and consistent monetary transactions.

✅ The customer gets a product and is charged money for it.
✅ The customer doesn’t get a product and isn’t charged any money for it.

❌ The customer gets a product and isn’t charged money for it.
❌ The customer didn’t get a product but is charged money for it.

This API implements this similarily to two-phase commit protocol. In payment systems, this is usually implemented implicitly by a separate authorization and capture messages, whereas in this API the behaviour is explicit.

The client is required to explicitly confirm every transaction as successful or failed after it has received the response to the transaction. In two-phase commit protocol, making a purchase is a “commit-request”, and confirming the purchase as successful is a “commit”.

In practice, this means that the client must:

  • Store the transaction external_id in stable storage, such as a database, before sending the purchase request to the server.
  • Attempt to confirm every transaction as either successful or failure indefinitely after the transaction is completed, until the server confirms that it has recorded the result in stable storage.

Transactions should only be confirmed as successful if the client has received the transaction details from the server and the state is AWAITING_CONFIRM and result_code is SUCCESS. The transaction state will be set to CONFIRMED by the confirm call.

A transaction can be confirmed as failed at any point up until it is committed (see below). This will work consistently even if the transaction wasn’t even created, in which case the server will create the failed transaction. This behaviour allows clients to recover from any failure scenario. A comprehensive list of all possible transaction states and their outcomes is shown below.

Based on the confirmed result of the transaction, the server will either settle the transaction according to the normal settlement flow for the merchant to receive the money, or release a hold on the customer’s funds if any was made. Confirming a transaction shifts the responsibility of completing the transaction from the client to the server.

Transaction commit

Every confirmed transaction eventually ends up in COMMITTED state. This means that the transaction is final and neither the client nor the server can make any further changes to it.

  • A transaction confirmed as failed will be immediately committed.
  • A transaction confirmed as successful can be re-confirmed as failed for 1 hour. After that grace period, the transaction will automatically be committed. This is meant to facilitate cases where a confirmed transaction still needs to be rejected, for example if it is determined that the transaction was incorrect.

Transaction confirmation

The transaction confirmation is done by sending a POST request to the /transaction/confirm endpoint with the external_id of the transaction and a result_code of either SUCCESS or any non-SUCCESS (ie. failure) code. Sending a result_code of SUCCESS means attempting to confirm the transaction as successful. Sending a result code of anything else than SUCCESS means attempting to confirm the transaction as failed. The client is free to use any failure code it wants, but it is recommended to use the error codes from the Results and errors.

Successful transactions

Transactions should be confirmed as successful primarily from AWAITING_CONFIRM state only. However, due to possible network errors, the server will also accept confirmations to success that do not modify anything, which keeps the confirmation idempotent. This means that the client can retry the confirmation indefinitely until it succeeds, without worrying about duplicate confirmations. If the transaction flow has been handled correctly, there is no way for the server to fail the confirmation to success, as it is past the after the “commit-request” phase.

StateCurrent result_codeOutcome
AWAITING_CONFIRMSUCCESSTransaction is confirmed as successful, transaction state is set to CONFIRMED and result_code is SUCCESS.
CONFIRMEDSUCCESSConfirm request is successful, but transaction is not modified.
COMMITTEDSUCCESSConfirm request is successful, but transaction is not modified.

Failed transactions

Transactions can be confirmed as failed from any point in the flow, unless the transaction has already been committed. If the transaction is currently being processed, confirming to failure will automatically trigger an abort of the transaction. The server will accept confirmations to failure from any state, but the given result_code will only be set as the transaction result_code if there was no failure code already set. This makes also the confirmation to failure as idempotent, meaning that the client can retry the confirmation indefinitely until it succeeds, without worrying about duplicate confirmations.

StateCurrent result_codeOutcome
PROCESSINGemptyTransaction is confirmed as failed, transaction state is set to COMMITTED and result_code is set to the given result code.
AWAITING_CONTINUEemptyTransaction is confirmed as failed, transaction state is set to COMMITTED and result_code is set to the given result code.
AWAITING_CONFIRMSUCCESSTransaction is confirmed as failed, transaction state is set to COMMITTED and result_code is set to the given result code.
AWAITING_CONFIRMany failureTransaction is confirmed as failed, transaction state is set to COMMITTED and the current result_code is kept.
CONFIRMEDSUCCESSTransaction is confirmed as failed, transaction state is set to COMMITTED and result_code is set to the given result code.
COMMITTEDany failureConfirm call is successful, but transaction is not modified.
nonenoneA new transaction is created with state COMMITTED and result_code set to the given result code.

Bad transitions

Certain transitions are not allowed or make no sense. The request will be rejected with BAD_REQUEST and transaction state is not modified. These results should never be reached in normal circumstances, so they indicate a consistency error in the client.

StateCurrent result_codeGiven result_codeOutcome
PROCESSINGemptySUCCESSConfirm call is rejected with BAD_REQUEST, transaction state is not modified.
AWAITING_CONTINUEemptySUCCESSConfirm call is rejected with BAD_REQUEST, transaction state is not modified.
AWAITING_CONFIRMany failureSUCCESSConfirm call is rejected with BAD_REQUEST, transaction state is not modified.
COMMITTEDSUCCESSany failureConfirm call is rejected with BAD_REQUEST, transaction state is not modified.
COMMITTEDany failureSUCCESSConfirm call is rejected with BAD_REQUEST, transaction state is not modified.
nonenoneSUCCESSConfirm call is rejected with BAD_REQUEST, no transaction is created.

Unconfirmed transactions

To ensure that the protocol works as expected, each terminal can only have 1 unconfirmed transaction. When there’s an unconfirmed transaction for a terminal, the server will not accept any new transactions for that terminal until the previous one is confirmed. This limit also includes unsuccessful transactions which must be confirmed as well.

This means that if the client ever forgets an external_id for a transaction before confirming that transaction, the client has no direct way of recovering from this situation. That is why it is crucial to ensure that the external_id is stored in stable storage, such as a database with durability guarantees, before sending the purchase request to the server.

Since there is no way to guarantee that this situation will never happen, as hardware can fail, there needs to be some way to recover from this situation. For this purpose, there is a separate API endpoint, /transaction/unconfirmed, to list all unconfirmed transactions for a terminal. This call can be used to first retrieve the list of unconfirmed transactions and then confirm them as either successful or failure one by one until the list is empty. This is meant as a last resort option and a development time aid and should not be used customarily.