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.
After a grace period of 1 hour, the transaction will automatically move to COMMITTED
state and cannot be changed anymore. This means that the transaction is now final and the server will not allow any further changes to it. The grace period is meant to facilitate cases where a confirmed transaction still needs to be rejected, for example if it is found out that the transaction was incorrect.
Transactions can be confirmed as failed from any point in the flow unless the transaction has already been committed. 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 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 some error 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 error 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.
State | Current result_code | Outcome |
---|---|---|
AWAITING_CONFIRM | SUCCESS | Transaction is confirmed as successful, transaction state is set to CONFIRMED and result_code is SUCCESS . |
CONFIRMED | SUCCESS | Confirm request is successful, but transaction is not modified. |
COMMITTED | SUCCESS | Confirm 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 error 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.
State | Current result_code | Outcome |
---|---|---|
PROCESSING | empty | Transaction is confirmed as failed, transaction state is set to CONFIRMED and result_code is set to the given result code. |
AWAITING_CONTINUE | empty | Transaction is confirmed as failed, transaction state is set to CONFIRMED and result_code is set to the given result code. |
AWAITING_CONFIRM | SUCCESS | Transaction is confirmed as failed, transaction state is set to CONFIRMED and result_code is set to the given result code. |
AWAITING_CONFIRM | some error | Transaction is confirmed as failed, transaction state is set to CONFIRMED and the current result_code is kept. |
CONFIRMED | SUCCESS | Transaction is confirmed as failed, transaction state is kept as CONFIRMED and result_code is set to the given result code. |
CONFIRMED | some error | Transaction is confirmed as failed, transaction state is kept as CONFIRMED and the current result_code is kept. |
COMMITTED | some error | Confirm call is successful, but transaction is not modified. |
none | none | A new transaction is created with state CONFIRMED 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.
State | Current result_code | Given result_code | Outcome |
---|---|---|---|
PROCESSING | empty | SUCCESS | Confirm call is rejected with BAD_REQUEST , transaction state is not modified. |
AWAITING_CONTINUE | empty | SUCCESS | Confirm call is rejected with BAD_REQUEST , transaction state is not modified. |
AWAITING_CONFIRM | some error | SUCCESS | Confirm call is rejected with BAD_REQUEST , transaction state is not modified. |
CONFIRMED | some error | SUCCESS | Confirm call is rejected with BAD_REQUEST , transaction state is not modified. |
COMMITTED | SUCCESS | some error | Confirm call is rejected with BAD_REQUEST , transaction state is not modified. |
COMMITTED | some error | SUCCESS | Confirm call is rejected with BAD_REQUEST , transaction state is not modified. |
none | none | SUCCESS | Confirm call is rejected with BAD_REQUEST , no transaction is created. |
Unconfirmed transactions
Currently there is no limit for number of unconfirmed transactions for a terminal. This will change in v1
of the API.
To ensure that the protocol works as expected, each terminal can only have 1 unconfirmed transaction. If the total number of unconfirmed transactions for a terminal exceeds this limit, the server will not accept any new transactions until a previous one is confirmed. This limit also includes failed 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.