Payments
Introduction
The process below describes the key milestones in the Saleor payment flow. Additional steps may also occur along the way; however, the purpose of this instruction is to deliver a base reference for the user to work with.
Saleor distinguishes two different approaches to processing payments:
- By using a Payment App.
- By using a Custom App.
- By using a Plugin embedded in the Saleor code (legacy).
If you're considering building a custom payment integration, please check if the payment provider you're interested in isn't already available Saleor App Store.
Payment App
What is a Payment App
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 feature was introduced in Saleor 3.13.
This feature is in the Feature Preview stage, which means that it is available for experimentation and feedback. However, it is still undergoing development and is subject to modifications.
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 theCheckoutorOrderamount- The amount requested for initializing the payment gateway. If not provided, the difference betweencheckout.totaland 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_SESSIONwill 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 theCheckoutorOrder.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. If not provided, the difference betweencheckout.totaland transactions that are already processed will be sent.action- The expected action (charge or authorization). By default, thechannel.defaultTransactionFlowStrategywill be used and can be configured from the Saleor Dashboard. The default value can be overridden only by an app that hasHANDLE_PAYMENTSpermission.idempotencyKey- Idempotency key which will be passed to the payment app. The key needs to be unique for the pair -idempotencyKeyandpaymentGateway.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_PAYMENTSpermission. This field was introduced in Saleor 3.16.
Depending on the provided idempotencyKey value, the below results will happen:
-
If
idempotencyKeykey is not provided, a new one will be generated automatically. -
If
idempotencyKeyis provided as an empty string, the error with the INVALID code will be raised. -
If provided pair:
idempotencyKeyandpaymentGateway.idis 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:
idempotencyKeyandpaymentGateway.idis used more than once for a single order/checkout but with differentamount,actioninput, the error with UNIQUE code will be raised. -
If provided pair:
idempotencyKeyandpaymentGateway.idis used more than once for a single order/checkout, with the same input foramount,action, the existingTransactionItemwill 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_REQUIREDorCHARGE_ACTION_REQUIREDindicates 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 thetransactionProcessmutation is required in order to proceed.AUTHORIZATION_SUCCESSindicates that funds have been authorized.AUTHORIZATION_FAILUREindicates that authorization has failed.AUTHORIZATION_REQUESTindicates that authorization is pending.CHARGE_SUCCESSindicates that funds have been charged.CHARGE_FAILUREindicates that charging has failed.CHARGE_REQUESTindicates 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_PAYMENTSpermission. 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.
Community resources
- witoszekdev/dummy-payment-server - App for testing Transactions API without a real payment provider
Custom App
What is a Custom App
In this guide, "Custom App" means a Saleor App that uses a custom logic for handling payments instead of listening for PAYMENT_GATEWAY_INITIALIZE_SESSION, TRANSACTION_INITIALIZE_SESSION and TRANSACTION_PROCESS_SESSION webhooks like Payment Apps do. End users need to communicate with the app directly and it's the app's responsibility to report back to Saleor.
Custom apps handle the payment process and call Saleor mutations on their backend to report any changes that happen for a specific transactionItem.
Orders can be created by a protected mutation orderCreateFromCheckout.
Building a Custom App is only recommended if Saleor Payment Apps don't fit your needs and for more complex use cases. For most of the payment providers, Saleor Payment Apps should be used instead.
When the custom app uses the transactionUpdate mutation, Saleor won't be able to automatically update the status and balance of the payment and the app will be responsible for doing it.
Creating transactions
This feature was introduced in Saleor 3.4.
This feature is in the Feature Preview stage, which means that it is available for experimentation and feedback. However, it is still undergoing development and is subject to modifications.
Transaction stores details of a payment transaction attached to an order or a checkout:
The transactionCreate mutation takes the following arguments:
id: The ID of the checkout or order.transaction: Input data required to create a new transaction object.transactionEvent: Data that defines a transaction event. It can be used to provide more context about the current state of the transaction.
The transactionCreate can only be called by staff users or apps with the HANDLE_PAYMENTS permission.
The following example shows how you can use the transactionCreate mutation to create a new transaction.
The transaction was authorized, and the payment was made with a credit card. The actions that can be called from Saleor are: CANCEL and CHARGE.
The authorized amount is $99.
Saleor 3.13+
mutation {
transactionCreate(
id: "Q2hlY2tvdXQ6MWQzNmU5YzctYWEwYS00NzM5LTk0MGQtNzdjNmU4Mjc5YmQ0"
transaction: {
name: "Credit card"
message: "Authorized"
pspReference: "PSP-ref123"
availableActions: [CANCEL, CHARGE]
amountAuthorized: { currency: "USD", amount: 99 }
externalUrl: "https://saleor.io/payment-id/123"
}
) {
transaction {
id
}
}
}
The response:
{
"data": {
"transactionCreate": {
"transaction": {
"id": "VHJhbnNhY3Rpb25JdGVtOjE="
}
}
},
"extensions": {
"cost": {
"requestedQueryCost": 0,
"maximumAvailable": 50000
}
}
}
Saleor 3.4 (deprecated)
mutation {
transactionCreate(
id: "Q2hlY2tvdXQ6MWQzNmU5YzctYWEwYS00NzM5LTk0MGQtNzdjNmU4Mjc5YmQ0"
transaction: {
status: "Authorized"
type: "Credit card"
reference: "PSP-ref123"
availableActions: [CANCEL, CHARGE]
amountAuthorized: { currency: "USD", amount: 99 }
}
transactionEvent: {
status: SUCCESS
name: "Authorized credit card"
reference: "PSP-ref123"
}
) {
transaction {
id
}
}
}
The response:
{
"data": {
"transactionCreate": {
"transaction": {
"id": "VHJhbnNhY3Rpb25JdGVtOjE="
}
}
},
"extensions": {
"cost": {
"requestedQueryCost": 0,
"maximumAvailable": 50000
}
}
}
- Transactions attached to checkout are accessible via the
checkout.transactionsfield. - Transactions attached to order are accessible via the
order.transactionsfield.
Updating transactions
This feature was introduced in Saleor 3.4.
This feature is in the Feature Preview stage, which means that it is available for experimentation and feedback. However, it is still undergoing development and is subject to modifications.
The transactionUpdate mutation allows updating the transaction details.
It takes the following arguments:
id: The ID of the transaction.transaction: Input data that will be used to update the transaction object.transactionEvent: Data that defines a transaction event. It can be used to provide more context about the current state of the transaction.
The transactionUpdate can only be called by staff users with the
HANDLE_PAYMENTS permission
or by the App that created the the transaction and has HANDLE_PAYMENTS permission.
The following example shows how you can use the transactionUpdate mutation to update the transaction.
The available action is REFUND. The authorized funds are charged, so amountAuthorized is $0 and amountCharged is $99.
mutation {
transactionUpdate(
id: "VHJhbnNhY3Rpb25JdGVtOjE="
transaction: {
name: "Credit card"
message: "Authorized"
pspReference: "PSP-ref123"
availableActions: [REFUND]
amountAuthorized: { currency: "USD", amount: 0 }
amountCharged: { currency: "USD", amount: 99 }
}
transactionEvent: {
message: "Payment charged"
pspReference: "PSP-ref123.charge"
}
) {
transaction {
id
}
}
}
The response:
{
"data": {
"transactionUpdate": {
"transaction": {
"id": "VHJhbnNhY3Rpb25JdGVtOjE="
}
}
},
"extensions": {
"cost": {
"requestedQueryCost": 0,
"maximumAvailable": 50000
}
}
}
During the update of transactions, all funds that go to a new state should be subtracted from the previous state.
Assuming we have a transaction with authorizedAmount equal to 100 USD. Moving the authorizedAmount to chargedAmount requires setting the authorizedAmount to 0.
This complexity is handled automatically when Payment Apps are used instead of a custom app.
mutation {
transactionUpdate(
id: "VHJhbnNhY3Rpb25JdGVtOjE="
transaction: {
status: "Charged"
availableActions: [REFUND]
amountAuthorized: { currency: "USD", amount: 0 }
amountCharged: { currency: "USD", amount: 100 }
}
transactionEvent: {
status: SUCCESS
name: "Charged credit card"
reference: "PSP-ref123.charge"
}
) {
transaction {
id
}
}
}
Comparison of Payment Apps and Custom Apps
| Payment Apps | Custom Apps | |
|---|---|---|
| Users communicate with | Saleor GraphQL API | The app directly |
| Consistency | The same set of queries and mutations for each Payment App | Different APIs depending on the custom app implementation |
| Transactions | Created automatically by Saleor based on requests to payment apps and their responses | Need to be created manually by custom apps; depends on the app implementation |
| Balances | Always automatically calculated by Saleor | In some cases the balances need to be manually calculated by the custom app |
| Flexibility | Limited by the set of predefined actions and mutations; designed to handle most of business scenarios | Almost anything is possible |
Additional steps
This section applies to both Payment Apps and Custom Apps.
This section describes additional steps that can be performed at different stages of the payment process.
Reporting actions for transactions
This feature was introduced in Saleor 3.13.
This feature is in the Feature Preview stage, which means that it is available for experimentation and feedback. However, it is still undergoing development and is subject to modifications.
The transactionEventReport is used to
report a new transaction event. The newly created event will be used to recalculate the transaction's amounts.
The mutation should be used for handling action requests or reporting any
changes that happened on the payment provider side (eg. asynchronous webhooks for delayed payment methods, chargebacks, disputes etc.).
It takes the following arguments:
id: The id of the transaction.type: Type of the reported action.amount: The amount of the reported action.pspReference: The reference assigned to the action.time: The time of the action.externalUrl: The URL for the staff user to check the details of the action on the payment provider's page. This URL will be available in the Saleor Dashboard.message: Message related to the action.availableActions: Current list of actions available for the transaction.
The transactionEventReport can only be called by staff users with
HANDLE_PAYMENTS permission
or by the App that created the transaction and has HANDLE_PAYMENTS permission.
The following example shows how the transactionEventReport mutation is used to report an event
that happened for a given transaction.
The report is a success charge action, with 20 as an amount. The currency is the same as declared
for the transaction. Available action that can proceed for a transaction is REFUND.
The provided data will be used to create a new TransactionEvent object that will be included in the
recalculation process.
mutation TransactionEventReport {
transactionEventReport(
id: "VHJhbnNhY3Rpb25JdGVtOjE="
type: CHARGE_SUCCESS
amount: 20
pspReference: "psp-123"
time: "2022-01-01"
externalUrl: "https://saleor.io/event-details/123"
message: "Charge completed"
availableActions: [REFUND]
) {
errors {
field
code
}
alreadyProcessed
transaction {
id
}
transactionEvent {
id
}
}
}
In the response, Saleor returns:
alreadyProcessed- Defines if the reported event hasn't been processed earlier. If there is an event with the samepspReference,amount, andtypeas the ones provided in the input mutation, Saleor will return it instead of creating a new one, and the flag will be set totrue. If the event with providedpspReferenceandtypewas already reported but with a different amount, the error with code INCORRECT_DETAILS will be raised.transaction- Transaction that has been updated based on the received report.transactionEvent- TransactionEvent that has been created based on the received report.
Handling action requests for transactions
This feature was introduced in Saleor 3.4.
This feature is in the Feature Preview stage, which means that it is available for experimentation and feedback. However, it is still undergoing development and is subject to modifications.
An action request is called when a staff user or an app requests an action for a given transaction.
Saleor 3.13+
Two mutations can trigger the action on the app side:
-
transactionRequestAction: will also create a newTransactionEventwith one of the request type (AUTHORIZATION_REQUEST,CHARGE_REQUEST,REFUND_REQUEST,CANCEL_REQUEST),amountand theowner(User or App). Saleor will send a synchronous webhook dedicated to the actionTRANSACTION_CHARGE_REQUESTED,TRANSACTION_CANCELATION_REQUESTED,TRANSACTION_REFUND_REQUESTED -
transactionRequestRefundForGrantedRefund: will create a newTransactionEventwithREFUND_REQUESTtype,amountand theowner(User or App). Saleor will send a synchronous webhookTRANSACTION_REFUND_REQUESTED.OrderGrantedRefundwill be included in the webhook payload (if requested in a subscription query for the webhook). This mutation is useful when the payment provider requires details about lines that are related to refund action.
The response should contain at least pspReference of the action. The pspReference will be placed in the previously created event of …_REQUEST type.
Optionally the response can contain the details of the completed action.
More information about request webhooks can be found in the synchronous webhooks for transactions guide.
The webhook will be sent only to the app that created the transaction.
Asynchronously processing actions
When action is processed asynchronously on the payment provider side, the app should call the transactionActionRequest
mutation once it receives a webhook notification from the payment provider.
The diagram below shows an example of processing asynchronous refund action.
Synchronously processing the action
The app immediately receives the status of the requested action. It can provide the details of the action in response to the received Saleor webhook. The following webhook events
can accept action details in the response: TRANSACTION_CHARGE_REQUESTED,
TRANSACTION_CANCELATION_REQUESTED,
TRANSACTION_REFUND_REQUESTED.
The below diagram shows an example of processing synchronous refund action.
Saleor 3.4 (Deprecated)
If your app should also process payment request actions (like charge, refund, or CANCEL) triggered by staff users on the Saleor dashboard side or by the app,
make sure that your app subscribes to the transaction-action-request webhook.
The chart below shows a workflow for handling a refund request.
Processing refunds for Order
There are two ways to process refunds:
- Manual refund: This method involves directly refunding the payment through the payment app.
- Refund based on
OrderGrantedRefund: This method triggers the refund request based on the details stored inOrderGrantedRefund.
Manual refund
The manual refund can be triggered by calling transactionRequestAction. If the refund is successful, the transaction.chargedAmount will be reduced. The order's authorizeStatus, chargeStatus, and totalBalance will also be recalculated based on the new values of chargedAmount and refundedAmount. This method is useful when handling overcharged orders.
The below example shows how to trigger the manual refund:
mutation {
transactionRequestAction(
id: "VHJhbnNhY3Rpb25JdGVtOjljY2NkYTYyLTllMjktNGE0OC05NzIyLWRlYzAwOTRmZmY5Yg=="
actionType: REFUND
amount: "10"
) {
transaction {
events {
id
message
pspReference
amount {
amount
}
type
}
}
}
}
The mutation accepts below arguments:
id- ID of the transaction which will be used to trigger the refund action.actionType- The type of action to be performed on the requestedTransactionItem. For a refund action, useREFUND.amount- The amount of the action. If not provided Saleor will useTransactionItem.chargedAmount.
Refund based on OrderGrantedRefund
When processing a refund based on OrderGrantedRefund, there are two steps involved. Firstly, an order must be granted a refund, which defines what should be refunded. This step requires the MANAGE_ORDERS permission. Secondly, a refund needs to be requested based on the created OrderGrantedRefund. This step requires the HANDLE_PAYMENTS permission.
A granted refund contains all the details related to the refund, such as the list of refunded lines, the amount, and the included shipping costs. This is useful when handling refunds based on the products returned by the customer.
The current status of the OrderGrantedRefund is represented by the status field. The possible statuses are:
NONE: No refund request has been triggered for the granted refund.PENDING: The refund request has been triggered, but the payment app has not provided the final result.SUCCESS: The refund has been successfully processed.FAILURE: The last refund request failed.
The status is calculated based on the latest refund TransactionEvents assigned to the OrderGrantedRefund. The events can be accessed via the OrderGrantedRefund.transactionEvents field. The assigned TransactionItem can be accessed via the OrderGrantedRefund.transaction field.
The OrderGrantedRefund has an impact on the authorizeStatus, chargeStatus, and totalBalance, as it reduces the total value used to calculate the totalBalance.
For example:
- if an order has a total of 100 USD and a single
TransactionItemwith achargedAmountof 100 USD, thetotalBalancewould be 0, aschargedAmount-order.totalresults in 0(1). - Adding a granted refund with an amount of 10 USD would result in a
totalBalanceof 10, aschargedAmount- (order.total-grantedRefund.amount) gives 10. ThechargeStatuswill beOVERCHARGED(2). - If a refund request is made based on the defined granted refund and is successfully processed by the payment app, it will reduce the
chargedAmount. ThetotalBalancewill be 0, aschargedAmount(90USD) - (order.total-grantedRefund.amount) gives 0. ThechargeStatuswill be changed toFULL(3).
| Step Nr | total | totalBalance | authorizeStatus | chargeStatus | tr.chargedAmount | orderGrantedRefund.amount |
|---|---|---|---|---|---|---|
| 1 | 100 | 0 | FULL | FULL | 100 | 0 |
| 2 | 100 | 10 | FULL | OVERCHARGED | 100 | 10 |
| 3 | 100 | 0 | FULL | FULL | 90 | 10 |
The following example shows how to create the granted refund with the assigned TransactionItem for the order.
mutation {
orderGrantRefundCreate(
id: "T3JkZXI6NWZlOTE5NzItYjg3OC00Y2QyLTkyN2UtZTQwZDJjZDRjMmEz"
input: {
transactionId: "VHJhbnNhY3Rpb25JdGVtOmUzOTVjNzdmLWFmNjQtNDRmZC05NmRiLThkZmNkMDYwNmZmOA=="
lines: [
{
id: "T3JkZXJMaW5lOjdlNzg5NzY0LTUyZWMtNDU3Mi05NWNkLTM5ZjQ0OTJmZDk4ZA=="
quantity: 5
reason: "Line reason"
}
{
id: "T3JkZXJMaW5lOjdlNzg5NzY0LTUyZWMtNDU3Mi05NWNkLTM5ZjQ0OTJmZDk4Z1=="
quantity: 5
}
]
grantRefundForShipping: true
amount: 10
reason: "Returned by customer"
}
) {
grantedRefund {
id
}
order {
id
}
errors {
field
code
message
lines {
lineId
field
message
code
}
}
}
}
The mutation accepts below arguments:
id- ID of theOrderto which granted refund should be assigned.input:lines- List of lines related to the planned refund action:id- ID of theOrderLine.quantity- The quantity of the specific lines planned to refund.reason- Reason of the refund related to the specific line.
grantRefundForShipping- Determines if the shipping costs will be also included in the refund.amount- Amount of the granted refund. If not provided, the amount will be calculated automatically based on providedlinesandgrantRefundForShipping.transactionId- ID ofTransactionItemthat will be used to process the refund. Ifamountis provided in the input, thetransaction.chargedAmountneeds to be equal to or greater than the providedamount. Ifamountis not provided in the input and calculated automatically by Saleor, themin(calculatedAmount, transaction.chargedAmount)will be used. This field was added in Saleor 3.20, and it will be a mandatory input field starting from Saleor 3.21.
The below example shows how to update the existing granted refund
mutation {
orderGrantRefundUpdate(
id: "T3JkZXJHcmFudGVkUmVmdW5kOjE="
input: {
addLines: [
{
id: "T3JkZXJMaW5lOjgzYmZmZWI3LTVkNjEtNDMzZS1iOGFkLTFhMTE1NWI2ZTgwNg=="
quantity: 3
}
]
removeLines: []
transactionId: "VHJhbnNhY3Rpb25JdGVtOmUzOTVjNzdmLWFmNjQtNDRmZC05NmRiLThkZmNkMDYwNmZmOA=="
amount: 10
grantRefundForShipping: false
reason: "New reason"
}
) {
grantedRefund {
id
}
errors {
addLines {
field
message
code
lineId
}
field
code
}
}
}
The mutation accepts below arguments:
id- ID of theOrderGrantedRefundwhich should be updatedinput:addLines: Lines that should be added toOrderGrantedRefundid- ID of theOrderLine.quantity- The quantity of the specific lines planned to refund.reason- Reason for the planned refund related to the specific line.
removeLines- List ofOrderGrantedRefundLine's IDs that should be removed fromOrderGrantedRefund.grantRefundForShipping- Determines if the shipping costs will be also included in the refund.amount- Amount of the granted refund. If not provided, the amount will be calculated automatically based on providedlinesandgrantRefundForShipping.transactionId- ID ofTransactionItemthat will be used to process the refund. Ifamountis provided in the input, thetransaction.chargedAmountneeds to be equal to or greater than the providedamount. Ifamountis not provided in the input and calculated automatically by Saleor, themin(calculatedAmount, transaction.chargedAmount)will be used. This field was added in Saleor 3.20, and it will be a mandatory input field starting from Saleor 3.21.
When OrderGrantedRefund.status is SUCCESS or PENDING, only reason can be updated.
Below example shows how to trigger the refund based on OrderGrantedRefund:
mutation {
transactionRequestRefundForGrantedRefund(
grantedRefundId: "T3JkZXJHcmFudGVkUmVmdW5kOjE="
) {
transaction {
id
}
errors {
field
message
code
}
}
}
To request a refund, you need to provide the grantedRefundId field. The mandatory refund details are stored as OrderGrantedRefund, so the refund request will be created based on this data.
The OrderGrantedRefund.status field will be updated based on the result of the refund action. The TransactionEvents related to the requested action will be accessible via OrderGrantedRefund.transactionEvents field.
Recalculations of transaction amounts
The recalculation will differ based on the value of the TransactionEventTypeEnum provided as the field type
used in the transactionEventReport, transactionInitialize, and transactionProcess mutations, or optionally in the webhook response:
Event types explanation
AUTHORIZATION_SUCCESS
Saleor assigns provided amount to transaction.authorizedAmount. If an AUTHORIZATION_SUCCESS event is related to an AUTHORIZATION_REQUEST event by pspReference, then the transactionItem.authorizePendingAmount
will be reduced by the amount reported in the AUTHORIZATION_SUCCESS event.
You can’t have two successful authorizations for a single transaction.
Each successful request after the first one will raise an exception. AUTHORIZATION_ADJUSTMENT should be used to change the transaction.authorizedAmount.
In the case of finding AUTHORIZATION_SUCCESS and AUTHORIZATION_FAILURE with the same pspReference,
the recalculation for transaction.authorizedAmount will not take into account the amount from
AUTHORIZATION_SUCCESS, when AUTHORIZATION_SUCCESS is the older one.
AUTHORIZATION_FAILURE
If an AUTHORIZATION_FAILURE event is related to an AUTHORIZATION_REQUEST event by pspReference, then the transactionItem.authorizePendingAmount
will be reduced by the amount reported in the AUTHORIZATION_FAILURE event. If there is no related AUTHORIZATION_REQUEST, the failure event will only
be used to provide the history of transactionItem actions.
AUTHORIZATION_ADJUSTMENT
Saleor assigns the provided amount to the transaction.authorizedAmount.
CHARGE_SUCCESS
Adds the provided amount to the transaction.chargedAmount. Each next call will add the amount to the existing
transaction.chargedAmount (transaction.chargedAmount += amount). If a CHARGE_SUCCESS event is related to a CHARGE_REQUEST event by pspReference, then the transactionItem.chargePendingAmount
will be reduced by the amount reported in the CHARGE_SUCCESS event. If there is no related CHARGE_REQUEST event, the provided amount will reduce transaction.authorizedAmount.
In the case of finding CHARGE_SUCCESS and CHARGE_FAILURE with the same pspReference, the recalculation
for transaction.chargedAmount will not take into account the amount from CHARGE_SUCCESS, when
CHARGE_SUCCESS is the older one.
CHARGE_FAILURE
If an CHARGE_FAILURE event is related to an CHARGE_REQUEST event by pspReference, then the transactionItem.chargePendingAmount
will be reduced by the amount reported in the CHARGE_FAILURE event. If there is no related CHARGE_REQUEST, the failure event will only
be used to provide the history of transactionItem actions.
CHARGE_BACK
The provided amount will be used to reduce transaction.chargedAmount
(transaction.chargedAmount -= amount).
REFUND_SUCCESS
Adds provided amount to transaction.refundedAmount. Each next call will add the amount to the existing
transaction.refundedAmount (transaction.refundedAmount += amount). If a REFUND_SUCCESS event is related to a REFUND_REQUEST event by pspReference, then the transactionItem.refundPendingAmount
will be reduced by the amount reported in the REFUND_SUCCESS event. If there is no related REFUND_REQUEST event, the provided amount will reduce transaction.chargedAmount.
In the case of finding REFUND_SUCCESS and REFUND_FAILURE with the same pspReference, the recalculation
for transaction.refundedAmount will not take into account the amount from REFUND_SUCCESS, when
REFUND_SUCCESS is the older one.
REFUND_FAILURE
If an REFUND_FAILURE event is related to an REFUND_REQUEST event by pspReference, then the transactionItem.refundPendingAmount
will be reduced by the amount reported in the REFUND_FAILURE event. If there is no related REFUND_REQUEST, the failure event will only
be used to provide the history of transactionItem actions.
REFUND_REVERSE
The provided amount will be used to reduce transaction.refundedAmount (transaction.refundedAmount -= amount).
The provided amount will increase transaction.chargedAmount.
CANCEL_SUCCESS
The provided amount will be added to transaction.canceledAmount
(transaction.canceledAmount += amount). If a CANCEL_SUCCESS event is related to a CANCEL_REQUEST event by pspReference, then the transactionItem.cancelPendingAmount
will be reduced by the amount reported in the CANCEL_SUCCESS event. If there is no related CANCEL_REQUEST event, the provided amount will reduce transaction.authorizedAmount.
In the case of finding CANCEL_SUCCESS and CANCEL_FAILURE with the same pspReference, the recalculation
for transaction.canceledAmount will not take into account the amount from CANCEL_SUCCESS, when
CANCEL_SUCCESS is the older one.
CANCEL_FAILURE
If an CANCEL_FAILURE event is related to an CANCEL_REQUEST event by pspReference, then the transactionItem.cancelPendingAmount
will be reduced by the amount reported in the CANCEL_FAILURE event. If there is no related CANCEL_REQUEST, the failure event will only
be used to provide the history of transactionItem actions.
Example of recalculations
The tables below show examples of transactions and their amounts when a given event is attached to the transaction. Depending on the event type, the amounts may change. The last columns show the current amounts assigned to the transaction after processing the events defined in a given row and in the rows above it.
| Event | Type | pspReference | Time | Amount | transaction.authorizedAmount | transaction.authorizePendingAmount |
|---|---|---|---|---|---|---|
| 1 | AUTHORIZATION_REQUEST | AB12 | 2022-03-28T12:50:33+00:00 | 10 | 0 | 10 |
| 2 | AUTHORIZATION_SUCCESS | AB12 | 2022-03-28T12:51:33+00:00 | 10 | 10 | 0 |
| 3 | AUTHORIZATION_FAILURE | YZ13 | 2022-03-28T12:52:33+00:00 | 10 | 10 | 0 |
After receiving an AUTHORIZATION_REQUEST event, the amount defined in the event is attached to transaction.authorizePendingAmount.
Then, an AUTHORIZATION_SUCCESS event is attached to the transaction. Since the AUTHORIZATION_SUCCESS event has the same pspReference as
the AUTHORIZATION_REQUEST event, the transaction.authorizePendingAmount is reduced by the event amount, and added to the transaction.authorizedAmount.
An AUTHORIZATION_FAILURE event doesn’t change anything, as there isn’t an AUTHORIZATION_REQUEST event related to it.
| Event | Type | pspReference | Time | Amount | transaction.authorizedAmount | transaction.authorizePendingAmount |
|---|---|---|---|---|---|---|
| 1 | AUTHORIZATION_REQUEST | AB12 | 2022-03-28T12:50:33+00:00 | 10 | 0 | 10 |
| 2 | AUTHORIZATION_SUCCESS | AB12 | 2022-03-28T12:51:33+00:00 | 10 | 10 | 0 |
| 3 | AUTHORIZATION_ADJUSTMENT | YZ13 | 2022-03-28T12:52:33+00:00 | 100 | 100 | 0 |
Upon receiving an AUTHORIZATION_REQUEST event, the corresponding amount is attached to transaction.authorizePendingAmount. An AUTHORIZATION_SUCCESS event
is then attached to the transaction, with the same pspReference as the AUTHORIZATION_REQUEST event. The transaction.authorizePendingAmount is reduced
by the event amount, and the result is added to the transaction.authorizedAmount.
Note that an AUTHORIZATION_ADJUSTMENT event will overwrite the previous transaction.authorizedAmount and cause the
previous AUTHORIZATION_SUCCESS event to be ignored.
| Event | Type | pspReference | Time | Amount | transaction.authorizedAmount | transaction.authorizePendingAmount |
|---|---|---|---|---|---|---|
| 1 | AUTHORIZATION_SUCCESS | AB12 | 2022-03-28T12:51:33+00:00 | 10 | 10 | 0 |
The transaction receives an AUTHORIZATION_SUCCESS event. Since there is no AUTHORIZATION_REQUEST with the same pspReference, the value of transaction.authorizePendingAmount
will not be changed. The amount from the event is assigned to transaction.authorizedAmount.
| Event | Type | pspReference | Time | Amount | transaction.chargedAmount | transaction.chargePendingAmount | transaction.authorizedAmount |
|---|---|---|---|---|---|---|---|
| 1 | AUTHORIZATION_SUCCESS | AB12 | 2022-03-28T12:50:33+00:00 | 10 | 0 | 0 | 10 |
| 2 | CHARGE_REQUEST | YZ13 | 2022-03-28T12:51:33+00:00 | 3 | 0 | 3 | 7 |
| 3 | CHARGE_SUCCESS | YZ13 | 2022-03-28T12:52:33+00:00 | 3 | 3 | 0 | 7 |
When the transaction receives an AUTHORIZATION_SUCCESS event, if there is no AUTHORIZATION_REQUEST with the same pspReference, the value of transaction.authorizePendingAmount
will not change. The amount from the event is then assigned to transaction.authorizedAmount. Additionally, a CHARGE_REQUEST event is added to the transaction, and the amount
from the event is added to transaction.chargePendingAmount. The amount also reduces transaction.authorizedAmount. Finally, when a CHARGE_SUCCESS event occurs, the transaction.chargePendingAmount
is reduced by the event’s amount and added to transaction.chargedAmount.
| Event | Type | pspReference | Time | Amount | transaction.chargedAmount | transaction.chargePendingAmount | transaction.authorizedAmount |
|---|---|---|---|---|---|---|---|
| 1 | AUTHORIZATION_SUCCESS | AB12 | 2022-03-28T12:50:33+00:00 | 10 | 0 | 0 | 10 |
| 2 | CHARGE_REQUEST | YZ13 | 2022-03-28T12:51:33+00:00 | 3 | 0 | 3 | 7 |
| 3 | CHARGE_SUCCESS | YZ13 | 2022-03-28T12:51:33+00:00 | 3 | 3 | 0 | 7 |
| 4 | CHARGE_FAILURE | YZ13 | 2022-03-28T12:55:33+00:00 | 3 | 0 | 0 | 10 |
When an AUTHORIZATION_SUCCESS event is attached to the transaction, the transaction.authorizedAmount is updated with the amount from the event.
If there is no AUTHORIZATION_REQUEST with the same pspReference, the transaction.authorizePendingAmount remains unchanged.
Additionally, a CHARGE_REQUEST event is added to the transaction, and its amount is added to transaction.chargePendingAmount.
The transaction.authorizedAmount is reduced by the amount from the CHARGE_REQUEST event.
Finally, when a CHARGE_SUCCESS event occurs, the transaction.chargedAmount is updated with the event's amount, and the
transaction.chargePendingAmount is reduced by the same amount. A CHARGE_FAILURE event with the same pspReference as CHARGE_SUCCESS is
also attached to the transaction, time indicating that it is newer than the CHARGE_SUCCESS. The previous CHARGE_SUCCESS event is ignored during the recalculation process.
| Event | Type | pspReference | Time | Amount | transaction.chargedAmount | transaction.chargePendingAmount | transaction.authorizedAmount |
|---|---|---|---|---|---|---|---|
| 1 | AUTHORIZATION_SUCCESS | AB12 | 2022-03-28T12:50:33+00:00 | 10 | 0 | 0 | 10 |
| 2 | CHARGE_REQUEST | YZ13 | 2022-03-28T12:51:33+00:00 | 3 | 0 | 3 | 7 |
| 3 | CHARGE_SUCCESS | YZ13 | 2022-03-28T12:51:33+00:00 | 3 | 3 | 0 | 7 |
| 4 | CHARGE_FAILURE | YZ13 | 2022-03-28T12:50:45+00:00 | 3 | 3 | 0 | 7 |
When an AUTHORIZATION_SUCCESS event is attached to a transaction, the transaction.authorizedAmount is updated with the event's amount. If there is no AUTHORIZATION_REQUEST with the
same pspReference, the transaction.authorizePendingAmount remains unchanged. In addition, a CHARGE_REQUEST event is added to the transaction, and its amount is added to
transaction.chargePendingAmount. The transaction.authorizedAmount is reduced by the amount from the CHARGE_REQUEST event. Finally, when a CHARGE_SUCCESS event occurs, the
transaction.chargedAmount is updated with the event's amount, and the transaction.chargePendingAmount is reduced by the same amount. A CHARGE_FAILURE event with
the same pspReference as CHARGE_SUCCESS is also attached to the transaction. The time field indicates that it is older than the CHARGE_SUCCESS event. This means
that the CHARGE_FAILURE event will be ignored, and the CHARGE_SUCCESS event will be included in the calculations.
| Event | Type | pspReference | Time | Amount | transaction.chargedAmount | transaction.chargePendingAmount | transaction.authorizedAmount |
|---|---|---|---|---|---|---|---|
| 1 | CHARGE_SUCCESS | AB12 | 2022-03-28T12:50:33+00:00 | 10 | 10 | 0 | 0 |
| Event | Type | pspReference | Time | Amount | transaction.chargedAmount | transaction.chargePendingAmount | transaction.authorizedAmount |
|---|---|---|---|---|---|---|---|
| 1 | AUTHORIZATION_SUCCESS | AB12 | 2022-03-28T12:50:33+00:00 | 10 | 0 | 0 | 10 |
| 2 | CHARGE_SUCCESS | YZ13 | 2022-03-28T12:51:33+00:00 | 3 | 3 | 0 | 7 |
The AUTHORIZATION_SUCCESS event is attached to the transaction, and its amount is added to transaction.authorizedAmount. Similarly, the CHARGE_SUCCESS event is attached to the transaction,
and its amount is added to transaction.chargedAmount. Finally, the amount of the AUTHORIZATION_SUCCESS event is subtracted from the transaction.authorizedAmount.
Stored payment methods
A stored payment method is a payment method saved by a customer for later use. It allows the user to use it without the need to provide all payment details again.
Saleor uses synchronous webhooks to notify the Payment App about actions related to stored payment methods (such as LIST_STORED_PAYMENT_METHODS) requested by the customer.
Listing user's stored payment methods
This feature was introduced in Saleor 3.15.
This feature is in the Feature Preview stage, which means that it is available for experimentation and feedback. However, it is still undergoing development and is subject to modifications.
This feature is dedicated to third-party apps.
Requesting checkout.storedPaymentMethods or User.storedPaymentMethods field triggers synchronous requests to each app subscribed to LIST_STORED_PAYMENT_METHODS
webhook. In response, Saleor returns a list of payment methods from the subscribed apps. Stored payment methods can be used in further steps of payment processing.
All details related to LIST_STORED_PAYMENT_METHODS
webhook can be found in the Stored Payment Methods guide.
The diagram below shows the workflow of fetching stored payment methods with a Payment App:
Requesting deletion of stored payment method
This feature was introduced in Saleor 3.16.
This feature is in the Feature Preview stage, which means that it is available for experimentation and feedback. However, it is still undergoing development and is subject to modifications.
This feature is dedicated to third-party apps.
The user can request a deletion of their stored payment methods. Saleor doesn't store directly a user's payment methods or transmit payment method details. It is a part of the responsibility of the payment app. The request deletion will be sent to the payment app which owns the specific stored payment method. Saleor will also invalidate the cache of stored-payment-methods. Fetching checkout.storedPaymentMethods or User.storedPaymentMethods will trigger the request to the payment app which was the owner of the payment method.
The following example shows how to use the storedPaymentMethodRequestDelete mutation to request deletion of stored payment method:
mutation {
storedPaymentMethodRequestDelete(
id: "YXBwOmR1bW15LnBheW1lbnQ6cGF5bWVudC1tZXRob2Qx"
channel: "default-channel"
) {
result
message
errors {
field
message
code
}
}
}
As a response, the mutation will return a result field which determines the status of the requested action. In case of a failure result, the errors list will contain details of the failed action.
The mutation will trigger the STORED_PAYMENT_METHOD_DELETE_REQUESTED webhook. For more details about this webhook, please refer to its documentation.
Tokenizing a new payment method
This feature was introduced in Saleor 3.16.
This feature is in the Feature Preview stage, which means that it is available for experimentation and feedback. However, it is still undergoing development and is subject to modifications.
This feature is dedicated to third-party apps.
Tokenizing a payment method can also be done during the checkout process. Please refer to your payment app's documentation for details.
Customers can request to store their payment method for later use. The tokenization process is handled by the payment app subscribed to synchronous webhooks. The process consists of three steps: initialization of the payment gateway (if required), tokenizing the payment method, and handling additional actions (if required). Each step has its dedicated mutation and webhook, which are described below.
The diagram illustrates the general flow of tokenizing a payment method. The process may vary slightly for specific payment providers; for example, some payment providers may not require the initialization of the payment gateway.
Initializing the payment gateway for tokenizing a payment method
The paymentGatewayInitializeTokenization mutation should be used when the payment gateway needs to be initialized first. For example, when retrieving the data required to initialize the drop-in on the storefront side.
This mutation triggers the synchronous webhook PAYMENT_GATEWAY_INITIALIZE_TOKENIZATION_SESSION. The webhook is sent to the app with the provided id.
The id is the ID of the payment app received from checkout.availablePaymentGateways or shop.availablePaymentGateways. The JSON data is passed to the payment app via the synchronous webhook.
mutation PaymentGatewayInitializeTokenization(
$id: String!
$channel: String!
$data: JSON
) {
paymentGatewayInitializeTokenization(
id: $id
channel: $channel
data: $data
) {
result
data
errors {
field
code
message
}
}
}
In the response, Saleor returns:
result: The enum value of PaymentGatewayInitializeTokenizationResult.data: The JSONdatareturned by the payment app.id: The ID of the payment method.
More details about the triggered webhook PAYMENT_GATEWAY_INITIALIZE_TOKENIZATION_SESSION can be found here.
Initializing payment method tokenization
The paymentMethodInitializeTokenization mutation starts the process of tokenizing the payment method. This mutation triggers the synchronous webhook PAYMENT_METHOD_INITIALIZE_TOKENIZATION_SESSION, which is sent to the app with the provided id.
The id is the ID of the payment app received from checkout.availablePaymentGateways or shop.availablePaymentGateways. The JSON data is passed to the payment app via the synchronous webhook. The paymentFlowToSupport is a value from the TokenizedPaymentFlowEnum. It represents the requested payment flow that the payment method should follow.
mutation {
paymentMethodInitializeTokenization(
id: "YXBwOmR1bW15LnBheW1lbnQ="
channel: "default-channel"
data: {"foo": "bar"}
paymentFlowToSupport: INTERACTIVE
) {
result
data
id
errors {
field
code
message
}
}
}
In the response, Saleor returns:
result: The enum value of PaymentMethodTokenizationResult, which determines the status of the tokenization.data: The JSONdatareturned by the payment app.id: The ID of the payment method.
More details about the triggered PAYMENT_METHOD_INITIALIZE_TOKENIZATION_SESSION webhook can be found here.
Processing additional actions required to tokenize the payment method
The paymentMethodProcessTokenization mutation should be called when the payment app returns the result PaymentMethodTokenizationResult.ADDITIONAL_ACTION_REQUIRED. This mutation is dedicated to processing additional actions required to finalize the tokenization of the payment method.
This mutation triggers the synchronous webhook PAYMENT_METHOD_PROCESS_TOKENIZATION_SESSION.
The id is the ID received as a response from paymentMethodInitializeTokenization or previous paymentMethodProcessTokenization calls. The JSON data is passed to the payment app via the synchronous webhook.
mutation PaymentMethodProcessTokenization(
$id: String!
$channel: String!
$data: JSON
) {
paymentMethodProcessTokenization(id: $id, channel: $channel, data: $data) {
result
data
id
errors {
field
code
message
}
}
}
In the response, Saleor returns:
result: The enum value of PaymentMethodTokenizationResult, which determines the status of the tokenization.data: The JSONdatareturned by the payment app.id: The ID of the payment method.
More details about the triggered PAYMENT_METHOD_PROCESS_TOKENIZATION_SESSION webhook can be found here.
Payment Plugin (legacy)
Using PaymentPlugin is explained in Checkout section.