Skip to main content

Refunds

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 in OrderGrantedRefund.
note

You may have seen we start referencing Page as a Model. While in the API we still use Page name for backwards compatibility, we will be renaming it to Model in the future.

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: ID, $actionType: TransactionActionEnum!, $amount: PositiveDecimal) {
transactionRequestAction(id: $id, actionType: $actionType, amount: $amount) {
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 requested TransactionItem. For a refund action, use REFUND.
  • amount - The amount of the action. The amount is rounded based on the given currency precision. If not provided Saleor will use TransactionItem.chargedAmount.
  • refundReason - Optional plain text reason for the refund. Will be populated to the TransactionEvent.message field.
  • refundReasonReference - The ID of a Page representing the reason for the refund. This is useful for tracking and reporting purposes, like aggregating refunds by specific type. See configuration to learn more.

Granted refunds​

A granted refund refers to the process where a customer receives their money back after a refund request has been approved. Once a refund is granted, the customer is reimbursed for the purchase.

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.

note

The maximum amount of OrderGrantedRefund is the order.total.

For example:

  • if an order has a total of 100 USD and a single TransactionItem with a chargedAmount of 100 USD, the totalBalance would be 0, as chargedAmount - order.total results in 0(1).
  • Adding a granted refund with an amount of 10 USD would result in a totalBalance of 10, as chargedAmount - (order.total - grantedRefund.amount) gives 10. The chargeStatus will be OVERCHARGED(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. The totalBalance will be 0, as chargedAmount (90USD) - (order.total - grantedRefund.amount) gives 0. The chargeStatus will be changed to FULL(3).
Step NrtotaltotalBalanceauthorizeStatuschargeStatustr.chargedAmountorderGrantedRefund.amount
11000FULLFULL1000
210010FULLOVERCHARGED10010
31000FULLFULL9010

The following example shows how to create the granted refund with the assigned TransactionItem for the order.

mutation OrderGrantRefundCreate($id: ID!, $input: OrderGrantRefundCreateInput!) {
orderGrantRefundCreate(id: $id, input: $input) {
grantedRefund {
id
reason
reasonReference {
id
title
}
lines {
id
reason
reasonReference {
id
title
}
orderLine {
id
}
quantity
}
}
order {
id
}
errors {
field
code
message
lines {
lineId
field
message
code
}
}
}
}

The mutation accepts below arguments:

  • id - ID of the Order to which granted refund should be assigned.
  • input:
    • lines - List of lines related to the planned refund action:
      • id - ID of the OrderLine.
      • quantity - The quantity of the specific lines planned to refund.
      • reason - Reason of the refund related to the specific line.
      • reasonReference - Optional. ID of the Page representing the reason for the refund for this specific line. When provided, the Page must match the PageType set in refund settings. See configuration to learn more. Added in Saleor 3.23.
    • 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 provided lines and grantRefundForShipping.
    • reason - Order-level reason text for the granted refund.
    • reasonReference - Order-level Page ID representing the reason for the refund. Required for staff when refundReasonReferenceType is configured. See configuration to learn more.
    • transactionId - ID of TransactionItem that will be used to process the refund. If amount is provided in the input, the transaction.chargedAmount needs to be equal to or greater than the provided amount. If amount is not provided in the input and calculated automatically by Saleor, the min(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 order-level reasonReference is required for staff users when the refundReasonReferenceType is configured, and always optional for apps. Per-line reasonReference is always optional for both staff and apps — lines without it are stored with null. There is no inheritance from order-level to line-level. When a per-line reasonReference is provided, it must match the configured PageType.

The below example shows how to update the existing granted refund

mutation OrderGrantRefundUpdate($id: ID!, $input: OrderGrantRefundUpdateInput!) {
orderGrantRefundUpdate(id: $id, input: $input) {
grantedRefund {
id
reason
reasonReference {
id
title
}
lines {
id
reason
reasonReference {
id
title
}
orderLine {
id
}
quantity
}
}
errors {
addLines {
field
message
code
lineId
}
field
code
}
}
}

The mutation accepts below arguments:

  • id - ID of the OrderGrantedRefund which should be updated
  • input:
    • addLines - Lines that should be added to OrderGrantedRefund:
      • id - ID of the OrderLine.
      • quantity - The quantity of the specific lines planned to refund.
      • reason - Reason for the planned refund related to the specific line.
      • reasonReference - Optional. ID of the Page representing the reason for the refund for this specific line. When provided, must match the configured PageType. See configuration to learn more. Added in Saleor 3.23.
    • removeLines - List of OrderGrantedRefundLine's IDs that should be removed from OrderGrantedRefund.
    • 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 provided lines and grantRefundForShipping.
    • reason - Order-level reason text for the granted refund.
    • reasonReference - Order-level Page ID representing the reason for the refund. Required for staff when refundReasonReferenceType is configured. See configuration to learn more.
    • transactionId - ID of TransactionItem that will be used to process the refund. If amount is provided in the input, the transaction.chargedAmount needs to be equal to or greater than the provided amount. If amount is not provided in the input and calculated automatically by Saleor, the min(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.
info

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: ID!) {
transactionRequestRefundForGrantedRefund(grantedRefundId: $grantedRefundId) {
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.

Total remaining grant calculations​

The amount specified in the OrderGrantedRefund determines the portion of the order.total that should be refunded to the customer. The order.totalRemainingGrant specifies the amount that still needs to be refunded to achieve a state where the total charged amount equals the order.total minus the granted refund amount.

The order.totalRemainingGrant amount is calculated based on the following formula:

totalRemainingGrant = totalGrantedRefund - alreadyGrantedRefund

where

  • alreadyGrantedRefund is the difference between already refunded and overcharged amounts

    alreadyGrantedRefund = max((totalRefunded - overchargedAmount), 0)
  • totalGrantedRefund is a sum of all amount from all OrderGrantedRefund. The maximum value is order.total, when the value is above, it's replaced with the order.total.

    totalGrantedRefund = max(sum(orderTotalGranted.amount), order.total)
  • totalRefunded is a sum of amount from all transactions with refunded or pending refunds amount

    totalRefunded = sum(amountRefunded + amountRefundPending)
  • overchargedAmount defines the amount that has been charged over the order.total value. It's calculated as a sum of all processed amounts (excluding cancel amounts) minus the order.total.

    processedAmount = sum(amountCharged + amountRefunded + amountAuthorized + amountChargePending + amountRefundPending + amountAuthorizePending)

    overchargedAmount = processedAmount - order.total

Below is an example order with multiple transactions and totalRemainingGrant at each stage:

  1. An order has a total of 100 USD and two TransactionItems: one with a chargedAmount of 100 USD and the second with chargedAmount of 60 USD. The totalBalance would be 60, as chargedAmount - order.total results in 60, and we do not have any granted refunds. The chargeStatus is OVERCHARGED.

  2. Adding a granted refund of 10 USD increases the totalBalance to 70, as it widens the difference between the total charged amount and the new expected order total. Here's the calculation:

    totalBalance = totalCharged - (order.total - totalGrantedRefunds) = 160 - (100 - 10) = 70

    The totalRemainingGrant becomes 10, as the totalRefundedAmount is 0.

  3. The second transaction is refunded for an amount of 50 USD. After this operation, the totalBalance is 20, and the order remains OVERCHARGED. The totalChargedAmount decreases, and the totalRefundedAmount increases. The totalRemainingGrant remains 10 because the overchargedAmount still exceeds the original order.total.

    overchargedAmount = totalChargedAmount + totalRefundedAmount - order.total = 110 + 50 - 100 = 60

    totalRefunded = max(totalGrantedRefund - (totalRefunded - overchargedAmount), 0) = max(10 - max(50 - 60, 0)) = 10 - 0 = 10
  4. In the next step the first transaction was refunded for 15 USD. This brings the total below the order.total value, reducing the totalRemainingGrant.

  5. Finally, the last refund of 5 USD is processed, resulting in a totalBalance of 0. The chargeStatus is FULL, and the totalRemainingGrant is 0.

Step NrtotaltotalBalanceauthorizeStatuschargeStatustotalChargedAmounttotalRefundedAmountorderGrantedRefund.amounttotalRemainingGrant.amount
110060FULLOVERCHARGED160000
210070FULLOVERCHARGED16001010
310020FULLOVERCHARGED110501010
41005FULLOVERCHARGED9565105
51000FULLFULL9070100

Note:

  • totalChargedAmount - is the sum of charged amounts from all transactions
  • totalRefundedAmount - is the sum of all refunded or pending refunds amount from all transactions
info

In case of calculating total remaining grant:

  • Pending amounts are treated as they were processed, so are included in all processed amounts. It also means that totalChargedAmount includes amountChargePending, and totalRefundedAmount includes amountRefundPending.
  • Authorized amounts, including amountAuthorized and amountAuthorizePending, are also considered processed. These amounts will contribute to the overall processed totals and impact the overchargedAmount.

Return and replace reasons​

note

Available from 3.22

The orderFulfillmentReturnProducts mutation supports attaching reasons to returns and replacements. Reasons can be provided at two levels:

  • Global - a single reason (free text) and reasonReference (Page ID) that applies to the entire return request. These are stored on the resulting Fulfillment object.
  • Per fulfillment line - a reason and reasonReference on each OrderReturnFulfillmentLineInput. These are stored on the individual FulfillmentLine objects, allowing different reasons for different items in the same return.

Input schema​

input OrderReturnProductsInput {
orderLines: [OrderReturnLineInput!]
fulfillmentLines: [OrderReturnFulfillmentLineInput!]
amountToRefund: PositiveDecimal
includeShippingCosts: Boolean
refund: Boolean
reason: String
reasonReference: ID
}

input OrderReturnFulfillmentLineInput {
fulfillmentLineId: ID!
quantity: Int!
replace: Boolean
reason: String
reasonReference: ID
}

Output types​

The reason data is persisted and available on the resulting fulfillment objects:

type Fulfillment {
# ... existing fields ...
reason: String
reasonReference: Page
}

type FulfillmentLine {
# ... existing fields ...
reason: String
reasonReference: Page
}

Example: Return with reasons​

mutation OrderFulfillmentReturnProducts($orderId: ID!, $input: OrderReturnProductsInput!) {
orderFulfillmentReturnProducts(order: $orderId, input: $input) {
returnFulfillment {
id
status
reason
reasonReference {
id
title
}
lines {
id
quantity
reason
reasonReference {
id
title
}
}
}
replaceFulfillment {
id
status
reason
reasonReference {
id
title
}
lines {
id
quantity
reason
reasonReference {
id
title
}
}
}
replaceOrder {
id
}
order {
status
}
errors {
field
code
message
}
}
}

The mutation accepts the following reason-related arguments:

  • input:
    • reason - Root free-text reason for the return. Stored on the resulting Fulfillment.
    • reasonReference - Global Page ID representing the reason for the return. Must match the PageType set in the returnReasonReferenceType return setting. Stored on the resulting Fulfillment. See return reasons configuration to learn more.
    • fulfillmentLines[].reason - Per-line free-text reason for the return.
    • fulfillmentLines[].reasonReference - Per-line Page ID. Overrides the global reasonReference for this specific line. Stored on the resulting FulfillmentLine.

Validation rules​

  • When returnReasonReferenceType is configured and the caller is a staff user, the global reasonReference is required.
  • Per-line reasonReference is always optional — for both staff and apps. Lines without it are stored with null. There is no inheritance from the root level.
  • Apps can omit reasonReference at any level, even when configuration requires it.
  • When reasonReference is provided (root or per-line), the referenced Page must belong to the PageType set in the returnReasonReferenceType setting. Otherwise, a ValidationError is raised.
  • The refund flag does not affect reason validation. Whether refund is true or false, the same validation rules apply. The return reason (why items are being sent back) is independent of the refund reason (tracked on OrderGrantedRefund).
note

Returns are gated by a dedicated setting, returnReasonReferenceType, which is independent of the refundReasonReferenceType used by the refund mutations. Configuring one does not configure the other. See return reasons configuration.

Refund reasons configuration​

note

Available from 3.22. Per-line reason references for grant refund mutations added in 3.23. Return/replace reasons use a separate setting — see return reasons configuration.

Staff users with MANAGE_SETTINGS permission can access additional refunds configuration available in Dashboard and via API.

Configuration is global and reflected on every channel.

Currently, RefundSettings type controls a single behavior: whether the reasonReference is required when the refund is created or updated.

Setting Refund Reason Type Requirement​

You can decide you want a strict policy to require a reason reference for every refund. Such reference will be a relation to a Model type. To enable that, you need first to create a Model Type dedicated for such refunds. You can do that in the Dashboard or by pageTypeCreate mutation.

Once you have a PageType ID, you can set it in the refundSettingsUpdate mutation.

mutation SetRefundTypeRequired($pageTypeId:ID!){
refundSettingsUpdate(input:{
refundReasonReferenceType:$pageTypeId
}){
errors{
message
}
}
}

Now, every time a staff user tries to create or update a refund (transactionRequestAction, orderGrantRefundCreate, orderGrantRefundUpdate), they will be required to provide a reason reference. The existing free-text "reason" field will still be available, but it will be optional.

Returns (orderFulfillmentReturnProducts) are governed by a separate returnReasonReferenceType setting, documented in return reasons configuration.

Note that permitted App will be able to omit this setting. For the staff user, the ValidationError will be raised if the reason reference is not provided. Apps can omit this field for more powerful workflows.

Apart from that, the reasonReference provided in mutations must be a valid Page ID of the type set in the refundReasonReferenceType setting. If not, a ValidationError will be raised as well.

Supported mutations​

These mutations require reasonReference (for staff) when the refund reason type (refundReasonReferenceType) is configured:

MutationOrder-levelPer-lineSince
transactionRequestActionrefundReasonReference-3.22
orderGrantRefundCreatereasonReferencelines[].reasonReference3.22 (order-level), 3.23 (per-line)
orderGrantRefundUpdatereasonReferenceaddLines[].reasonReference3.22 (order-level), 3.23 (per-line)

This mutation requires reasonReference (for staff) when the return reason type (returnReasonReferenceType) is configured:

MutationOrder-levelPer-lineSince
orderFulfillmentReturnProductsreasonReferencefulfillmentLines[].reasonReference3.23

Disabling refund reason type requirement​

To opt-out from this strict policy, staff user can disable it in the Dashboard or by running the refundReasonReferenceClear mutation:

mutation DisableRefundTypeRequired {
refundReasonReferenceClear {
errors {
message
}
}
}

Return reasons configuration​

note

Available from 3.23

Returns use a configuration that is separate from refunds. The ReturnSettings type controls whether a reasonReference is required when products are returned via orderFulfillmentReturnProducts. Like refund settings, it is global and reflected on every channel, and requires the MANAGE_SETTINGS permission.

You can read the current setting with the returnSettings query:

query {
returnSettings {
reasonReferenceType {
id
name
}
}
}

Setting Return Reason Type Requirement​

As with refunds, you first create a dedicated Model (Page) type (in the Dashboard or via pageTypeCreate), then set its ID with the returnSettingsUpdate mutation:

mutation SetReturnTypeRequired($pageTypeId: ID!) {
returnSettingsUpdate(input: {
returnReasonReferenceType: $pageTypeId
}) {
returnSettings {
reasonReferenceType { id }
}
errors {
field
message
code
}
}
}

Once configured, a staff user calling orderFulfillmentReturnProducts must provide a global reasonReference whose Page belongs to the configured type; otherwise a ValidationError is raised. Per-line reasonReference remains optional. Apps may always omit it. This is independent of the refund flag.

Disabling return reason type requirement​

To opt out, run the returnReasonReferenceClear mutation (or use the Dashboard):

mutation DisableReturnTypeRequired {
returnReasonReferenceClear {
errors {
message
}
}
}