Using Payment Apps
Payment App is a Saleor App that handles the payment process for a specific payment provider.
End users are unaware of the payment app's existence as they communicate only with Saleor via GraphQL API.
Saleor then communicates with the app using synchronous webhooks, automatically updates statuses and balances, and returns the response to the storefront. Checkout is finalized with the checkoutComplete
mutation.
Payment apps are responsible for handling the implementation details of payments with a certain payment gateway (i.e. Adyen, Stripe) while Saleor hides the complexity of the payment process from the storefront under a consistent set of GraphQL queries and mutations.
In this guide, we use the terms Payment App and Payment Gateway interchangeably.
Processing Payments With the Payment App
This section describes the flow for processing payments using Payment Apps. The communication between the storefront and the Payment App goes through Saleor. Based on the Payment App's response, Saleor creates or updates the transactions with the appropriate statuses and balances.
The following diagram shows the flow of processing a payment with a Payment App:
Creating Orders
Payment Apps support two distinct ways of processing checkouts:
- first payments, then order
- first order, then payments
The first approach is used by default. It means that the order can be only created after the checkout is fully paid. See allowUnpaidOrders
setting for more details.
In both cases, the order is created by calling the checkoutComplete
mutation.
Initialize Payment Gateway
To initialize the payment app, call the paymentGatewayInitialize
mutation.
The data
provided in the
paymentGateways
will be sent to the payment app. Saleor will then return the data
received from each app as a response.
This step is useful when information needs to be obtained from the Payment App even before initializing the payment process. Typically used for getting the available payment methods for a specific checkout, or for fetching the payment form configuration to be rendered on the frontend. Multiple Payment Apps can be initialized with a single mutation call.
The paymentGatewayInitialize
mutation requires the following arguments:
id
- ID of theCheckout
orOrder
amount
- The amount requested for initializing the payment gateway. The amount is rounded based on the given currency precision. If not provided, the difference betweencheckout.total
and transactions that are already processed will be sent to the apps.paymentGateways
- A list of Payment Apps to initialize. If not provided, all apps subscribed to the webhookPAYMENT_GATEWAY_INITIALIZE_SESSION
will be used.paymentGateways.id
- The identifier of the Payment App to initialize.paymentGateways.data
- The data to be passed to the Payment App.
The paymentGatewayInitialize
mutation returns the following response:
gatewayConfigs
- A list of Payment Apps that were initialized.gatewayConfigs.id
- The app identifier.gatewayConfigs.data
- The data required to initialize the Payment App.gatewayConfigs.errors
- A list of errors related to the specific payment gateway app.errors
- Errors related to the mutation call.
The following example shows how to use the paymentGatewayInitialize
mutation to initialize the payment gateways:
mutation {
paymentGatewayInitialize(
id: "Q2hlY2tvdXQ6ZmIxMzljMjgtYWY4OS00Mzk2LWEyMjgtZmM2ZDg0NGFhOWY3"
amount: 100
paymentGateways: [
{ id: "app.saleor.payment", data: { details: { passed: "to-app" } } }
]
) {
gatewayConfigs {
id
data
errors {
field
message
code
}
}
errors {
field
message
code
}
}
}
The response:
{
"data": {
"paymentGatewayInitialize": {
"gatewayConfigs": [
{
"id": "app.saleor.payment",
"data": {
"json": "data-returned-by-app"
},
"errors": []
}
],
"errors": []
}
}
}
The mutation will trigger the PAYMENT_GATEWAY_INITIALIZE_SESSION
webhook.
For more details about this webhook, please refer to the Transaction events guide.
Initialize Transaction
To initiate payment processing, call the transactionInitialize
mutation.
The data
provided in the
paymentGateways
field will be sent to the payment app, and Saleor will return the
data
received from the app as a response.
Based on the Payment App's response, Saleor will create or update the transaction with the appropriate status and balance.
Depending on the implementation of the Payment App, the transactionInitialize
may charge or authorize the customer.
The transactionInitialize
mutation accepts the following arguments:
id
- ID of theCheckout
orOrder
.paymentGateway
- The payment gateway used to initiate payment processing.paymentGateway.id
- The identifier of the payment gateway app.paymentGateway.data
- The data to be passed to the payment gateway.amount
- The amount requested for the payment. The amount is rounded based on the given currency precision. If not provided, the difference betweencheckout.total
and transactions that are already processed will be sent.action
- The expected action (charge or authorization). By default, thechannel.defaultTransactionFlowStrategy
will be used and can be configured from the Saleor Dashboard. The default value can be overridden only by an app that hasHANDLE_PAYMENTS
permission.idempotencyKey
- Idempotency key which will be passed to the payment app. The key needs to be unique for the pair -idempotencyKey
andpaymentGateway.id
. This field was introduced in Saleor 3.14.customerIpAddress
- The customer's IP address. If not provided, Saleor will try to determine the customer's IP address on its own. The customer's IP address will be passed to the payment app. The IP should be in ipv4 or ipv6 format. The field can be used only by an app that hasHANDLE_PAYMENTS
permission. This field was introduced in Saleor 3.16.
Depending on the provided idempotencyKey
value, the below results will happen:
-
If
idempotencyKey
key is not provided, a new one will be generated automatically. -
If
idempotencyKey
is provided as an empty string, the error with the INVALID code will be raised. -
If provided pair:
idempotencyKey
andpaymentGateway.id
is already assigned to the existing transaction that belongs to a different order/checkout, the error with the UNIQUE code will be raised. -
If provided pair:
idempotencyKey
andpaymentGateway.id
is used more than once for a single order/checkout but with differentamount
,action
input, the error with UNIQUE code will be raised. -
If provided pair:
idempotencyKey
andpaymentGateway.id
is used more than once for a single order/checkout, with the same input foramount
,action
, the existingTransactionItem
will be used to send the request to the payment app.
The idempotency is used to detect the same payment request made by transactionInitialize
. The idempotency key should be generated by the storefront and passed as a mutation input. Any retries made with transactionInitialize
mutation should provide the same idempotency key.
In Web, UUID can be generated by using Crytpo Web API:
const idempotencyKey = window.crypto.randomUUID();
The key should be also stored in the form of persistent storage. This way it can be re-used even if the user leaves the page/app. On the Web, this can be done with localStorage
:
// set
localStorage.setItem("idempotencyKey", idempotencyKey);
// get
const idempotencyKey = localStorage.getItem("idempotencyKey");
The transactionInitialize
mutation returns the following response:
transaction
- The initialized transaction.transactionEvent
- The event created based on the response received from the payment app.data
- The data returned by the app.errors
- Errors related to the mutation call.
The mutation will trigger the TRANSACTION_INITIALIZE_SESSION
webhook.
For more details about this webhook, please refer to the Transaction events guide.
The following example demonstrates how to use the transactionInitialize
mutation to initiate payment processing:
mutation TransactionInitialize {
transactionInitialize(
amount: 100
id: "Q2hlY2tvdXQ6MzE3NDk5MjgtZDFkMC00NjdjLTgxNjktMGRiZGM4ZGJhMGZh"
paymentGateway: {
id: "app.saleor.payment"
data: { details: "passed-to-app" }
}
) {
transaction {
id
}
transactionEvent {
type
pspReference
}
errors {
field
message
code
}
}
}
The response:
{
"data": {
"transactionInitialize": {
"data": {
"some-json": "data"
},
"transaction": {
"id": "VHJhbnNhY3Rpb25JdGVtOjhhMzE1ODVlLWE1MTktNDVkMS1hNTFmLTYwZGZlNGRkM2FkNg"
},
"transactionEvent": {
"type": "CHARGE_SUCCESS",
"pspReference": "ppp-123"
},
"errors": []
}
}
}
The current status of payment can be determined based on the value of transactionEvent.type
:
AUTHORIZATION_ACTION_REQUIRED
orCHARGE_ACTION_REQUIRED
indicates that additional action is required from the customer to finalize the payment (e.g. 3D Secure, mobile app confirmation, redirect to the payment provider page, etc.) Calling thetransactionProcess
mutation is required in order to proceed.AUTHORIZATION_SUCCESS
indicates that funds have been authorized.AUTHORIZATION_FAILURE
indicates that authorization has failed.AUTHORIZATION_REQUEST
indicates that authorization is pending.CHARGE_SUCCESS
indicates that funds have been charged.CHARGE_FAILURE
indicates that charging has failed.CHARGE_REQUEST
indicates that charging is pending.
Pending statuses are used when the Payment App requires additional time or a manual action on the payment provider side to process the payment.
Depending on the implementation, the Payment App may report the final status of the payment later using the transactionEventReport
mutation.
Process Transaction
If transactionInitialize
or transactionProcess
returns transactionEvent.type
equal to AUTHORIZATION_ACTION_REQUIRED
or CHARGE_ACTION_REQUIRED
, additional steps from the customer are required. After completing those steps, pass the results to the transactionProcess
mutation.
The transactionProcess
mutation accepts the following arguments:
id
- The ID of the transaction returned bytransactionInitialize
.data
- The data to be passed to the payment gateway.customerIpAddress
- The customer's IP address. If not provided, Saleor will try to determine the customer's IP address on its own. The customer's IP address will be passed to the payment app. The IP should be in ipv4 or ipv6 format. The field can be used only by an app that hasHANDLE_PAYMENTS
permission. This field was introduced in Saleor 3.16.
The transactionProcess
mutation returns:
transaction
- The processed transaction.transactionEvent
- The event created based on the response received from the payment app.data
- The data returned by the app.errors
- Errors related to the mutation call.
The mutation triggers the TRANSACTION_PROCESS_SESSION
webhook.
For more details about this webhook, refer to the Transaction events guide.
The following example demonstrates how to use the transactionProcess
mutation to process the payment:
mutation TransactionInitialize {
transactionProcess(
id: "VHJhbnNhY3Rpb25JdGVtOjhhMzE1ODVlLWE1MTktNDVkMS1hNTFmLTYwZGZlNGRkM2FkNg"
data: { additional: { actions: "details" } }
) {
transaction {
id
}
transactionEvent {
type
pspReference
}
errors {
field
message
code
}
}
}
The response:
{
"data": {
"transactionProcess": {
"data": {
"some-json": "data"
},
"transaction": {
"id": "VHJhbnNhY3Rpb25JdGVtOjhhMzE1ODVlLWE1MTktNDVkMS1hNTFmLTYwZGZlNGRkM2FkNg"
},
"transactionEvent": {
"type": "CHARGE_SUCCESS",
"pspReference": "ppp-123"
},
"errors": []
}
}
}
The current status of payment can be determined based on the value of transactionEvent.type
.
On some occasions, multiple calls to transactionProcess
may be required to proceed with the payment.
Related Resources
- Building payment apps - Guide on how to build a payment app