Processing a refund

To process a refund, Shopify makes an HTTP call to your app, and your app completes the refund by replying with a GraphQL mutation. This interaction is illustrated in the following diagram:

payment processing steps

  1. Merchant requests refund.
  2. Shopify sends a backend request to the payments app, specifying the refund.
  3. The app responds with 201 OK.
  4. The app finalizes the refund using either RefundSessionResolve or RefundSessionReject mutations.
  5. Shopify updates the refund status.

Requirements

Scopes

To use the GraphQL mutations, your app must be aware of access scopes for payments apps.

Initiate the refund flow

The refund flow begins with an HTTP request sent from Shopify to the provider's refund session URL as provided during app extension configuration:

Request Body

{
  "id": "2sl4WR9jF82W0vQVg8fjux9S",
  "gid": "gid://shopify\/RefundSession/2sl4WR9jF82W0vQVg8fjux9S",
  "payment_id": "e6dXWOq7-_NSjXFeCjQ9jsGZ"
  "amount": "123.00",
  "currency": "CAD",
  "merchant_locale": "en",
  "proposed_at": "2020-07-13T00:00:00Z",
}

Request headers

Shopify-Shop-Domain: my-test-shop.myshopify.com
Shopify-Request-Id: 94169f7e-ac8d-4ef4-9fd2-90f0791daddf

Request attributes

Attribute Description Type
idRequired Identifies the refund when communicating with Shopify (in GraphQL mutations, for example). Unique identifier for the refund attempt. Used as the idempotency key. It can be assumed that requests with a given ID are identical to any previously-received requests with the same ID. string
gidRequired Identifies the refund when communicating with Shopify (in GraphQL mutations, for example). string
payment_idRequired ID of the payment that is to be refunded. string
amountRequired Amount to be refunded. string
currencyRequired Three-letter ISO currency code. String (ISO-8601)
merchant_localeRequired IETF BCP 47 language tag representing the language used by the merchant. Boolean
proposed_atRequired Timestamp representing when the refund request was proposed. String (ISO-8601)

Response

Shopify must receive a HTTP 201 response for the refund session creation to be successful.

If the request fails, then it is retried several times. If the request still fails, then the merchant needs to manually retry the refund in the Shopify admin.

Resolve a Refund

After you've successfully processed the refund request, you can resolve it by using the RefundSessionResolve mutation:

mutation RefundSessionResolve($id: ID!) {
  refundSessionResolve(id: $id) {
    refundSession {
      id
      status {
        code
      }
    }
    userErrors {
      field
      message
    }
  }
}

The id argument corresponds to the gid of the refund.

Variables:

{
  "id": "gid://shopify/RefundSession/PJUG6iB0xU7czy1ZN3xEwn4o"
}

View response

JSON response:

{
  "data": {
    "refundSessionResolve": {
      "refundSession": {
        "id": "gid://shopify/RefundSession/vZnLIUzgV08M2AWqJnAv_Uc0",
        "status": {
          "code": "RESOLVED"
        }
      },
      "userErrors": []
    }
  }
}

Reject a refund

If you cannot process a refund, then you should reject it. You should only reject a refund in the case of final and irrecoverable errors. Otherwise, you can re-attempt to process the refund.

You can reject a mutation using the RefundSessionReject mutation:

mutation RefundSessionReject($id: ID!, $reason: RefundSessionRejectionReasonInput!) {
  refundSessionReject(id: $id, reason: $reason) {
    refundSession {
      id
      status {
        code
        reason {
          ... on RefundSessionStatusReason {
            code
            merchantMessage
          }
        }
      }
    }
    userErrors {
      field
      message
    }
  }
}

Variables:

{
  "id": "gid://shopify/RefundSession/PJUG6iB0xU7czy1ZN3xEwn4o",
  "reason": {
    "code": "PROCESSING_ERROR",
    "merchantMessage": "too much sun, time for a break"
  }
}

As part of the rejection, you need to include a reason why the refund was rejected as part of RefundSessionRejectionReasonInput.

The RefundSessionRejectionReasonInput.code is a RefundSessionStatusReasonRejectionCode, which is an enum of standardized error codes.

The RefundSessionRejectionReasonInput.merchantMessage argument is a localized error message presented to the merchant explaining why the refund was rejected.

View response

JSON response:

{
  "data": {
    "refundSessionReject": {
      "refundSession": {
        "id": "gid://shopify/RefundSession/8rK86HoqXnLiW-8shNCWZhTH",
        "status": {
          "code": "REJECTED",
          "reason": {
            "code": "PROCESSING_ERROR",
            "merchantMessage": "too much sun, time for a break"
          }
        }
      },
      "userErrors": []
    }
  }
}

Retry policy

If there is a Shopify service disruption (or if 500s are being returned), then you must implement a retry policy. For refunds, it is suggested to implement an exponential backoff strategy with a maximum backoff of 64 seconds.

After the final request fails, you need to display an error to tell the customer that there was a problem processing the order.

Additional information