---
title: Build an alternative payments extension
description: >-
  Learn how to build a Shopify alternative payments extension using Polaris and
  Prisma.
source_url:
  html: >-
    https://shopify.dev/docs/apps/build/payments/alternative/build-an-alternative-payment-extension?framework=remix
  md: >-
    https://shopify.dev/docs/apps/build/payments/alternative/build-an-alternative-payment-extension.md?framework=remix
api_name: payments-apps
---

# Build an alternative payments extension

**Beta:**

Processing a payment with an alternative payments extension is currently in an invite-only closed beta.

**Tip:**

Ensure you have the [latest version of Shopify CLI](https://shopify.dev/docs/api/shopify-cli#upgrade) installed to access all payment extension features.

Alternative payments extensions allow Partners to incorporate additional fields and gather all necessary information upfront, directly on the checkout page, before initiating a payment.

This provides your payments extension with the ability to finalize a customer's payment without having to redirect them to an offsite page, yet if required, can still redirect them similar to the offsite payments extension.

Alternative payments extensions also enable Partners to prompt buyers to complete an additional verification step, if necessary, after they click Pay Now. This is an improvement to the existing flow where buyers are redirected to an offsite page to complete verification challenges.

## What you'll learn

In this tutorial, you'll learn how to do the following tasks:

* Set up your app.
* Create a checkout UI extension.
* Create an alternative payments extension.
* Explore the payment, refund, void, reject, and capture session flows, as well as how to implement them yourself.
* Implement buyer verification challenges using payment session modals.

## Requirements

[Create a Partner account](https://www.shopify.com/partners)

[Create a dev store](https://shopify.dev/docs/apps/build/stores/development-stores)

The dev store should be pre-populated with test data.

[Become a Payments Partner](https://shopify.dev/docs/apps/build/payments/payments-extension-review#payments-partner-application-review)

Apply and receive approval to become a Payments Partner.

## Project

[View on GitHub](https://github.com/Shopify/example-app--payments-app-template--remix/blob/main-js)

## Scaffold an app

Create a new payments app using [Shopify CLI](https://shopify.dev/docs/api/shopify-cli).

### Scaffold an app using Shopify CLI

1. Run the following command to start creating your app:

   ## Terminal

   ```bash
   npm init @shopify/app@latest
   ```

2. When prompted, enter the name of your app.

3. When prompted for the approach, select the option to add your first extension.

   ## Terminal

   ```bash
   Build a Remix app (recommended)
   > Build an extension-only app
   ```

## Create a checkout UI extension

Once the skeleton app has been created, create and publish checkout UI extension for your app.

1. Follow these [steps](https://shopify.dev/docs/api/checkout-ui-extensions) to create your checkout UI extension for your app.
2. After [generating your extension](https://shopify.dev/docs/api/shopify-cli/app/app-generate-extension), the only difference is that the [target](https://shopify.dev/docs/api/checkout-ui-extensions/latest/targets) you will bind to, and the hook you will call to update attributes from your extension, are not listed in the public documentation. A [sample checkout UI extension](https://shopify.dev/docs/apps/build/payments/alternative/build-an-alternative-payment-extension#sample-checkout-ui-extension) shown below is done in Typescript/Preact. This framework is recommended when generating the extension.

### Sample checkout UI Extension

**Note:**

Update to the latest version of checkout UI extensions so you can use [web components](https://shopify.dev/docs/api/polaris). The legacy `checkout-ui-extensions-react` package is no longer required.

The following example demonstrates a checkout UI extension for a Custom Payment Method payments app. It uses hooks, such as `useApplyPaymentMethodAttributesChange` from the Preact-specific package.

## extensions/\<extension-name>/src/Checkout.jsx

```ts
import '@shopify/ui-extensions/preact';
import { render } from "preact";
import { useApplyPaymentMethodAttributesChange, useExtension } from '@shopify/ui-extensions/checkout/preact';
import { useEffect, useState } from 'preact/hooks';


const LOG_PREFIX = '[checkout UI Extension]';


export default async () => {
  render(<Extension />, document.body)
};
function Extension() {
  const [bankName, setBankName] = useState('');
  const [bankNumber, setBankNumber] = useState('');
  const applyPaymentMethodAttributesChange =
    useApplyPaymentMethodAttributesChange();
  const { rendered } = useExtension();


  /** @type {% raw %}{{type: 'updatePaymentMethodAttributes', attributes: Array<{key: string, value: string}>}}{% endraw %} */
  const change = {
    type: 'updatePaymentMethodAttributes',
    attributes: [
      {key: 'bank_name', value: bankName},
      {key: 'bank_number', value: bankNumber},
    ],
  };
  useEffect(() => {
    if (!rendered.value) {
      return;
    }


    applyPaymentMethodAttributesChange(change)
      .then((result) => {
        console.log(`${LOG_PREFIX} Applied change`, change, result);
      })
      .catch((error) => {
        console.error(`${LOG_PREFIX} Failed to apply`, change, error);
      });
  }, [change, rendered.value]);
  return (
    <s-form onSubmit={() => applyPaymentMethodAttributesChange(change)}>
      <s-grid gridTemplateColumns="50% 50%" gap="base">
        <s-box>
          <s-text-field
            label="Bank Name"
            value={bankName}
            required
            onChange={(event) => setBankName(/** @type {any} */ (event.currentTarget).value)}
          />
        </s-box>
        <s-box>
          <s-number-field
            label="Bank Number"
            value={bankNumber}
            required
            onChange={(event) => setBankNumber(/** @type {any} */ (event.currentTarget).value)}
          />
        </s-box>
      </s-grid>
    </s-form>
  );
}
```

**Note:**

You must specify the target you're rendering to (`purchase.checkout.payment-option-item.details.render`) in the `shopify.extension.toml` file in your extension directory.

### Deploy your checkout UI extension

Create and release an app version with the [`deploy`](https://shopify.dev/docs/api/shopify-cli/app/app-deploy) command.

1. Navigate to your app directory.

2. Run the following command. You can optionally provide a name or message for the version using the `--version` and `--message` flags.

## Terminal

```terminal
shopify app deploy
```

***

An app version created using Shopify CLI contains the following:

* The app configuration from the local [configuration file](https://shopify.dev/docs/apps/build/cli-for-apps/app-configuration).
* The local version of the app's extensions.

**Caution:**

If you have an extension in your deployed app, but the extension code doesn't exist locally, then the extension isn't included in your app version.

Releasing an app version replaces the current active version that's served to stores with your app installed. It might take several minutes for app users to be upgraded to the new version.

**Note:**

If you want to create a version, but want to avoid releasing it to users, then run the `deploy` command with a `--no-release` flag.

You can release the unreleased app version using Shopify CLI's [`release`](https://shopify.dev/docs/api/shopify-cli/app/app-release) command, or through the Dev Dashboard.

## Create a payments extension

Your Shopify app becomes a payments app after you've created and configured your payments extension.

1. Run the following command to start generating your payment extension:

   ## Terminal

   ```terminal
   shopify app generate extension
   ```

2. When prompted for "Type of extension", select "Payments App Extension > Custom Onsite" and name your extension.

## Configure your payments extension

When you [generate an app extension](https://shopify.dev/docs/api/shopify-cli/app/app-generate-extension), a TOML configuration file named `shopify.extension.toml` is automatically generated in your app's extension directory. You can find your extension configuration in `extensions/<extension-name>/shopify.extensions.toml`.

| Property name | Description |
| - | - |
| `payment_session_url` required | The URL that receives payment and order details from the checkout. |
| `refund_session_url` required | The URL that refund session requests are sent to. |
| `capture_session_url` optional | The URL that capture session requests are sent to. This is only used if your payments app supports merchant manual capture. |
| `void_session_url` optional | The URL that void session requests are sent to. This is only used if your payments app supports merchant manual capture or void payments. |
| `confirmation_callback_url` optional | The URL that confirm session requests are sent to. This URL is required if your payments app supports inventory confirmation. If set, then the payments app can use the [`paymentSessionConfirm`](https://shopify.dev/docs/api/payments-apps/latest/mutations/paymentSessionConfirm) mutation to confirm with Shopify whether to proceed with the payment request. Refer to [Explore confirm sessions](#explore-confirm-sessions) section to learn more. |
| `supported_countries` required | The countries where your payments app is available. Refer to the [list of ISO 3166 (alpha-2) country codes](https://www.iso.org/iso-3166-country-codes.html) where your app is available for installation by merchants. Ensure the countries match the geographic requirements in your app listing when [submitting your app for review](https://shopify.dev/docs/apps/launch/app-store-review/submit-app-for-review#create-a-listing). |
| `supports_3ds` required | 3-D Secure support is mandated in some instances. For example, you must enable the 3-D Secure field if you plan to support payments in countries which have mandated 3-D Secure. |
| `supported_payment_methods` required | The payment methods (for example, Visa) that are available with your payments app. [Learn more](https://github.com/activemerchant/payment_icons/blob/master/db/payment_icons.yml). |
| `supports_installments` required | Enables installments |
| `supports_deferred_payments` required | Enables deferred payments |
| `merchant_label` optional | The name for your payment provider extension. This name is displayed to merchants in the Shopify admin when they search for payment methods to add to their store. Limited to 50 characters. |
| `buyer_label` optional | The name of the method. Your checkout name can be the same as your merchant admin name or it can be customized for customers. This name is displayed with the payment methods that you support in the customer checkout. After a checkout name has been set, translations should be provided for localization. |
| `test_mode_available` optional | Enables merchants using your payments app to test their setup by simulating transactions. To test your app on a dev store, your payment provider in the Shopify admin must be set to test mode. |
| `api_version` optional | The Payments Apps GraphQL API version used by the payment provider app to receive requests from Shopify. You must use the same API version for sending GraphQL requests. You must not use unstable in production. API versions are updated in accordance with Shopify's general [API versioning timelines](https://shopify.dev/docs/api/usage/versioning). |
| `multiple_capture` optional | Enables merchants using your payment provider app to partially capture an authorized payment multiple times up to the full authorization amount. This is used only if your payments app supports merchant manual capture. |

The properties are identical to what exists for the [Offsite payments app extension](https://shopify.dev/docs/apps/build/payments/offsite/use-the-cli); however, there are two additional configuration items that are worth highlighting below.

### UI extension

This is where you link the [checkout extension](#create-a-checkout-ui-extension) you previously built with your payment extension.

| Property name | Description |
| - | - |
| `ui_extension_handle` required | The UI extension that renders your payments app in checkout. This value can only be a UI extension linked to this specific payments app. |

### UI Extension Field Definitions

Specify the fields in your UI extensions to ensure the payment method validates the correct data sent from the front end.

| Property name | Description |
| - | - |
| `checkout_payment_method_fields` required | The fields your payments app will accept from buyers in checkout (for example, installment details, payment plan). Each field is composed of a key name, and a data type. The data type defines the input that a buyer can provide. |

## Example UI extension field definitions

```text
[[extensions.checkout_payment_method_fields]]
key = "bank_name"
type = "string"
required = true


[[extensions.checkout_payment_method_fields]]
key = "bank_number"
type = "string"
required = false
```

The example field definitions configure a checkout form that appears as follows:

![A custom payments app on checkout with the Bogus Onsite Payments App selected, showing bank name and bank number input fields.](https://shopify.dev/assets/assets/apps/payments/custom-payment-method-payments-app-example-CC8dFxID.png)

## Set up your payments app

### Update your app configuration

Shopify apps are embedded by default, but payments apps are an exception to this, because they don't need to render anything in Shopify admin. In `shopify.app.toml`, update the `embedded` and set it to false.

The `write_payment_gateways` and `write_payment_sessions` [access scopes](https://shopify.dev/docs/api/usage/access-scopes#authenticated-access-scopes) are automatically granted through the payment extension. Omit them from your `shopify.app.toml` file on initial deployment. In later deployments, you can add these scopes to request merchant permission for your payment extension.

### Deploy your payments extension

Create and release an app version with the [`deploy`](https://shopify.dev/docs/api/shopify-cli/app/app-deploy) command.

1. Navigate to your app directory.
2. Run the following command. You can optionally provide a name or message for the version using the `--version` and `--message` flags.

## Terminal

```terminal
shopify app deploy
```

***

An app version created using Shopify CLI contains the following:

* The app configuration from the local [configuration file](https://shopify.dev/docs/apps/build/cli-for-apps/app-configuration).
* The local version of the app's extensions.

**Caution:**

If you have an extension in your deployed app, but the extension code doesn't exist locally, then the extension isn't included in your app version.

Releasing an app version replaces the current active version that's served to stores with your app installed. It might take several minutes for app users to be upgraded to the new version.

**Note:**

If you want to create a version, but want to avoid releasing it to users, then run the `deploy` command with a `--no-release` flag.

You can release the unreleased app version using Shopify CLI's [`release`](https://shopify.dev/docs/api/shopify-cli/app/app-release) command, or through the Dev Dashboard.

### Test your app locally

1. Start `app dev` if it's not already running:

   ## Terminal

   ```terminal
   shopify app dev
   ```

   In your terminal, select your development store. You can use the generated URL to test your payments app by using it in your [payments app configuration](#configure-your-payments-extension). If you want a consistent tunnel URL, then you can use the `--tunnel-url` flag with your own tunnel when starting your server.

2. Press `p` to open the app in your browser.

## Explore payment sessions

In this step, you'll explore the flows that an app needs to implement to process a payment.

In the app template, the endpoint that handles start payment session requests is predefined, and will automatically resolve or reject the payment by calling the Payments Apps API, based on the customer's name.

Note that this behavior is exclusively for testing and should be replaced with your own payment processing logic in a production app.

### Start the payment session

When a customer selects your payment provider, Shopify sends an HTTP `POST` request the payment session URL for the app. The request contains information about the customer and the order. To learn more about the request body and header, refer to the [Offsite payment request reference](https://shopify.dev/apps/payments/request-reference#offsite-payment-request-reference).

When the `POST` request is received, the payments app returns an HTTP `2xx` response with a `redirect_url` in the body. The `redirect_url` should be less than `8192 bytes` in length. This response and parameter are required for the payment session creation to be successful.

If the request fails, then it's retried several times. If the request still fails, then the customer needs to retry their payment through Shopify Checkout. If there's an error on the payments app's side, then return an appropriate error status code instead.

***

You configure the payment session URL for your app as part of the [app extension configuration](#configure-your-payments-extension).

**Note:**

It isn't necessary to specify a redirect URL unless additional information is required. You may proceed to resolve or reject the payment after acknowledging the start of the payment session, if appropriate.

## /app/routes/app.payment\_session.jsx

```jsx
import { createPaymentSession } from "~/payments.repository";


/**
 * Saves and starts a payment session.
 * Redirects back to shop if payment session was created.
 */
export const action = async ({ request }) => {
  const requestBody = await request.json();


  const shopDomain = request.headers.get("shopify-shop-domain");


  const paymentSession = await createPaymentSession(createParams(requestBody, shopDomain));


  if (!paymentSession) throw new Response("A PaymentSession couldn't be created.", { status: 500 });


  return { "redirect_url": buildRedirectUrl(request, paymentSession.id) };
}


const createParams = ({id, gid, group, amount, currency, test, kind, customer, payment_method, proposed_at, cancel_url}, shopDomain) => (
  {
    id,
    gid,
    group,
    amount,
    currency,
    test,
    kind,
    customer,
    paymentMethod: payment_method,
    proposedAt: proposed_at,
    cancelUrl: cancel_url,
    shop: shopDomain
  }
)


const buildRedirectUrl = (request, id) => {
  return `${request.url.slice(0, request.url.lastIndexOf("/"))}/payment_simulator/${id}`
}
```

### Sample payment session payload

The metadata we are passing through payment session as part of the checkout UI extension would be contained within the `payment_method` request params under attributes:

## Example payment method request parameters

```json
"payment_method": {
  "type": "custom-onsite",
  "data": {
    "attributes": [
      {
        "key": "payment_plan",
        "value": "pay-in-full"
      }
    ],
      "cancel_url": "https://my-test-shop.com/1/checkouts/4c94d6f5b93f726a82dadfe45cdde432"
  }
},
```

## Example Payload

```json
{
  "id": "u0nwmSrNntjIWozmNslK5Tlq",
  "gid": "gid://shopify/PaymentSession/u0nwmSrNntjIWozmNslK5Tlq",
  "group": "rZNvy+1jH6Z+BcPqA5U5BSIcnUavBha3C63xBalm+xE=",
  "session_id": "4B2dxmle3vGgimS4deUX3+2PgLF2+/0ZWnNsNSZcgdU=",
  "amount": "123.00",
  "currency": "CAD",
  "test": false,
  "merchant_locale": "en",
  "payment_method": {
    "type": "custom-onsite",
    "data": {
      "attributes": [
        {
          "key": "payment_plan",
          "value": "pay-in-full"
        }
      ],
      "cancel_url": "https://my-test-shop.com/1/checkouts/4c94d6f5b93f726a82dadfe45cdde432"
    }
  },
  "proposed_at": "2020-07-13T00:00:00Z",
  "customer": {
    "email": "buyer@example.com",
    "phone_number": "5555555555",
    "locale": "fr",
    "billing_address": {
      "given_name": "Alice",
      "family_name": "Smith",
      "line1": "123 Street",
      "line2": "Suite B",
      "city": "Montreal",
      "postal_code": "H2Z 0B3",
      "province": "Quebec",
      "country_code": "CA",
      "phone_number": "5555555555",
      "company": ""
    },
    "shipping_address": {
      "given_name": "Alice",
      "family_name": "Smith",
      "line1": "123 Street",
      "line2": "Suite B",
      "city": "Montreal",
      "postal_code": "H2Z 0B3",
      "province": "Quebec",
      "country_code": "CA",
      "phone_number": "5555555555",
      "company": ""
    }
  },
  "kind": "sale"
}
```

### Sample payment session payload with localized fields

For certain countries that require additional fields on orders, `localized_fields` are included in the payload inside the `transaction_metadata` object. In the case of Brazil, `localized_fields` contains the CPF value. As a result of this, payment app developers won't need to manually add a CPF field to the payments app.

## Example payload with localized fields

```json
"request_params": {
  "amount":"1.13",
  "app_method":"offsite",
  "cancel_url":"https://<domain>.myshopify.com/checkouts/c/157449dddf0e8eb0c1206ff66eba4b0f/processing",
  "currency":"CAD",
  "customer":{
    "billing_address":{
      "city":"Toronto",
      "country_code":"CA",
      "family_name":"smith",
      "line1":"CN Tower",
      "postal_code":"M5V 3L9",
      "province":"Ontario"
    },
    "email":"someemail@example.com",
    "locale":"en-CA",
    "shipping_address":{
      "city":"Toronto",
      "country_code":"CA",
      "family_name":"smith",
      "line1":"CN Tower",
      "postal_code":"M5V 3L9",
      "province":"Ontario"
    },
  },
  "fx_reconciliation":{
    "currency":"CAD"
  },
  "gid": "gid://shopify/PaymentSession/reItEndH7tKB4sGkjronhdEgv",
  "group": "LWXrK8B1pn0h+qX9VYtVjzRK/bTKAvXzZZqrnwZ0nnA=",
  "session_id": "4B2dxmle3vGgimS4deUX3+2PgLF2+/0ZWnNsNSZcgdU=",
  "id":"reItEndH7tKB4sGkjronhdEgv",
  "kind":"authorization",
  "merchant_locale":"en-CA",
  "payment_method":{
    "data":{
      "cancel_url":"https://<domain>.myshopify.com/checkouts/c/157449dddf0e8eb0c1206ff66eba4b0f/processing"
    },
    "attributes":"[{\"key\":\"payment_plan\",\"value\":\"pay-in-full\"}]",
    "type":"custom_onsite"
  },
  "payment_session":{
    "amount":"1.13",
    "cancel_url":"https://<domain>.myshopify.com/checkouts/c/157449dddf0e8eb0c1206ff66eba4b0f/processing",
    "currency":"CAD",
    "customer":{
      "billing_address":{
        "city":"Toronto",
        "country_code":"CA",
        "family_name":"smith",
        "line1":"CN Tower",
        "postal_code":"M5V 3L9",
        "province":"Ontario"
      },
      "email":"someemail@example.com",
      "locale":"en-CA",
      "shipping_address":{
        "city":"Toronto",
        "country_code":"CA",
        "family_name":"smith",
        "line1":"CN Tower",
        "postal_code":"M5V 3L9",
        "province":"Ontario"
      }
    },
    "transaction_metadata": {
      "localized_fields":[
        {
          "key": "shipping",
          "country_code": "BR",
          "value": "06305371008"
        }
      ],
    },
    "fx_reconciliation":{
      "currency":"CAD"
    },
    "gid": "gid://shopify/PaymentSession/reItEndH7tKB4sGkjronhdEgv",
    "group": "LWXrK8B1pn0h+qX9VYtVjzRK/bTKAvXzZZqrnwZ0nnA=",
    "session_id": "4B2dxmle3vGgimS4deUX3+2PgLF2+/0ZWnNsNSZcgdU=",
    "id":"reItEndH7tKB4sGkjronhdEgv",
    "kind":"authorization",
    "merchant_locale":"en-CA",
    "payment_method":{
      "data":{
      "cancel_url":"https://<domain>.myshopify.com/checkouts/c/157449dddf0e8eb0c1206ff66eba4b0f/processing"
      },
      "attributes":"[{\"key\":\"payment_plan\",\"value\":\"pay-in-full\"}]",
      "type":"custom_onsite"
    },
    "proposed_at":"2023-06-23T17:03:36Z",
    "test":false
  },
  "proposed_at":"2023-06-23T17:03:36Z",
  "test":false
}
```

### Resolve a payment

The payments app uses the `paymentSessionResolve` mutation after the customer has successfully gone through the payment process to complete the payment. The `id` argument corresponds to the global identifier (`gid`) of the payment.

***

In the referenced code, `this.resolveMutation` corresponds to the `paymentSessionResolve` mutation.

[payment​Session​Resolve](https://shopify.dev/docs/api/payments-apps/latest/mutations/paymentSessionResolve)

## /app/payments-apps.graphql.js

```javascript
import schema from "./payments-apps.schema";


import {
  updatePaymentSessionStatus,
  updateRefundSessionStatus,
  updateCaptureSessionStatus,
  updateVoidSessionStatus,
  RESOLVE,
  REJECT,
  PENDING
} from "./payments.repository";


/**
 * Client to interface with the Payments Apps GraphQL API.
 *
 * paymentsAppConfigure: Configure the payments app with the provided variables.
 * paymentSessionResolve: Resolves the given payment session.
 * paymentSessionReject: Rejects the given payment session.
 * refundSessionResolve: Resolves the given refund session.
 * refundSessionReject: Rejects the given refund session.
 */
export default class PaymentsAppsClient {
  constructor(shop, accessToken, type) {
    this.shop = shop;
    this.type = type || PAYMENT; // default
    this.accessToken = accessToken;
    this.resolveMutation = "";
    this.rejectMutation = "";
    this.pendingMutation = "";
    this.dependencyInjector(type);
  }


  /**
   * Generic session resolution function
   * @param {*} session the session to resolve upon
   * @returns the response body from the Shopify Payments Apps API
   */
  async resolveSession({ id, gid }) {
    const response = await this.#perform(schema[this.resolveMutation], { id: gid });
    const responseData = response[this.resolveMutation]
    if (responseData?.userErrors?.length === 0) await this.update?.(id, RESOLVE);


    return responseData;
  }


  /**
   * Generic session rejection function
   * @param {*} session the session to reject upon
   * @returns the response body from the Shopify Payments Apps API
   */
  async rejectSession({ id, gid }) {
    const response = await this.#perform(schema[this.rejectMutation], {
      id: gid,
      reason: {
        code: "PROCESSING_ERROR",
        merchantMessage: "The session was rejected."
      }
    })
    const responseData = response[this.rejectMutation]
    if (responseData?.userErrors?.length === 0) await this.update?.(id, REJECT);


    return responseData;
  }




  /**
   * Generic session pending function
   * @param {*} session the session to pend
   * @returns the response body from the Shopify Payments Apps API
   */
  async pendSession({ id, gid }) {
    if (this.type !== PAYMENT) throw new Error("Cannot pend a session for this client");


    const tomorrow = new Date();
    tomorrow.setDate(tomorrow.getDate() + 1);


    const response = await this.#perform(schema[this.pendingMutation], {
      id: gid,
      pendingExpiresAt: tomorrow.toISOString(),
      reason: "PARTNER_ACTION_REQUIRED"
    });
    const responseData = response[this.pendingMutation];
    if (responseData?.userErrors?.length === 0) await this.update?.(id, PENDING);


    return responseData;
  }


  /**
   * Client perform function. Calls Shopify Payments Apps API.
   * @param {*} query the query to run
   * @param {*} variables the variables to pass
   * @returns
   */
  async #perform(query, variables) {
    const apiVersion = "unstable"


    const response = await fetch(`https://${this.shop}/payments_apps/api/${apiVersion}/graphql.json`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Shopify-Access-Token': this.accessToken
      },
      body: JSON.stringify({
        query,
        variables
      })
    })
    console.log(`[GraphQL] Making request for shop: "${this.shop}", api: "${apiVersion}"`)


    const responseBody = await response.json();
    console.log(`[GraphQL] response: ${JSON.stringify(responseBody)}`);


    return response.ok ? responseBody.data : null
  }


  async paymentsAppConfigure(externalHandle, ready) {
    const response = await this.#perform(schema.paymentsAppConfigure, { externalHandle, ready })
    return response?.paymentsAppConfigure
  }


  /**
   * Function that injects the dependencies for this client based on the session type
   * @param {'payment' | 'refund' | 'capture' | 'void'} type
   * @returns
   */
  dependencyInjector(type) {
    switch(type) {
      case PAYMENT:
        this.resolveMutation = "paymentSessionResolve"
        this.rejectMutation = "paymentSessionReject"
        this.pendingMutation = "paymentSessionPending"
        this.update = updatePaymentSessionStatus
        break;
      case REFUND:
        this.resolveMutation = "refundSessionResolve"
        this.rejectMutation = "refundSessionReject"
        this.update = updateRefundSessionStatus
        break;
      case CAPTURE:
        this.resolveMutation = "captureSessionResolve"
        this.rejectMutation = "captureSessionReject"
        this.update = updateCaptureSessionStatus
        break;
      case VOID:
        this.resolveMutation = "voidSessionResolve"
        this.rejectMutation = "voidSessionReject"
        this.update = updateVoidSessionStatus
        break;
    }
  }
}


export const PAYMENT = "payment"
export const REFUND = "refund"
export const CAPTURE = "capture"
export const VOID = "void"
```

### Reject a payment

The payments app should reject a payment if the customer can't complete a payment with the provider. The rejected payment tells Shopify that the checkout process will be halted. For example, if you don't want to process a high-risk payment, then you can reject the payment using the `paymentSessionReject` mutation.

Rejecting a payment is final. You can't call other actions on a payment after it has been rejected. The payments app should retry a failed user attempt and complete the payment before calling `paymentSessionReject`. For example, if any of the following conditions are met, then you don't need to reject the payment:

* The user doesn't interact with your payments app.
* The user cancels the payment.
* The user needs to retry the payment because of specific errors, such as the user entering the wrong CVV.

***

In the referenced code, `this.rejectMutation` corresponds to the `paymentSessionReject` mutation.

[payment​Session​Reject](https://shopify.dev/docs/api/payments-apps/latest/mutations/paymentSessionReject)

## /app/payments-apps.graphql.js

```javascript
import schema from "./payments-apps.schema";


import {
  updatePaymentSessionStatus,
  updateRefundSessionStatus,
  updateCaptureSessionStatus,
  updateVoidSessionStatus,
  RESOLVE,
  REJECT,
  PENDING
} from "./payments.repository";


/**
 * Client to interface with the Payments Apps GraphQL API.
 *
 * paymentsAppConfigure: Configure the payments app with the provided variables.
 * paymentSessionResolve: Resolves the given payment session.
 * paymentSessionReject: Rejects the given payment session.
 * refundSessionResolve: Resolves the given refund session.
 * refundSessionReject: Rejects the given refund session.
 */
export default class PaymentsAppsClient {
  constructor(shop, accessToken, type) {
    this.shop = shop;
    this.type = type || PAYMENT; // default
    this.accessToken = accessToken;
    this.resolveMutation = "";
    this.rejectMutation = "";
    this.pendingMutation = "";
    this.dependencyInjector(type);
  }


  /**
   * Generic session resolution function
   * @param {*} session the session to resolve upon
   * @returns the response body from the Shopify Payments Apps API
   */
  async resolveSession({ id, gid }) {
    const response = await this.#perform(schema[this.resolveMutation], { id: gid });
    const responseData = response[this.resolveMutation]
    if (responseData?.userErrors?.length === 0) await this.update?.(id, RESOLVE);


    return responseData;
  }


  /**
   * Generic session rejection function
   * @param {*} session the session to reject upon
   * @returns the response body from the Shopify Payments Apps API
   */
  async rejectSession({ id, gid }) {
    const response = await this.#perform(schema[this.rejectMutation], {
      id: gid,
      reason: {
        code: "PROCESSING_ERROR",
        merchantMessage: "The session was rejected."
      }
    })
    const responseData = response[this.rejectMutation]
    if (responseData?.userErrors?.length === 0) await this.update?.(id, REJECT);


    return responseData;
  }




  /**
   * Generic session pending function
   * @param {*} session the session to pend
   * @returns the response body from the Shopify Payments Apps API
   */
  async pendSession({ id, gid }) {
    if (this.type !== PAYMENT) throw new Error("Cannot pend a session for this client");


    const tomorrow = new Date();
    tomorrow.setDate(tomorrow.getDate() + 1);


    const response = await this.#perform(schema[this.pendingMutation], {
      id: gid,
      pendingExpiresAt: tomorrow.toISOString(),
      reason: "PARTNER_ACTION_REQUIRED"
    });
    const responseData = response[this.pendingMutation];
    if (responseData?.userErrors?.length === 0) await this.update?.(id, PENDING);


    return responseData;
  }


  /**
   * Client perform function. Calls Shopify Payments Apps API.
   * @param {*} query the query to run
   * @param {*} variables the variables to pass
   * @returns
   */
  async #perform(query, variables) {
    const apiVersion = "unstable"


    const response = await fetch(`https://${this.shop}/payments_apps/api/${apiVersion}/graphql.json`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Shopify-Access-Token': this.accessToken
      },
      body: JSON.stringify({
        query,
        variables
      })
    })
    console.log(`[GraphQL] Making request for shop: "${this.shop}", api: "${apiVersion}"`)


    const responseBody = await response.json();
    console.log(`[GraphQL] response: ${JSON.stringify(responseBody)}`);


    return response.ok ? responseBody.data : null
  }


  async paymentsAppConfigure(externalHandle, ready) {
    const response = await this.#perform(schema.paymentsAppConfigure, { externalHandle, ready })
    return response?.paymentsAppConfigure
  }


  /**
   * Function that injects the dependencies for this client based on the session type
   * @param {'payment' | 'refund' | 'capture' | 'void'} type
   * @returns
   */
  dependencyInjector(type) {
    switch(type) {
      case PAYMENT:
        this.resolveMutation = "paymentSessionResolve"
        this.rejectMutation = "paymentSessionReject"
        this.pendingMutation = "paymentSessionPending"
        this.update = updatePaymentSessionStatus
        break;
      case REFUND:
        this.resolveMutation = "refundSessionResolve"
        this.rejectMutation = "refundSessionReject"
        this.update = updateRefundSessionStatus
        break;
      case CAPTURE:
        this.resolveMutation = "captureSessionResolve"
        this.rejectMutation = "captureSessionReject"
        this.update = updateCaptureSessionStatus
        break;
      case VOID:
        this.resolveMutation = "voidSessionResolve"
        this.rejectMutation = "voidSessionReject"
        this.update = updateVoidSessionStatus
        break;
    }
  }
}


export const PAYMENT = "payment"
export const REFUND = "refund"
export const CAPTURE = "capture"
export const VOID = "void"
```

### Cancel the payment

If a customer wants to cancel a payment on your provider page, then they are redirected to the merchant's website or store by using the `cancel_url`.

The `cancel_url` is sent to your payments app in the payment request request-body that was sent from Shopify.

Don't use the `paymentSessionReject` mutation to cancel the payment; otherwise, the customer will be unable to pay again with your provider.

## /app/routes/app.payment\_simulator.$paymentId.jsx

```jsx
import {
  Button,
  Card,
  FooterHelp,
  FormLayout,
  Layout,
  Page,
  Text,
  Select,
  BlockStack,
  Link,
  Banner,
} from "@shopify/polaris";
import { useEffect, useState } from "react";
import {
  Form,
  useLoaderData,
  useActionData,
} from "@remix-run/react";
import { json, redirect } from "@remix-run/node";


import { sessionStorage } from "../shopify.server";
import { getPaymentSession, RESOLVE, REJECT, PENDING } from "~/payments.repository";
import PaymentsAppsClient, { PAYMENT } from "~/payments-apps.graphql";


/**
 * Loads the payment session being simulated.
 */
export const loader = async ({ params: { paymentId } }) => {
  const paymentSession = await getPaymentSession(paymentId);
  return json({ paymentSession });
}


/**
 * Completes a payment session based on the simulator's form.
 */
export const action = async ({ request, params: { paymentId } }) => {
  const formData = await request.formData();
  const resolution = formData.get("resolution");


  const paymentSession = await getPaymentSession(paymentId);


  const session = (await sessionStorage.findSessionsByShop(paymentSession.shop))[0];


  const client = new PaymentsAppsClient(session.shop, session.accessToken, PAYMENT);
  let response;


  switch(resolution) {
    case RESOLVE:
      response = await client.resolveSession(paymentSession);
      break;
    case REJECT:
      response = await client.rejectSession(paymentSession);
      break;
    case PENDING:
      response = await client.pendSession(paymentSession);
      break;
  }


  const userErrors = response.userErrors;
  if (userErrors?.length > 0) return json({ errors: userErrors });


  return redirect(response.paymentSession.nextAction.context.redirectUrl);
}


export default function PaymentSimulator() {
  const action = useActionData();
  const { paymentSession } = useLoaderData();
  const [resolution, setResolution] = useState('resolve');
  const [errors, setErrors] = useState([]);


  useEffect(() => {
    if (action?.errors.length > 0) setErrors(action.errors);
  }, [action]);


  const errorBanner = () => (
    errors.length > 0 && (
      <Banner
        title={'😢 An error ocurred!'}
        status="critical"
        onDismiss={() => { setErrors([]) }}
      >
        {
          errors.map(({message}, idx) => (
            <Text as="p" key={idx}>{message}</Text>
          ))
        }
      </Banner>
    )
  )


  const resolutionOptions = [
    {value: RESOLVE, label: 'Resolve'},
    {value: REJECT, label: 'Reject'},
    {value: PENDING, label: 'Pending'}
  ];


  const cancelUrl = paymentSession.cancelUrl;


  return (
    <Page
      title="Payment Simulator"
      backAction={{ url: cancelUrl }}
    >
      <Layout>
        <Layout.Section>
          <BlockStack gap="4">
            {errorBanner()}
          </BlockStack>
        </Layout.Section>
        <Layout.Section>
          <Card>
            <BlockStack gap="5">
              <Form method="post">
                <FormLayout>
                  <Select
                    label="Resolution"
                    name="resolution"
                    options={resolutionOptions}
                    onChange={(change) => setResolution(change)}
                    value={resolution}
                  />
                  <Button submit>Submit</Button>
                </FormLayout>
              </Form>
            </BlockStack>
          </Card>
        </Layout.Section>
      </Layout>


      <FooterHelp>
        <Text as="span">Learn more about </Text>
        <Link url="https://help.shopify.com/en/api/guides/payment-gateway">
          payment sessions
        </Link>
      </FooterHelp>
    </Page>
  );
}
```

### Mark the payment as pending

You can mark a payment as pending if it's awaiting asynchronous action by the customer, the merchant, the payment Partner, or a payment network.

Not all payments can be processed and finalized quickly. Some payments can take several days to complete. Pending a payment indicates to the customer that you have started processing the payment, but require more time to complete the payment.

If an order is in pending payment status, then merchants might be restricted from editing, canceling, or manually capturing payment for the order until the payment is finalized. For more information, refer to the [pending payments documentation](https://help.shopify.com/en/manual/orders/manage-orders/alt_payments_pending) on the Shopify Help Center.

***

In the referenced code, `this.pendingMutation` corresponds to the `paymentSessionPending` mutation.

[payment​Session​Pending](https://shopify.dev/docs/api/payments-apps/latest/mutations/paymentSessionPending)

## /app/payments-apps.graphql.js

```javascript
import schema from "./payments-apps.schema";


import {
  updatePaymentSessionStatus,
  updateRefundSessionStatus,
  updateCaptureSessionStatus,
  updateVoidSessionStatus,
  RESOLVE,
  REJECT,
  PENDING
} from "./payments.repository";


/**
 * Client to interface with the Payments Apps GraphQL API.
 *
 * paymentsAppConfigure: Configure the payments app with the provided variables.
 * paymentSessionResolve: Resolves the given payment session.
 * paymentSessionReject: Rejects the given payment session.
 * refundSessionResolve: Resolves the given refund session.
 * refundSessionReject: Rejects the given refund session.
 */
export default class PaymentsAppsClient {
  constructor(shop, accessToken, type) {
    this.shop = shop;
    this.type = type || PAYMENT; // default
    this.accessToken = accessToken;
    this.resolveMutation = "";
    this.rejectMutation = "";
    this.pendingMutation = "";
    this.dependencyInjector(type);
  }


  /**
   * Generic session resolution function
   * @param {*} session the session to resolve upon
   * @returns the response body from the Shopify Payments Apps API
   */
  async resolveSession({ id, gid }) {
    const response = await this.#perform(schema[this.resolveMutation], { id: gid });
    const responseData = response[this.resolveMutation]
    if (responseData?.userErrors?.length === 0) await this.update?.(id, RESOLVE);


    return responseData;
  }


  /**
   * Generic session rejection function
   * @param {*} session the session to reject upon
   * @returns the response body from the Shopify Payments Apps API
   */
  async rejectSession({ id, gid }) {
    const response = await this.#perform(schema[this.rejectMutation], {
      id: gid,
      reason: {
        code: "PROCESSING_ERROR",
        merchantMessage: "The session was rejected."
      }
    })
    const responseData = response[this.rejectMutation]
    if (responseData?.userErrors?.length === 0) await this.update?.(id, REJECT);


    return responseData;
  }




  /**
   * Generic session pending function
   * @param {*} session the session to pend
   * @returns the response body from the Shopify Payments Apps API
   */
  async pendSession({ id, gid }) {
    if (this.type !== PAYMENT) throw new Error("Cannot pend a session for this client");


    const tomorrow = new Date();
    tomorrow.setDate(tomorrow.getDate() + 1);


    const response = await this.#perform(schema[this.pendingMutation], {
      id: gid,
      pendingExpiresAt: tomorrow.toISOString(),
      reason: "PARTNER_ACTION_REQUIRED"
    });
    const responseData = response[this.pendingMutation];
    if (responseData?.userErrors?.length === 0) await this.update?.(id, PENDING);


    return responseData;
  }


  /**
   * Client perform function. Calls Shopify Payments Apps API.
   * @param {*} query the query to run
   * @param {*} variables the variables to pass
   * @returns
   */
  async #perform(query, variables) {
    const apiVersion = "unstable"


    const response = await fetch(`https://${this.shop}/payments_apps/api/${apiVersion}/graphql.json`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Shopify-Access-Token': this.accessToken
      },
      body: JSON.stringify({
        query,
        variables
      })
    })
    console.log(`[GraphQL] Making request for shop: "${this.shop}", api: "${apiVersion}"`)


    const responseBody = await response.json();
    console.log(`[GraphQL] response: ${JSON.stringify(responseBody)}`);


    return response.ok ? responseBody.data : null
  }


  async paymentsAppConfigure(externalHandle, ready) {
    const response = await this.#perform(schema.paymentsAppConfigure, { externalHandle, ready })
    return response?.paymentsAppConfigure
  }


  /**
   * Function that injects the dependencies for this client based on the session type
   * @param {'payment' | 'refund' | 'capture' | 'void'} type
   * @returns
   */
  dependencyInjector(type) {
    switch(type) {
      case PAYMENT:
        this.resolveMutation = "paymentSessionResolve"
        this.rejectMutation = "paymentSessionReject"
        this.pendingMutation = "paymentSessionPending"
        this.update = updatePaymentSessionStatus
        break;
      case REFUND:
        this.resolveMutation = "refundSessionResolve"
        this.rejectMutation = "refundSessionReject"
        this.update = updateRefundSessionStatus
        break;
      case CAPTURE:
        this.resolveMutation = "captureSessionResolve"
        this.rejectMutation = "captureSessionReject"
        this.update = updateCaptureSessionStatus
        break;
      case VOID:
        this.resolveMutation = "voidSessionResolve"
        this.rejectMutation = "voidSessionReject"
        this.update = updateVoidSessionStatus
        break;
    }
  }
}


export const PAYMENT = "payment"
export const REFUND = "refund"
export const CAPTURE = "capture"
export const VOID = "void"
```

### Triggering resolve, reject, or pending for a payment in the template

In the app template, after a start payment session request has been received by the app, the payment is automatically resolved or rejected based on the customer's name.

If the customer's first name is `reject`, then the payment is rejected. If the customer's first name is `pending`, then the payment is marked as pending. Otherwise, the payment is resolved.

## /app/routes/app.payment\_simulator.$paymentId.jsx

```jsx
import {
  Button,
  Card,
  FooterHelp,
  FormLayout,
  Layout,
  Page,
  Text,
  Select,
  BlockStack,
  Link,
  Banner,
} from "@shopify/polaris";
import { useEffect, useState } from "react";
import {
  Form,
  useLoaderData,
  useActionData,
} from "@remix-run/react";
import { json, redirect } from "@remix-run/node";


import { sessionStorage } from "../shopify.server";
import { getPaymentSession, RESOLVE, REJECT, PENDING } from "~/payments.repository";
import PaymentsAppsClient, { PAYMENT } from "~/payments-apps.graphql";


/**
 * Loads the payment session being simulated.
 */
export const loader = async ({ params: { paymentId } }) => {
  const paymentSession = await getPaymentSession(paymentId);
  return json({ paymentSession });
}


/**
 * Completes a payment session based on the simulator's form.
 */
export const action = async ({ request, params: { paymentId } }) => {
  const formData = await request.formData();
  const resolution = formData.get("resolution");


  const paymentSession = await getPaymentSession(paymentId);


  const session = (await sessionStorage.findSessionsByShop(paymentSession.shop))[0];


  const client = new PaymentsAppsClient(session.shop, session.accessToken, PAYMENT);
  let response;


  switch(resolution) {
    case RESOLVE:
      response = await client.resolveSession(paymentSession);
      break;
    case REJECT:
      response = await client.rejectSession(paymentSession);
      break;
    case PENDING:
      response = await client.pendSession(paymentSession);
      break;
  }


  const userErrors = response.userErrors;
  if (userErrors?.length > 0) return json({ errors: userErrors });


  return redirect(response.paymentSession.nextAction.context.redirectUrl);
}


export default function PaymentSimulator() {
  const action = useActionData();
  const { paymentSession } = useLoaderData();
  const [resolution, setResolution] = useState('resolve');
  const [errors, setErrors] = useState([]);


  useEffect(() => {
    if (action?.errors.length > 0) setErrors(action.errors);
  }, [action]);


  const errorBanner = () => (
    errors.length > 0 && (
      <Banner
        title={'😢 An error ocurred!'}
        status="critical"
        onDismiss={() => { setErrors([]) }}
      >
        {
          errors.map(({message}, idx) => (
            <Text as="p" key={idx}>{message}</Text>
          ))
        }
      </Banner>
    )
  )


  const resolutionOptions = [
    {value: RESOLVE, label: 'Resolve'},
    {value: REJECT, label: 'Reject'},
    {value: PENDING, label: 'Pending'}
  ];


  const cancelUrl = paymentSession.cancelUrl;


  return (
    <Page
      title="Payment Simulator"
      backAction={{ url: cancelUrl }}
    >
      <Layout>
        <Layout.Section>
          <BlockStack gap="4">
            {errorBanner()}
          </BlockStack>
        </Layout.Section>
        <Layout.Section>
          <Card>
            <BlockStack gap="5">
              <Form method="post">
                <FormLayout>
                  <Select
                    label="Resolution"
                    name="resolution"
                    options={resolutionOptions}
                    onChange={(change) => setResolution(change)}
                    value={resolution}
                  />
                  <Button submit>Submit</Button>
                </FormLayout>
              </Form>
            </BlockStack>
          </Card>
        </Layout.Section>
      </Layout>


      <FooterHelp>
        <Text as="span">Learn more about </Text>
        <Link url="https://help.shopify.com/en/api/guides/payment-gateway">
          payment sessions
        </Link>
      </FooterHelp>
    </Page>
  );
}
```

### Process next action

Upon receiving the response from either the `paymentSessionResolve`, `paymentSessionReject`, or `paymentSessionPending` mutations, the next action that the payments app performs is specified under `nextAction`.

The `nextAction` will either be `nil` or contain two fields. In the case where it is `nil`, no next action is expected of the payments app.

Otherwise, the two fields are as follows:

* `action`: A `PaymentSessionNextActionAction` enum that specifies the type of the action the app must perform.
* `context`: A union type requiring inline fragments to access data on the underlying type. Takes a type of `PaymentSessionActionsRedirect`.

[next​Action](https://shopify.dev/docs/api/payments-apps/latest/objects/PaymentSessionNextAction) [Payment​Session​Next​Action​Action](https://shopify.dev/docs/api/payments-apps/latest/enums/PaymentSessionNextActionAction) [Payment​Session​Actions​Redirect](https://shopify.dev/docs/api/payments-apps/latest/objects/PaymentSessionActionsRedirect)

## /app/routes/app.payment\_simulator.$paymentId.jsx

```jsx
import {
  Button,
  Card,
  FooterHelp,
  FormLayout,
  Layout,
  Page,
  Text,
  Select,
  BlockStack,
  Link,
  Banner,
} from "@shopify/polaris";
import { useEffect, useState } from "react";
import {
  Form,
  useLoaderData,
  useActionData,
} from "@remix-run/react";
import { json, redirect } from "@remix-run/node";


import { sessionStorage } from "../shopify.server";
import { getPaymentSession, RESOLVE, REJECT, PENDING } from "~/payments.repository";
import PaymentsAppsClient, { PAYMENT } from "~/payments-apps.graphql";


/**
 * Loads the payment session being simulated.
 */
export const loader = async ({ params: { paymentId } }) => {
  const paymentSession = await getPaymentSession(paymentId);
  return json({ paymentSession });
}


/**
 * Completes a payment session based on the simulator's form.
 */
export const action = async ({ request, params: { paymentId } }) => {
  const formData = await request.formData();
  const resolution = formData.get("resolution");


  const paymentSession = await getPaymentSession(paymentId);


  const session = (await sessionStorage.findSessionsByShop(paymentSession.shop))[0];


  const client = new PaymentsAppsClient(session.shop, session.accessToken, PAYMENT);
  let response;


  switch(resolution) {
    case RESOLVE:
      response = await client.resolveSession(paymentSession);
      break;
    case REJECT:
      response = await client.rejectSession(paymentSession);
      break;
    case PENDING:
      response = await client.pendSession(paymentSession);
      break;
  }


  const userErrors = response.userErrors;
  if (userErrors?.length > 0) return json({ errors: userErrors });


  return redirect(response.paymentSession.nextAction.context.redirectUrl);
}


export default function PaymentSimulator() {
  const action = useActionData();
  const { paymentSession } = useLoaderData();
  const [resolution, setResolution] = useState('resolve');
  const [errors, setErrors] = useState([]);


  useEffect(() => {
    if (action?.errors.length > 0) setErrors(action.errors);
  }, [action]);


  const errorBanner = () => (
    errors.length > 0 && (
      <Banner
        title={'😢 An error ocurred!'}
        status="critical"
        onDismiss={() => { setErrors([]) }}
      >
        {
          errors.map(({message}, idx) => (
            <Text as="p" key={idx}>{message}</Text>
          ))
        }
      </Banner>
    )
  )


  const resolutionOptions = [
    {value: RESOLVE, label: 'Resolve'},
    {value: REJECT, label: 'Reject'},
    {value: PENDING, label: 'Pending'}
  ];


  const cancelUrl = paymentSession.cancelUrl;


  return (
    <Page
      title="Payment Simulator"
      backAction={{ url: cancelUrl }}
    >
      <Layout>
        <Layout.Section>
          <BlockStack gap="4">
            {errorBanner()}
          </BlockStack>
        </Layout.Section>
        <Layout.Section>
          <Card>
            <BlockStack gap="5">
              <Form method="post">
                <FormLayout>
                  <Select
                    label="Resolution"
                    name="resolution"
                    options={resolutionOptions}
                    onChange={(change) => setResolution(change)}
                    value={resolution}
                  />
                  <Button submit>Submit</Button>
                </FormLayout>
              </Form>
            </BlockStack>
          </Card>
        </Layout.Section>
      </Layout>


      <FooterHelp>
        <Text as="span">Learn more about </Text>
        <Link url="https://help.shopify.com/en/api/guides/payment-gateway">
          payment sessions
        </Link>
      </FooterHelp>
    </Page>
  );
}
```

## Explore refund sessions

In this step, you'll explore the flows that an app needs to implement to process a refund. In the app template, the endpoint that handles the start of refund session requests is predefined to store sessions for an asynchronous resolution.

The refund flow begins with an HTTP POST request sent from Shopify to the payments app's refund session URL provided during app extension configuration. To learn more about the request body and header, refer to the [Refund request reference](https://shopify.dev/apps/payments/request-reference#refund-request-reference).

### Start the refund session

The refund flow begins with an HTTP `POST` request sent from Shopify to the payments app's refund session URL. Shopify must receive an HTTP `201` (Created) response for the refund session creation to be successful.

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

***

You configure the refund session URL for your app as part of the [app extension configuration](#configure-your-payments-extension).

## /app/routes/app.refund\_session.jsx

```jsx
import { json } from "@remix-run/node";


import { createRefundSession } from "~/payments.repository";


/**
 * Saves and starts a refund session.
 */
export const action = async ({ request }) => {
  const requestBody = await request.json();


  const refundSessionHash = createParams(requestBody);
  const refundSession = await createRefundSession(refundSessionHash);


  if (!refundSession) throw new Response("A RefundSession couldn't be created.", { status: 500 });


  return json(refundSessionHash);
}


const createParams = ({id, gid, amount, currency, payment_id, proposed_at}) => (
  {
    id,
    gid,
    amount,
    currency,
    paymentId: payment_id,
    proposedAt: proposed_at,
  }
)
```

### Resolve a refund

After the app successfully processes the refund request, the `refundSessionResolve` mutation automatically resolves the refund. The `id` argument corresponds to the `gid` of the refund.

***

In the referenced code, `this.resolveMutation` corresponds to the `refundSessionResolve` mutation.

[refund​Session​Resolve](https://shopify.dev/docs/api/payments-apps/latest/mutations/refundSessionResolve)

## /app/payments-apps.graphql.js

```javascript
import schema from "./payments-apps.schema";


import {
  updatePaymentSessionStatus,
  updateRefundSessionStatus,
  updateCaptureSessionStatus,
  updateVoidSessionStatus,
  RESOLVE,
  REJECT,
  PENDING
} from "./payments.repository";


/**
 * Client to interface with the Payments Apps GraphQL API.
 *
 * paymentsAppConfigure: Configure the payments app with the provided variables.
 * paymentSessionResolve: Resolves the given payment session.
 * paymentSessionReject: Rejects the given payment session.
 * refundSessionResolve: Resolves the given refund session.
 * refundSessionReject: Rejects the given refund session.
 */
export default class PaymentsAppsClient {
  constructor(shop, accessToken, type) {
    this.shop = shop;
    this.type = type || PAYMENT; // default
    this.accessToken = accessToken;
    this.resolveMutation = "";
    this.rejectMutation = "";
    this.pendingMutation = "";
    this.dependencyInjector(type);
  }


  /**
   * Generic session resolution function
   * @param {*} session the session to resolve upon
   * @returns the response body from the Shopify Payments Apps API
   */
  async resolveSession({ id, gid }) {
    const response = await this.#perform(schema[this.resolveMutation], { id: gid });
    const responseData = response[this.resolveMutation]
    if (responseData?.userErrors?.length === 0) await this.update?.(id, RESOLVE);


    return responseData;
  }


  /**
   * Generic session rejection function
   * @param {*} session the session to reject upon
   * @returns the response body from the Shopify Payments Apps API
   */
  async rejectSession({ id, gid }) {
    const response = await this.#perform(schema[this.rejectMutation], {
      id: gid,
      reason: {
        code: "PROCESSING_ERROR",
        merchantMessage: "The session was rejected."
      }
    })
    const responseData = response[this.rejectMutation]
    if (responseData?.userErrors?.length === 0) await this.update?.(id, REJECT);


    return responseData;
  }




  /**
   * Generic session pending function
   * @param {*} session the session to pend
   * @returns the response body from the Shopify Payments Apps API
   */
  async pendSession({ id, gid }) {
    if (this.type !== PAYMENT) throw new Error("Cannot pend a session for this client");


    const tomorrow = new Date();
    tomorrow.setDate(tomorrow.getDate() + 1);


    const response = await this.#perform(schema[this.pendingMutation], {
      id: gid,
      pendingExpiresAt: tomorrow.toISOString(),
      reason: "PARTNER_ACTION_REQUIRED"
    });
    const responseData = response[this.pendingMutation];
    if (responseData?.userErrors?.length === 0) await this.update?.(id, PENDING);


    return responseData;
  }


  /**
   * Client perform function. Calls Shopify Payments Apps API.
   * @param {*} query the query to run
   * @param {*} variables the variables to pass
   * @returns
   */
  async #perform(query, variables) {
    const apiVersion = "unstable"


    const response = await fetch(`https://${this.shop}/payments_apps/api/${apiVersion}/graphql.json`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Shopify-Access-Token': this.accessToken
      },
      body: JSON.stringify({
        query,
        variables
      })
    })
    console.log(`[GraphQL] Making request for shop: "${this.shop}", api: "${apiVersion}"`)


    const responseBody = await response.json();
    console.log(`[GraphQL] response: ${JSON.stringify(responseBody)}`);


    return response.ok ? responseBody.data : null
  }


  async paymentsAppConfigure(externalHandle, ready) {
    const response = await this.#perform(schema.paymentsAppConfigure, { externalHandle, ready })
    return response?.paymentsAppConfigure
  }


  /**
   * Function that injects the dependencies for this client based on the session type
   * @param {'payment' | 'refund' | 'capture' | 'void'} type
   * @returns
   */
  dependencyInjector(type) {
    switch(type) {
      case PAYMENT:
        this.resolveMutation = "paymentSessionResolve"
        this.rejectMutation = "paymentSessionReject"
        this.pendingMutation = "paymentSessionPending"
        this.update = updatePaymentSessionStatus
        break;
      case REFUND:
        this.resolveMutation = "refundSessionResolve"
        this.rejectMutation = "refundSessionReject"
        this.update = updateRefundSessionStatus
        break;
      case CAPTURE:
        this.resolveMutation = "captureSessionResolve"
        this.rejectMutation = "captureSessionReject"
        this.update = updateCaptureSessionStatus
        break;
      case VOID:
        this.resolveMutation = "voidSessionResolve"
        this.rejectMutation = "voidSessionReject"
        this.update = updateVoidSessionStatus
        break;
    }
  }
}


export const PAYMENT = "payment"
export const REFUND = "refund"
export const CAPTURE = "capture"
export const VOID = "void"
```

### Reject a refund

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

The refund is rejected using the `refundSessionReject` mutation.

As part of the rejection, a reason why the refund was rejected must be included 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.

***

In the referenced code, `this.rejectMutation` corresponds to the `refundSessionReject` mutation.

[refund​Session​Reject](https://shopify.dev/docs/api/payments-apps/latest/mutations/refundSessionReject) [Refund​Session​Rejection​Reason​Input](https://shopify.dev/docs/api/payments-apps/latest/input-objects/RefundSessionRejectionReasonInput) [Refund​Session​Status​Reason​Rejection​Code](https://shopify.dev/docs/api/payments-apps/latest/enums/RefundSessionStatusReasonRejectionCode)

## /app/payments-apps.graphql.js

```javascript
import schema from "./payments-apps.schema";


import {
  updatePaymentSessionStatus,
  updateRefundSessionStatus,
  updateCaptureSessionStatus,
  updateVoidSessionStatus,
  RESOLVE,
  REJECT,
  PENDING
} from "./payments.repository";


/**
 * Client to interface with the Payments Apps GraphQL API.
 *
 * paymentsAppConfigure: Configure the payments app with the provided variables.
 * paymentSessionResolve: Resolves the given payment session.
 * paymentSessionReject: Rejects the given payment session.
 * refundSessionResolve: Resolves the given refund session.
 * refundSessionReject: Rejects the given refund session.
 */
export default class PaymentsAppsClient {
  constructor(shop, accessToken, type) {
    this.shop = shop;
    this.type = type || PAYMENT; // default
    this.accessToken = accessToken;
    this.resolveMutation = "";
    this.rejectMutation = "";
    this.pendingMutation = "";
    this.dependencyInjector(type);
  }


  /**
   * Generic session resolution function
   * @param {*} session the session to resolve upon
   * @returns the response body from the Shopify Payments Apps API
   */
  async resolveSession({ id, gid }) {
    const response = await this.#perform(schema[this.resolveMutation], { id: gid });
    const responseData = response[this.resolveMutation]
    if (responseData?.userErrors?.length === 0) await this.update?.(id, RESOLVE);


    return responseData;
  }


  /**
   * Generic session rejection function
   * @param {*} session the session to reject upon
   * @returns the response body from the Shopify Payments Apps API
   */
  async rejectSession({ id, gid }) {
    const response = await this.#perform(schema[this.rejectMutation], {
      id: gid,
      reason: {
        code: "PROCESSING_ERROR",
        merchantMessage: "The session was rejected."
      }
    })
    const responseData = response[this.rejectMutation]
    if (responseData?.userErrors?.length === 0) await this.update?.(id, REJECT);


    return responseData;
  }




  /**
   * Generic session pending function
   * @param {*} session the session to pend
   * @returns the response body from the Shopify Payments Apps API
   */
  async pendSession({ id, gid }) {
    if (this.type !== PAYMENT) throw new Error("Cannot pend a session for this client");


    const tomorrow = new Date();
    tomorrow.setDate(tomorrow.getDate() + 1);


    const response = await this.#perform(schema[this.pendingMutation], {
      id: gid,
      pendingExpiresAt: tomorrow.toISOString(),
      reason: "PARTNER_ACTION_REQUIRED"
    });
    const responseData = response[this.pendingMutation];
    if (responseData?.userErrors?.length === 0) await this.update?.(id, PENDING);


    return responseData;
  }


  /**
   * Client perform function. Calls Shopify Payments Apps API.
   * @param {*} query the query to run
   * @param {*} variables the variables to pass
   * @returns
   */
  async #perform(query, variables) {
    const apiVersion = "unstable"


    const response = await fetch(`https://${this.shop}/payments_apps/api/${apiVersion}/graphql.json`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Shopify-Access-Token': this.accessToken
      },
      body: JSON.stringify({
        query,
        variables
      })
    })
    console.log(`[GraphQL] Making request for shop: "${this.shop}", api: "${apiVersion}"`)


    const responseBody = await response.json();
    console.log(`[GraphQL] response: ${JSON.stringify(responseBody)}`);


    return response.ok ? responseBody.data : null
  }


  async paymentsAppConfigure(externalHandle, ready) {
    const response = await this.#perform(schema.paymentsAppConfigure, { externalHandle, ready })
    return response?.paymentsAppConfigure
  }


  /**
   * Function that injects the dependencies for this client based on the session type
   * @param {'payment' | 'refund' | 'capture' | 'void'} type
   * @returns
   */
  dependencyInjector(type) {
    switch(type) {
      case PAYMENT:
        this.resolveMutation = "paymentSessionResolve"
        this.rejectMutation = "paymentSessionReject"
        this.pendingMutation = "paymentSessionPending"
        this.update = updatePaymentSessionStatus
        break;
      case REFUND:
        this.resolveMutation = "refundSessionResolve"
        this.rejectMutation = "refundSessionReject"
        this.update = updateRefundSessionStatus
        break;
      case CAPTURE:
        this.resolveMutation = "captureSessionResolve"
        this.rejectMutation = "captureSessionReject"
        this.update = updateCaptureSessionStatus
        break;
      case VOID:
        this.resolveMutation = "voidSessionResolve"
        this.rejectMutation = "voidSessionReject"
        this.update = updateVoidSessionStatus
        break;
    }
  }
}


export const PAYMENT = "payment"
export const REFUND = "refund"
export const CAPTURE = "capture"
export const VOID = "void"
```

### Triggering resolve or reject for a refund in the template

In the app template, the simulator built into the dashboard handles the resolution or rejection of all post-payment sessions, including refunds, asynchronously. This means that after a refund is created in a store's Shopify admin, it must be manually completed from the app template's dashboard.

In a production-ready app, your app would process the refund itself once it receives the start refund session request.

## /app/routes/app.dashboard\_simulator.$paymentId.jsx

```jsx
import {
  Button,
  Card,
  Layout,
  Page,
  Text,
  Banner,
  DescriptionList,
  InlineCode,
  Link,
  Icon,
  LegacyStack,
  DataTable,
  Modal,
} from "@shopify/polaris";
import { CancelSmallMinor, TickMinor } from "@shopify/polaris-icons";
import { useState, useCallback, useEffect } from "react";
import {
  useActionData,
  useLoaderData,
  useSubmit
} from "@remix-run/react";
import { json } from "@remix-run/node";


import { sessionStorage } from "../shopify.server";
import {getPaymentSession, PENDING} from "~/payments.repository";
import PaymentsAppsClient, { PAYMENT, REFUND, CAPTURE, VOID } from "~/payments-apps.graphql";


/**
 * Loads in the relevant payment session along with it's related refund, capture, and void sessions.
 */
export const loader = async ({ params: { paymentId } }) => {
  const paymentSession = await getPaymentSession(paymentId);


  return json({
    paymentSession,
    refundSessions: paymentSession.refunds,
    captureSessions: paymentSession.captures,
    voidSession: paymentSession.void
  });
}


/**
 * Action that performs resolution for payment, refund, capture, and void sessions
 *
 * formData: {
 *  resolution: {true/false},
 *  type: {"payment"/"refund"/"capture"/"void"},
 *  session: the payment/refund/capture/void session, as a string
 * }
 */
export const action = async ({ request }) => {
  const formData = await request.formData();
  const resolve = formData.get("resolve") === "true";


  const anySession = JSON.parse(formData.get("session"));
  const session = (await sessionStorage.findSessionsByShop(anySession.shop))[0];


  const client = new PaymentsAppsClient(session.shop, session.accessToken, formData.get("type"));


  const response = resolve
    ? await client.resolveSession(anySession)
    : await client.rejectSession(anySession);


  const userErrors = response.userErrors;
  if (userErrors?.length > 0) return json({ errors: userErrors });


  return json({ active: false })
}


export default function DashboardSimulator() {
  const {
    paymentSession,
    refundSessions,
    captureSessions,
    voidSession
  } = useLoaderData();
  const submit = useSubmit();
  const action = useActionData();


  const [errors, setErrors] = useState([]);
  const [active, setActive] = useState(false);
  const [simulatedType, setSimulatedType]= useState(REFUND);
  const [simulatedSession, setSimulatedSession] = useState({});


  useEffect(() => {
    if (action?.errors && action.errors?.length > 0) setErrors(action.errors);
  }, [action]);


  const errorBanner = () => (
    errors?.length > 0 && (
      <Banner
        title={'😢 An error ocurred!'}
        status="critical"
        onDismiss={() => { setErrors([]) }}
      >
        { errors.map(({message}, idx) => (<Text as="p" key={idx}>{message}</Text>)) }
      </Banner>
    )
  );


  // Resolution Modal


  const modalItems = useCallback(() => {
    const items = [];
    Object.keys(simulatedSession).forEach((key) => {
      if (['refunds', 'captures', 'void', 'test'].includes(key)) return;


      if (key == 'status' && !simulatedSession[key]) {
        items.push({ term: key, description: <Text as="span">Requires Resolution</Text>})
        return;
      }


      items.push({ term: key, description: <InlineCode>{simulatedSession[key]}</InlineCode> })
    })


    return items;
  }, [simulatedSession])


  const resolveModal = useCallback(() => {
    submit({
      resolve: true,
      type: simulatedType,
      session: JSON.stringify(simulatedSession)
    }, { method: "post" })
    setActive(!active)
  }, [simulatedType, simulatedSession, active, setActive]);


  const rejectModal = useCallback(() => {
    submit({
      resolve: false,
      type: simulatedType,
      session: JSON.stringify(simulatedSession)
    }, { method: "post" })
    setActive(!active)
  }, [simulatedType, simulatedSession, active, setActive]);


  const raiseModal = useCallback((session, type) => {
    setSimulatedSession(session)
    setSimulatedType(type)
    setActive(!active)
  }, [active, setSimulatedSession, setSimulatedType, setActive]);


  const activator = (session, type) => {
    if (!session.status || session.status === PENDING)
      return <Button onClick={() => raiseModal(session, type)}>Open</Button>
    return <Icon source={TickMinor} />
  }


  const refundRows = refundSessions.map((refund) => [
    refund.id,
    refund.gid,
    amountString(refund),
    refund.proposedAt,
    refund.status || 'Requires Resolution',
    activator(refund, REFUND),
  ])


  const captureRows = captureSessions.map((capture) => [
    capture.id,
    capture.gid,
    amountString(capture),
    capture.proposedAt,
    capture.status || 'Requires Resolution',
    activator(capture, CAPTURE),
  ])


  const voidItems = () => {
    if (!voidSession) return [];


    const items = Object.keys(voidSession).map((key) => {
      if (key === "status" && !voidSession[key]) {
        return { term: key, description: <Text as="span">Requires Resolution</Text> }
      }
      return {
        term: key,
        description: <Text as="span">{voidSession[key]}</Text>
      }
    })
    if (!voidSession.status) {
      items.push({
        term: "Action",
        description: activator(voidSession, VOID)
      })
    }


    return items
  }


  const paymentItems = buildPaymentItems(paymentSession, activator);


  return (
    <Page
      title="Simulator"
      backAction={{ url: "/app/dashboard" }}
    >
      <Layout>
        <Layout.Section>
          {errorBanner()}
        </Layout.Section>
        <Layout.Section>
          <Card>
            <Text variant="headingMd" as="h6">Payment Details</Text>
            <DescriptionList items={paymentItems}/>
          </Card>
        </Layout.Section>
        <Layout.Section>
          <Card>
            <Text variant="headingMd" as="h6">Refunds</Text>
            <DataTable
              truncate
              verticalAlign="middle"
              columnContentTypes={['text', 'text', 'text', 'text', 'text', 'text']}
              headings={['Refund Session ID', 'GraphQL ID', 'Amount', 'Proposed At', 'Status', 'Action']}
              rows={refundRows}
            />
          </Card>
        </Layout.Section>
        <Layout.Section>
          <Card>
            <Text variant="headingMd" as="h6">Captures</Text>
            <DataTable
              truncate
              verticalAlign="middle"
              columnContentTypes={['text', 'text', 'text', 'text', 'text', 'text']}
              headings={['Capture Session ID', 'GraphQL ID', 'Amount', 'Proposed At', 'Status', 'Action']}
              rows={captureRows}
            />
          </Card>
        </Layout.Section>
        <Layout.Section>
          <Card>
            <Text variant="headingMd" as="h6">Void</Text>
            {
              voidSession
                ? <DescriptionList items={voidItems()}/>
                : <Text as="span" color="subdued"> None found.</Text>
            }
          </Card>
        </Layout.Section>
        <Layout.Section/>
      </Layout>
      <Modal
        open={active}
        onClose={() => setActive(!active)}
        title={`Simulate a ${simulatedType}`}
        primaryAction={{ content: 'Resolve', onAction: resolveModal }}
        secondaryActions={[{ content: 'Reject', onAction: rejectModal }]}
      >
        <Modal.Section>
          <DescriptionList items={modalItems()} />
        </Modal.Section>
      </Modal>
    </Page>
  );
}


const buildPaymentItems = (paymentSession, activator) => {
  const paymentMethod = JSON.parse(paymentSession.paymentMethod)
  const paymentMethodData = Object.keys(paymentMethod.data).map((key) => {
    const value = paymentMethod.data[key]
    let component;
    if (key.includes("url")) {
      component = <Link url={value}>{value}</Link>
    } else {
      component = <Text as="span">{value}</Text>
    }


    return {
      term: key,
      description: component
    }
  })


  const items = [
    {
      term: "Payment Session ID",
      description: (
        <Text as="span">
          <InlineCode>{paymentSession.id}</InlineCode>
        </Text>
      )
    },
    {
      term: "GraphQL ID",
      description: (
        <Text as="span">
          <InlineCode>{paymentSession.gid}</InlineCode>
        </Text>
      )
    },
    {
      term: "Shop Domain",
      description: (
        <Link url={`https://${paymentSession.shop}`}>{paymentSession.shop}</Link>
      )
    },
    {
      term: "Group",
      description: (
        <Text as="span">
          <InlineCode>{paymentSession.group}</InlineCode>
        </Text>
      )
    },
    {
      term: "Amount",
      description: (
        <Text as="span">{amountString(paymentSession)}</Text>
      )
    },
    {
      term: "Test",
      description: (
        <LegacyStack>
          {
            paymentSession.test
              ? (<Icon source={TickMinor} color="primary"/>)
              : (<Icon source={CancelSmallMinor} color="critical"/>)
          }
        </LegacyStack>
      )
    },
    {
      term: "Payment Kind",
      description: (
        <Text as="span">
          <InlineCode>{paymentSession.kind}</InlineCode>
        </Text>
      )
    },
    {
      term: "Payment Method",
      description: (
        <Card>
          <DescriptionList
            items={[
              {
                term: "type",
                description: <Text as="span">{paymentMethod.type}</Text>
              },
              ...paymentMethodData
            ]}
          />
        </Card>
      )
    },
    {
      term: "Proposed At",
      description: (
        <Text as="span">{new Date(paymentSession.proposedAt).toString()}</Text>
      )
    },
    {
      term: "Status",
      description: (
        <Text as="span">{paymentSession.status || "Requires Resolution"}</Text>
      )
    }
  ]


  if (paymentSession.status === PENDING) {
    items.push({
      term: "Action",
      description: activator(paymentSession, PAYMENT)
    })
  }


  return items
}


const amountString = ({amount, currency}) => (amount.toString().concat(' ', currency))
```

## Explore capture sessions (optional)

A capture describes the process of how merchants capture funds for an authorized payment. A capture is the next step of the payment flow, and occurs after an authorized payment is finalized. Finalized payments have `kind` set to `authorization`.

The app template is designed to either resolve or reject a transaction when Shopify sends a capture request to a payments app. This occurs after a merchant tries to capture the funds from an authorized transaction.

### Start the capture session

A capture can only be performed when the payment initiated by Shopify has a `kind` property with a value of `authorization`. With an `authorization`, the app places a hold on the funds and then responds to Shopify's capture request. The `captureSessionResolve` or `captureSessionReject` mutation is used to accept or reject the capture of funds, respectively.

The capture flow begins with an HTTP `POST` request sent from Shopify to the payments app's `capture session URL`.

***

You configure the capture session URL for your app as part of the [app extension configuration](#configure-your-payments-app-extension).

Shopify sends a capture request to the payments app after a merchant tries to capture the funds on an authorized transaction. When this occurs, the app template is set up to store a capture session. These sessions can then be resolved through the simulator for testing. In a production-ready app, this is when your app would process the capture request.

[capture​Session​Reject](https://shopify.dev/docs/api/payments-apps/latest/mutations/captureSessionReject)

## /app/routes/app.capture\_session.jsx

```jsx
import { json } from "@remix-run/node";


import { createCaptureSession } from "~/payments.repository";


/**
 * Saves and starts a capture session.
 */
export const action = async ({ request }) => {
  const requestBody = await request.json();


  const captureSessionHash = createParams(requestBody);
  const captureSession = await createCaptureSession(captureSessionHash);


  if (!captureSession) throw new Response("A CaptureSession couldn't be created.", { status: 500 });


  return json(captureSessionHash);
}


const createParams = ({id, gid, amount, currency, payment_id, proposed_at}) => (
  {
    id,
    gid,
    amount,
    currency,
    paymentId: payment_id,
    proposedAt: proposed_at,
  }
)
```

### Resolve a capture session

After the app successfully processes the capture request, the `captureSessionResolve` mutation automatically resolves the capture. The `id` argument corresponds to the `gid` of the capture.

***

In the referenced code, `this.resolveMutation` corresponds to the `captureSessionResolve` mutation.

[capture​Session​Resolve](https://shopify.dev/docs/api/payments-apps/latest/mutations/captureSessionResolve)

## /app/payments-apps.schema.js

```javascript
/**
* This "schema" contains the mutations consumed by the client in payments-apps.graphql
*/
const paymentsAppConfigure = `
  mutation PaymentsAppConfigure($externalHandle: String, $ready: Boolean!) {
    paymentsAppConfigure(externalHandle: $externalHandle, ready: $ready) {
      userErrors{
        field
        message
      }
    }
  }
`;


const paymentSessionResolve = `
  mutation PaymentSessionResolve($id: ID!) {
    paymentSessionResolve(id: $id) {
      paymentSession {
        id
        state {
          ... on PaymentSessionStateResolved {
            code
          }
        }
        nextAction {
          action
          context {
            ... on PaymentSessionActionsRedirect {
              redirectUrl
            }
          }
        }
        authorizationExpiresAt
        pendingExpiresAt
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const paymentSessionReject = `
  mutation PaymentSessionReject($id: ID!, $reason: PaymentSessionRejectionReasonInput!) {
    paymentSessionReject(id: $id, reason: $reason) {
      paymentSession {
        id
        state {
          ... on PaymentSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
        nextAction {
          action
          context {
            ... on PaymentSessionActionsRedirect {
              redirectUrl
            }
          }
        }
        authorizationExpiresAt
        pendingExpiresAt
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const paymentSessionPending = `
  mutation PaymentSessionPending($id: ID!, $pendingExpiresAt: DateTime!, $reason: PaymentSessionStatePendingReason!) {
    paymentSessionPending(id: $id, pendingExpiresAt: $pendingExpiresAt, reason: $reason) {
      paymentSession {
        id
        state {
          ... on PaymentSessionStatePending {
            code
            reason
          }
        }
        nextAction {
          action
          context {
            ... on PaymentSessionActionsRedirect {
              redirectUrl
            }
          }
        }
        authorizationExpiresAt
        pendingExpiresAt
      }
      userErrors{
        field
        message
      }
    }
  }
`


const refundSessionResolve = `
  mutation RefundSessionResolve($id: ID!) {
    refundSessionResolve(id: $id) {
      refundSession {
        id
        state {
          ... on RefundSessionStateResolved {
            code
          }
        }
      }
      userErrors{
        field
        message
      }
    }
  }
`;


const refundSessionReject = `
  mutation RefundSessionReject($id: ID!, $reason: RefundSessionRejectionReasonInput!) {
    refundSessionReject(id: $id, reason: $reason) {
      refundSession {
        id
        state {
          ... on RefundSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const captureSessionResolve = `
  mutation CaptureSessionResolve($id: ID!) {
    captureSessionResolve(id: $id) {
      captureSession {
        id
        state {
          ... on CaptureSessionStateResolved {
            code
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const captureSessionReject = `
  mutation CaptureSessionReject($id: ID!, $reason: CaptureSessionRejectionReasonInput!) {
    captureSessionReject(id: $id, reason: $reason) {
      captureSession {
        id
        state {
          ... on CaptureSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const voidSessionResolve = `
  mutation VoidSessionResolve($id: ID!) {
    voidSessionResolve(id: $id) {
      voidSession {
        id
        state {
          ... on VoidSessionStateResolved {
            code
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const voidSessionReject = `
  mutation VoidSessionReject($id: ID!, $reason: VoidSessionRejectionReasonInput!) {
    voidSessionReject(id: $id, reason: $reason) {
      voidSession {
        id
        state {
          ... on VoidSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


export default {
  paymentsAppConfigure,
  paymentSessionResolve,
  paymentSessionReject,
  refundSessionResolve,
  refundSessionReject,
  captureSessionResolve,
  captureSessionReject,
  voidSessionResolve,
  voidSessionReject,
  paymentSessionPending,
};
```

### Reject a capture

If you don't want to process a capture request, then you should reject it. You might want to reject a capture if authorization has expired or if you suspect that the request is fraudulent or high risk. You should only reject a capture in the case of final and irrecoverable errors. Otherwise, you should re-attempt to resolve the capture.

The app rejects a capture using the `captureSessionReject` mutation.

As part of the rejection, you need to include a reason why the capture was rejected as part of `CaptureSessionRejectionReasonInput`.

The `CaptureSessionRejectionReasonInput.code` is a `CaptureSessionStatusReasonRejectionCode`, which is an enum of standardized error codes.

The `CaptureSessionRejectionReasonInput.merchantMessage` argument is a localized error message presented to the merchant explaining why the capture was rejected.

***

[capture​Session​Reject](https://shopify.dev/docs/api/payments-apps/latest/mutations/captureSessionReject) [Capture​Session​Rejection​Reason​Input](https://shopify.dev/docs/api/payments-apps/latest/input-objects/CaptureSessionRejectionReasonInput)

## /app/payments-apps.schema.js

```javascript
/**
* This "schema" contains the mutations consumed by the client in payments-apps.graphql
*/
const paymentsAppConfigure = `
  mutation PaymentsAppConfigure($externalHandle: String, $ready: Boolean!) {
    paymentsAppConfigure(externalHandle: $externalHandle, ready: $ready) {
      userErrors{
        field
        message
      }
    }
  }
`;


const paymentSessionResolve = `
  mutation PaymentSessionResolve($id: ID!) {
    paymentSessionResolve(id: $id) {
      paymentSession {
        id
        state {
          ... on PaymentSessionStateResolved {
            code
          }
        }
        nextAction {
          action
          context {
            ... on PaymentSessionActionsRedirect {
              redirectUrl
            }
          }
        }
        authorizationExpiresAt
        pendingExpiresAt
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const paymentSessionReject = `
  mutation PaymentSessionReject($id: ID!, $reason: PaymentSessionRejectionReasonInput!) {
    paymentSessionReject(id: $id, reason: $reason) {
      paymentSession {
        id
        state {
          ... on PaymentSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
        nextAction {
          action
          context {
            ... on PaymentSessionActionsRedirect {
              redirectUrl
            }
          }
        }
        authorizationExpiresAt
        pendingExpiresAt
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const paymentSessionPending = `
  mutation PaymentSessionPending($id: ID!, $pendingExpiresAt: DateTime!, $reason: PaymentSessionStatePendingReason!) {
    paymentSessionPending(id: $id, pendingExpiresAt: $pendingExpiresAt, reason: $reason) {
      paymentSession {
        id
        state {
          ... on PaymentSessionStatePending {
            code
            reason
          }
        }
        nextAction {
          action
          context {
            ... on PaymentSessionActionsRedirect {
              redirectUrl
            }
          }
        }
        authorizationExpiresAt
        pendingExpiresAt
      }
      userErrors{
        field
        message
      }
    }
  }
`


const refundSessionResolve = `
  mutation RefundSessionResolve($id: ID!) {
    refundSessionResolve(id: $id) {
      refundSession {
        id
        state {
          ... on RefundSessionStateResolved {
            code
          }
        }
      }
      userErrors{
        field
        message
      }
    }
  }
`;


const refundSessionReject = `
  mutation RefundSessionReject($id: ID!, $reason: RefundSessionRejectionReasonInput!) {
    refundSessionReject(id: $id, reason: $reason) {
      refundSession {
        id
        state {
          ... on RefundSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const captureSessionResolve = `
  mutation CaptureSessionResolve($id: ID!) {
    captureSessionResolve(id: $id) {
      captureSession {
        id
        state {
          ... on CaptureSessionStateResolved {
            code
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const captureSessionReject = `
  mutation CaptureSessionReject($id: ID!, $reason: CaptureSessionRejectionReasonInput!) {
    captureSessionReject(id: $id, reason: $reason) {
      captureSession {
        id
        state {
          ... on CaptureSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const voidSessionResolve = `
  mutation VoidSessionResolve($id: ID!) {
    voidSessionResolve(id: $id) {
      voidSession {
        id
        state {
          ... on VoidSessionStateResolved {
            code
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const voidSessionReject = `
  mutation VoidSessionReject($id: ID!, $reason: VoidSessionRejectionReasonInput!) {
    voidSessionReject(id: $id, reason: $reason) {
      voidSession {
        id
        state {
          ... on VoidSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


export default {
  paymentsAppConfigure,
  paymentSessionResolve,
  paymentSessionReject,
  refundSessionResolve,
  refundSessionReject,
  captureSessionResolve,
  captureSessionReject,
  voidSessionResolve,
  voidSessionReject,
  paymentSessionPending,
};
```

### Triggering resolve or reject for a capture in the template

In the app template, the simulator built into the dashboard (`/app/dashboard`) handles the resolution or rejection of all post-payment sessions, including captures, asynchronously. This means that after a capture is created in a store's Shopify admin, it must be manually completed from the app template's dashboard.

In a production-ready app, your app would process the capture itself after it receives the start capture session request.

## /app/routes/app.dashboard\_simulator.$paymentId.jsx

```jsx
import {
  Button,
  Card,
  Layout,
  Page,
  Text,
  Banner,
  DescriptionList,
  InlineCode,
  Link,
  Icon,
  LegacyStack,
  DataTable,
  Modal,
} from "@shopify/polaris";
import { CancelSmallMinor, TickMinor } from "@shopify/polaris-icons";
import { useState, useCallback, useEffect } from "react";
import {
  useActionData,
  useLoaderData,
  useSubmit
} from "@remix-run/react";
import { json } from "@remix-run/node";


import { sessionStorage } from "../shopify.server";
import {getPaymentSession, PENDING} from "~/payments.repository";
import PaymentsAppsClient, { PAYMENT, REFUND, CAPTURE, VOID } from "~/payments-apps.graphql";


/**
 * Loads in the relevant payment session along with it's related refund, capture, and void sessions.
 */
export const loader = async ({ params: { paymentId } }) => {
  const paymentSession = await getPaymentSession(paymentId);


  return json({
    paymentSession,
    refundSessions: paymentSession.refunds,
    captureSessions: paymentSession.captures,
    voidSession: paymentSession.void
  });
}


/**
 * Action that performs resolution for payment, refund, capture, and void sessions
 *
 * formData: {
 *  resolution: {true/false},
 *  type: {"payment"/"refund"/"capture"/"void"},
 *  session: the payment/refund/capture/void session, as a string
 * }
 */
export const action = async ({ request }) => {
  const formData = await request.formData();
  const resolve = formData.get("resolve") === "true";


  const anySession = JSON.parse(formData.get("session"));
  const session = (await sessionStorage.findSessionsByShop(anySession.shop))[0];


  const client = new PaymentsAppsClient(session.shop, session.accessToken, formData.get("type"));


  const response = resolve
    ? await client.resolveSession(anySession)
    : await client.rejectSession(anySession);


  const userErrors = response.userErrors;
  if (userErrors?.length > 0) return json({ errors: userErrors });


  return json({ active: false })
}


export default function DashboardSimulator() {
  const {
    paymentSession,
    refundSessions,
    captureSessions,
    voidSession
  } = useLoaderData();
  const submit = useSubmit();
  const action = useActionData();


  const [errors, setErrors] = useState([]);
  const [active, setActive] = useState(false);
  const [simulatedType, setSimulatedType]= useState(REFUND);
  const [simulatedSession, setSimulatedSession] = useState({});


  useEffect(() => {
    if (action?.errors && action.errors?.length > 0) setErrors(action.errors);
  }, [action]);


  const errorBanner = () => (
    errors?.length > 0 && (
      <Banner
        title={'😢 An error ocurred!'}
        status="critical"
        onDismiss={() => { setErrors([]) }}
      >
        { errors.map(({message}, idx) => (<Text as="p" key={idx}>{message}</Text>)) }
      </Banner>
    )
  );


  // Resolution Modal


  const modalItems = useCallback(() => {
    const items = [];
    Object.keys(simulatedSession).forEach((key) => {
      if (['refunds', 'captures', 'void', 'test'].includes(key)) return;


      if (key == 'status' && !simulatedSession[key]) {
        items.push({ term: key, description: <Text as="span">Requires Resolution</Text>})
        return;
      }


      items.push({ term: key, description: <InlineCode>{simulatedSession[key]}</InlineCode> })
    })


    return items;
  }, [simulatedSession])


  const resolveModal = useCallback(() => {
    submit({
      resolve: true,
      type: simulatedType,
      session: JSON.stringify(simulatedSession)
    }, { method: "post" })
    setActive(!active)
  }, [simulatedType, simulatedSession, active, setActive]);


  const rejectModal = useCallback(() => {
    submit({
      resolve: false,
      type: simulatedType,
      session: JSON.stringify(simulatedSession)
    }, { method: "post" })
    setActive(!active)
  }, [simulatedType, simulatedSession, active, setActive]);


  const raiseModal = useCallback((session, type) => {
    setSimulatedSession(session)
    setSimulatedType(type)
    setActive(!active)
  }, [active, setSimulatedSession, setSimulatedType, setActive]);


  const activator = (session, type) => {
    if (!session.status || session.status === PENDING)
      return <Button onClick={() => raiseModal(session, type)}>Open</Button>
    return <Icon source={TickMinor} />
  }


  const refundRows = refundSessions.map((refund) => [
    refund.id,
    refund.gid,
    amountString(refund),
    refund.proposedAt,
    refund.status || 'Requires Resolution',
    activator(refund, REFUND),
  ])


  const captureRows = captureSessions.map((capture) => [
    capture.id,
    capture.gid,
    amountString(capture),
    capture.proposedAt,
    capture.status || 'Requires Resolution',
    activator(capture, CAPTURE),
  ])


  const voidItems = () => {
    if (!voidSession) return [];


    const items = Object.keys(voidSession).map((key) => {
      if (key === "status" && !voidSession[key]) {
        return { term: key, description: <Text as="span">Requires Resolution</Text> }
      }
      return {
        term: key,
        description: <Text as="span">{voidSession[key]}</Text>
      }
    })
    if (!voidSession.status) {
      items.push({
        term: "Action",
        description: activator(voidSession, VOID)
      })
    }


    return items
  }


  const paymentItems = buildPaymentItems(paymentSession, activator);


  return (
    <Page
      title="Simulator"
      backAction={{ url: "/app/dashboard" }}
    >
      <Layout>
        <Layout.Section>
          {errorBanner()}
        </Layout.Section>
        <Layout.Section>
          <Card>
            <Text variant="headingMd" as="h6">Payment Details</Text>
            <DescriptionList items={paymentItems}/>
          </Card>
        </Layout.Section>
        <Layout.Section>
          <Card>
            <Text variant="headingMd" as="h6">Refunds</Text>
            <DataTable
              truncate
              verticalAlign="middle"
              columnContentTypes={['text', 'text', 'text', 'text', 'text', 'text']}
              headings={['Refund Session ID', 'GraphQL ID', 'Amount', 'Proposed At', 'Status', 'Action']}
              rows={refundRows}
            />
          </Card>
        </Layout.Section>
        <Layout.Section>
          <Card>
            <Text variant="headingMd" as="h6">Captures</Text>
            <DataTable
              truncate
              verticalAlign="middle"
              columnContentTypes={['text', 'text', 'text', 'text', 'text', 'text']}
              headings={['Capture Session ID', 'GraphQL ID', 'Amount', 'Proposed At', 'Status', 'Action']}
              rows={captureRows}
            />
          </Card>
        </Layout.Section>
        <Layout.Section>
          <Card>
            <Text variant="headingMd" as="h6">Void</Text>
            {
              voidSession
                ? <DescriptionList items={voidItems()}/>
                : <Text as="span" color="subdued"> None found.</Text>
            }
          </Card>
        </Layout.Section>
        <Layout.Section/>
      </Layout>
      <Modal
        open={active}
        onClose={() => setActive(!active)}
        title={`Simulate a ${simulatedType}`}
        primaryAction={{ content: 'Resolve', onAction: resolveModal }}
        secondaryActions={[{ content: 'Reject', onAction: rejectModal }]}
      >
        <Modal.Section>
          <DescriptionList items={modalItems()} />
        </Modal.Section>
      </Modal>
    </Page>
  );
}


const buildPaymentItems = (paymentSession, activator) => {
  const paymentMethod = JSON.parse(paymentSession.paymentMethod)
  const paymentMethodData = Object.keys(paymentMethod.data).map((key) => {
    const value = paymentMethod.data[key]
    let component;
    if (key.includes("url")) {
      component = <Link url={value}>{value}</Link>
    } else {
      component = <Text as="span">{value}</Text>
    }


    return {
      term: key,
      description: component
    }
  })


  const items = [
    {
      term: "Payment Session ID",
      description: (
        <Text as="span">
          <InlineCode>{paymentSession.id}</InlineCode>
        </Text>
      )
    },
    {
      term: "GraphQL ID",
      description: (
        <Text as="span">
          <InlineCode>{paymentSession.gid}</InlineCode>
        </Text>
      )
    },
    {
      term: "Shop Domain",
      description: (
        <Link url={`https://${paymentSession.shop}`}>{paymentSession.shop}</Link>
      )
    },
    {
      term: "Group",
      description: (
        <Text as="span">
          <InlineCode>{paymentSession.group}</InlineCode>
        </Text>
      )
    },
    {
      term: "Amount",
      description: (
        <Text as="span">{amountString(paymentSession)}</Text>
      )
    },
    {
      term: "Test",
      description: (
        <LegacyStack>
          {
            paymentSession.test
              ? (<Icon source={TickMinor} color="primary"/>)
              : (<Icon source={CancelSmallMinor} color="critical"/>)
          }
        </LegacyStack>
      )
    },
    {
      term: "Payment Kind",
      description: (
        <Text as="span">
          <InlineCode>{paymentSession.kind}</InlineCode>
        </Text>
      )
    },
    {
      term: "Payment Method",
      description: (
        <Card>
          <DescriptionList
            items={[
              {
                term: "type",
                description: <Text as="span">{paymentMethod.type}</Text>
              },
              ...paymentMethodData
            ]}
          />
        </Card>
      )
    },
    {
      term: "Proposed At",
      description: (
        <Text as="span">{new Date(paymentSession.proposedAt).toString()}</Text>
      )
    },
    {
      term: "Status",
      description: (
        <Text as="span">{paymentSession.status || "Requires Resolution"}</Text>
      )
    }
  ]


  if (paymentSession.status === PENDING) {
    items.push({
      term: "Action",
      description: activator(paymentSession, PAYMENT)
    })
  }


  return items
}


const amountString = ({amount, currency}) => (amount.toString().concat(' ', currency))
```

## Explore void sessions (optional)

A void describes the process of how merchants void funds for an authorized payment. A void is the next step of the payment flow, and occurs after an authorized payment is finalized. Finalized payments have `kind` set to `authorization`.

### Start the void session

A void can only be performed when the payment initiated by Shopify has a `kind` property with a value of `authorization`. With an authorization, you place a hold on funds and then reply to Shopify's void request with the `voidSessionResolve` or `voidSessionReject` mutation to accept or reject the voiding of funds respectively.

The void flow begins with an HTTP `POST` request sent from Shopify to the payments app's `void session URL`.

***

You configure the void session URL for your app as part of the [app extension configuration](#configure-your-payments-app-extension).

The app template stores the void session when Shopify sends a void request to a payments app after a merchant tries to cancel the order for an authorized transaction. These sessions can then be resolved through the simulator for testing. In a production-ready app, this is when your app would process the void request.

[void​Session​Resolve](https://shopify.dev/docs/api/payments-apps/latest/mutations/voidSessionResolve) [void​Session​Reject](https://shopify.dev/docs/api/payments-apps/latest/mutations/voidSessionReject)

## /app/routes/app.void\_session.jsx

```jsx
import { json } from "@remix-run/node";


import { createVoidSession } from "~/payments.repository";


/**
 * Saves and starts a void session.
 */
export const action = async ({ request }) => {
  const requestBody = await request.json();


  const voidSessionHash = createParams(requestBody);
  const voidSession = await createVoidSession(voidSessionHash);


  if (!voidSession) throw new Response("A VoidSession couldn't be created.", { status: 500 });


  return json(voidSessionHash);
}


const createParams = ({id, gid, payment_id, proposed_at}) => (
  {
    id,
    gid,
    paymentId: payment_id,
    proposedAt: proposed_at,
  }
)
```

### Resolve a void session

After the app successfully processes the void request, the `voidSessionResolve` mutation automatically resolves the void. The `id` argument corresponds to the `gid` of the void.

***

[void​Session​Resolve](https://shopify.dev/docs/api/payments-apps/latest/mutations/voidSessionResolve)

## /app/payments-apps.schema.js

```javascript
/**
* This "schema" contains the mutations consumed by the client in payments-apps.graphql
*/
const paymentsAppConfigure = `
  mutation PaymentsAppConfigure($externalHandle: String, $ready: Boolean!) {
    paymentsAppConfigure(externalHandle: $externalHandle, ready: $ready) {
      userErrors{
        field
        message
      }
    }
  }
`;


const paymentSessionResolve = `
  mutation PaymentSessionResolve($id: ID!) {
    paymentSessionResolve(id: $id) {
      paymentSession {
        id
        state {
          ... on PaymentSessionStateResolved {
            code
          }
        }
        nextAction {
          action
          context {
            ... on PaymentSessionActionsRedirect {
              redirectUrl
            }
          }
        }
        authorizationExpiresAt
        pendingExpiresAt
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const paymentSessionReject = `
  mutation PaymentSessionReject($id: ID!, $reason: PaymentSessionRejectionReasonInput!) {
    paymentSessionReject(id: $id, reason: $reason) {
      paymentSession {
        id
        state {
          ... on PaymentSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
        nextAction {
          action
          context {
            ... on PaymentSessionActionsRedirect {
              redirectUrl
            }
          }
        }
        authorizationExpiresAt
        pendingExpiresAt
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const paymentSessionPending = `
  mutation PaymentSessionPending($id: ID!, $pendingExpiresAt: DateTime!, $reason: PaymentSessionStatePendingReason!) {
    paymentSessionPending(id: $id, pendingExpiresAt: $pendingExpiresAt, reason: $reason) {
      paymentSession {
        id
        state {
          ... on PaymentSessionStatePending {
            code
            reason
          }
        }
        nextAction {
          action
          context {
            ... on PaymentSessionActionsRedirect {
              redirectUrl
            }
          }
        }
        authorizationExpiresAt
        pendingExpiresAt
      }
      userErrors{
        field
        message
      }
    }
  }
`


const refundSessionResolve = `
  mutation RefundSessionResolve($id: ID!) {
    refundSessionResolve(id: $id) {
      refundSession {
        id
        state {
          ... on RefundSessionStateResolved {
            code
          }
        }
      }
      userErrors{
        field
        message
      }
    }
  }
`;


const refundSessionReject = `
  mutation RefundSessionReject($id: ID!, $reason: RefundSessionRejectionReasonInput!) {
    refundSessionReject(id: $id, reason: $reason) {
      refundSession {
        id
        state {
          ... on RefundSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const captureSessionResolve = `
  mutation CaptureSessionResolve($id: ID!) {
    captureSessionResolve(id: $id) {
      captureSession {
        id
        state {
          ... on CaptureSessionStateResolved {
            code
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const captureSessionReject = `
  mutation CaptureSessionReject($id: ID!, $reason: CaptureSessionRejectionReasonInput!) {
    captureSessionReject(id: $id, reason: $reason) {
      captureSession {
        id
        state {
          ... on CaptureSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const voidSessionResolve = `
  mutation VoidSessionResolve($id: ID!) {
    voidSessionResolve(id: $id) {
      voidSession {
        id
        state {
          ... on VoidSessionStateResolved {
            code
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const voidSessionReject = `
  mutation VoidSessionReject($id: ID!, $reason: VoidSessionRejectionReasonInput!) {
    voidSessionReject(id: $id, reason: $reason) {
      voidSession {
        id
        state {
          ... on VoidSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


export default {
  paymentsAppConfigure,
  paymentSessionResolve,
  paymentSessionReject,
  refundSessionResolve,
  refundSessionReject,
  captureSessionResolve,
  captureSessionReject,
  voidSessionResolve,
  voidSessionReject,
  paymentSessionPending,
};
```

### Reject a void

If you can't process a void request, then you should reject it. You should only reject a void in the case of final and irrecoverable errors. Otherwise, you can re-attempt to resolve the void.

You can reject a void using the `voidSessionReject` mutation.

As part of the rejection, you need to include a reason why the void was rejected as part of `VoidSessionRejectionReasonInput`.

The `VoidSessionRejectionReasonInput.code` is a `VoidSessionStatusReasonRejectionCode`, which is an enum of standardized error codes.

The `VoidSessionRejectionReasonInput.merchantMessage` argument is a localized error message presented to the merchant explaining why the void was rejected.

***

[void​Session​Reject](https://shopify.dev/docs/api/payments-apps/latest/mutations/voidSessionReject) [Void​Session​Rejection​Reason​Input](https://shopify.dev/docs/api/payments-apps/latest/input-objects/VoidSessionRejectionReasonInput) [Void​Session​Status​Reason​Rejection​Code](https://shopify.dev/docs/api/payments-apps/latest/enums/VoidSessionStatusReasonRejectionCode)

## /app/payments-apps.schema.js

```javascript
/**
* This "schema" contains the mutations consumed by the client in payments-apps.graphql
*/
const paymentsAppConfigure = `
  mutation PaymentsAppConfigure($externalHandle: String, $ready: Boolean!) {
    paymentsAppConfigure(externalHandle: $externalHandle, ready: $ready) {
      userErrors{
        field
        message
      }
    }
  }
`;


const paymentSessionResolve = `
  mutation PaymentSessionResolve($id: ID!) {
    paymentSessionResolve(id: $id) {
      paymentSession {
        id
        state {
          ... on PaymentSessionStateResolved {
            code
          }
        }
        nextAction {
          action
          context {
            ... on PaymentSessionActionsRedirect {
              redirectUrl
            }
          }
        }
        authorizationExpiresAt
        pendingExpiresAt
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const paymentSessionReject = `
  mutation PaymentSessionReject($id: ID!, $reason: PaymentSessionRejectionReasonInput!) {
    paymentSessionReject(id: $id, reason: $reason) {
      paymentSession {
        id
        state {
          ... on PaymentSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
        nextAction {
          action
          context {
            ... on PaymentSessionActionsRedirect {
              redirectUrl
            }
          }
        }
        authorizationExpiresAt
        pendingExpiresAt
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const paymentSessionPending = `
  mutation PaymentSessionPending($id: ID!, $pendingExpiresAt: DateTime!, $reason: PaymentSessionStatePendingReason!) {
    paymentSessionPending(id: $id, pendingExpiresAt: $pendingExpiresAt, reason: $reason) {
      paymentSession {
        id
        state {
          ... on PaymentSessionStatePending {
            code
            reason
          }
        }
        nextAction {
          action
          context {
            ... on PaymentSessionActionsRedirect {
              redirectUrl
            }
          }
        }
        authorizationExpiresAt
        pendingExpiresAt
      }
      userErrors{
        field
        message
      }
    }
  }
`


const refundSessionResolve = `
  mutation RefundSessionResolve($id: ID!) {
    refundSessionResolve(id: $id) {
      refundSession {
        id
        state {
          ... on RefundSessionStateResolved {
            code
          }
        }
      }
      userErrors{
        field
        message
      }
    }
  }
`;


const refundSessionReject = `
  mutation RefundSessionReject($id: ID!, $reason: RefundSessionRejectionReasonInput!) {
    refundSessionReject(id: $id, reason: $reason) {
      refundSession {
        id
        state {
          ... on RefundSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const captureSessionResolve = `
  mutation CaptureSessionResolve($id: ID!) {
    captureSessionResolve(id: $id) {
      captureSession {
        id
        state {
          ... on CaptureSessionStateResolved {
            code
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const captureSessionReject = `
  mutation CaptureSessionReject($id: ID!, $reason: CaptureSessionRejectionReasonInput!) {
    captureSessionReject(id: $id, reason: $reason) {
      captureSession {
        id
        state {
          ... on CaptureSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const voidSessionResolve = `
  mutation VoidSessionResolve($id: ID!) {
    voidSessionResolve(id: $id) {
      voidSession {
        id
        state {
          ... on VoidSessionStateResolved {
            code
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const voidSessionReject = `
  mutation VoidSessionReject($id: ID!, $reason: VoidSessionRejectionReasonInput!) {
    voidSessionReject(id: $id, reason: $reason) {
      voidSession {
        id
        state {
          ... on VoidSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


export default {
  paymentsAppConfigure,
  paymentSessionResolve,
  paymentSessionReject,
  refundSessionResolve,
  refundSessionReject,
  captureSessionResolve,
  captureSessionReject,
  voidSessionResolve,
  voidSessionReject,
  paymentSessionPending,
};
```

### Triggering resolve or reject for a void in the template

In the app template, the simulator built into the dashboard handles the resolution or rejection of all post-payment sessions, including voids, asynchronously. This means that after a void is created in a store's Shopify admin, it must be manually completed from the app template's dashboard.

In a production-ready app, your app would process the void itself after it receives the start void session request.

## /app/routes/app.dashboard\_simulator.$paymentId.jsx

```jsx
import {
  Button,
  Card,
  Layout,
  Page,
  Text,
  Banner,
  DescriptionList,
  InlineCode,
  Link,
  Icon,
  LegacyStack,
  DataTable,
  Modal,
} from "@shopify/polaris";
import { CancelSmallMinor, TickMinor } from "@shopify/polaris-icons";
import { useState, useCallback, useEffect } from "react";
import {
  useActionData,
  useLoaderData,
  useSubmit
} from "@remix-run/react";
import { json } from "@remix-run/node";


import { sessionStorage } from "../shopify.server";
import {getPaymentSession, PENDING} from "~/payments.repository";
import PaymentsAppsClient, { PAYMENT, REFUND, CAPTURE, VOID } from "~/payments-apps.graphql";


/**
 * Loads in the relevant payment session along with it's related refund, capture, and void sessions.
 */
export const loader = async ({ params: { paymentId } }) => {
  const paymentSession = await getPaymentSession(paymentId);


  return json({
    paymentSession,
    refundSessions: paymentSession.refunds,
    captureSessions: paymentSession.captures,
    voidSession: paymentSession.void
  });
}


/**
 * Action that performs resolution for payment, refund, capture, and void sessions
 *
 * formData: {
 *  resolution: {true/false},
 *  type: {"payment"/"refund"/"capture"/"void"},
 *  session: the payment/refund/capture/void session, as a string
 * }
 */
export const action = async ({ request }) => {
  const formData = await request.formData();
  const resolve = formData.get("resolve") === "true";


  const anySession = JSON.parse(formData.get("session"));
  const session = (await sessionStorage.findSessionsByShop(anySession.shop))[0];


  const client = new PaymentsAppsClient(session.shop, session.accessToken, formData.get("type"));


  const response = resolve
    ? await client.resolveSession(anySession)
    : await client.rejectSession(anySession);


  const userErrors = response.userErrors;
  if (userErrors?.length > 0) return json({ errors: userErrors });


  return json({ active: false })
}


export default function DashboardSimulator() {
  const {
    paymentSession,
    refundSessions,
    captureSessions,
    voidSession
  } = useLoaderData();
  const submit = useSubmit();
  const action = useActionData();


  const [errors, setErrors] = useState([]);
  const [active, setActive] = useState(false);
  const [simulatedType, setSimulatedType]= useState(REFUND);
  const [simulatedSession, setSimulatedSession] = useState({});


  useEffect(() => {
    if (action?.errors && action.errors?.length > 0) setErrors(action.errors);
  }, [action]);


  const errorBanner = () => (
    errors?.length > 0 && (
      <Banner
        title={'😢 An error ocurred!'}
        status="critical"
        onDismiss={() => { setErrors([]) }}
      >
        { errors.map(({message}, idx) => (<Text as="p" key={idx}>{message}</Text>)) }
      </Banner>
    )
  );


  // Resolution Modal


  const modalItems = useCallback(() => {
    const items = [];
    Object.keys(simulatedSession).forEach((key) => {
      if (['refunds', 'captures', 'void', 'test'].includes(key)) return;


      if (key == 'status' && !simulatedSession[key]) {
        items.push({ term: key, description: <Text as="span">Requires Resolution</Text>})
        return;
      }


      items.push({ term: key, description: <InlineCode>{simulatedSession[key]}</InlineCode> })
    })


    return items;
  }, [simulatedSession])


  const resolveModal = useCallback(() => {
    submit({
      resolve: true,
      type: simulatedType,
      session: JSON.stringify(simulatedSession)
    }, { method: "post" })
    setActive(!active)
  }, [simulatedType, simulatedSession, active, setActive]);


  const rejectModal = useCallback(() => {
    submit({
      resolve: false,
      type: simulatedType,
      session: JSON.stringify(simulatedSession)
    }, { method: "post" })
    setActive(!active)
  }, [simulatedType, simulatedSession, active, setActive]);


  const raiseModal = useCallback((session, type) => {
    setSimulatedSession(session)
    setSimulatedType(type)
    setActive(!active)
  }, [active, setSimulatedSession, setSimulatedType, setActive]);


  const activator = (session, type) => {
    if (!session.status || session.status === PENDING)
      return <Button onClick={() => raiseModal(session, type)}>Open</Button>
    return <Icon source={TickMinor} />
  }


  const refundRows = refundSessions.map((refund) => [
    refund.id,
    refund.gid,
    amountString(refund),
    refund.proposedAt,
    refund.status || 'Requires Resolution',
    activator(refund, REFUND),
  ])


  const captureRows = captureSessions.map((capture) => [
    capture.id,
    capture.gid,
    amountString(capture),
    capture.proposedAt,
    capture.status || 'Requires Resolution',
    activator(capture, CAPTURE),
  ])


  const voidItems = () => {
    if (!voidSession) return [];


    const items = Object.keys(voidSession).map((key) => {
      if (key === "status" && !voidSession[key]) {
        return { term: key, description: <Text as="span">Requires Resolution</Text> }
      }
      return {
        term: key,
        description: <Text as="span">{voidSession[key]}</Text>
      }
    })
    if (!voidSession.status) {
      items.push({
        term: "Action",
        description: activator(voidSession, VOID)
      })
    }


    return items
  }


  const paymentItems = buildPaymentItems(paymentSession, activator);


  return (
    <Page
      title="Simulator"
      backAction={{ url: "/app/dashboard" }}
    >
      <Layout>
        <Layout.Section>
          {errorBanner()}
        </Layout.Section>
        <Layout.Section>
          <Card>
            <Text variant="headingMd" as="h6">Payment Details</Text>
            <DescriptionList items={paymentItems}/>
          </Card>
        </Layout.Section>
        <Layout.Section>
          <Card>
            <Text variant="headingMd" as="h6">Refunds</Text>
            <DataTable
              truncate
              verticalAlign="middle"
              columnContentTypes={['text', 'text', 'text', 'text', 'text', 'text']}
              headings={['Refund Session ID', 'GraphQL ID', 'Amount', 'Proposed At', 'Status', 'Action']}
              rows={refundRows}
            />
          </Card>
        </Layout.Section>
        <Layout.Section>
          <Card>
            <Text variant="headingMd" as="h6">Captures</Text>
            <DataTable
              truncate
              verticalAlign="middle"
              columnContentTypes={['text', 'text', 'text', 'text', 'text', 'text']}
              headings={['Capture Session ID', 'GraphQL ID', 'Amount', 'Proposed At', 'Status', 'Action']}
              rows={captureRows}
            />
          </Card>
        </Layout.Section>
        <Layout.Section>
          <Card>
            <Text variant="headingMd" as="h6">Void</Text>
            {
              voidSession
                ? <DescriptionList items={voidItems()}/>
                : <Text as="span" color="subdued"> None found.</Text>
            }
          </Card>
        </Layout.Section>
        <Layout.Section/>
      </Layout>
      <Modal
        open={active}
        onClose={() => setActive(!active)}
        title={`Simulate a ${simulatedType}`}
        primaryAction={{ content: 'Resolve', onAction: resolveModal }}
        secondaryActions={[{ content: 'Reject', onAction: rejectModal }]}
      >
        <Modal.Section>
          <DescriptionList items={modalItems()} />
        </Modal.Section>
      </Modal>
    </Page>
  );
}


const buildPaymentItems = (paymentSession, activator) => {
  const paymentMethod = JSON.parse(paymentSession.paymentMethod)
  const paymentMethodData = Object.keys(paymentMethod.data).map((key) => {
    const value = paymentMethod.data[key]
    let component;
    if (key.includes("url")) {
      component = <Link url={value}>{value}</Link>
    } else {
      component = <Text as="span">{value}</Text>
    }


    return {
      term: key,
      description: component
    }
  })


  const items = [
    {
      term: "Payment Session ID",
      description: (
        <Text as="span">
          <InlineCode>{paymentSession.id}</InlineCode>
        </Text>
      )
    },
    {
      term: "GraphQL ID",
      description: (
        <Text as="span">
          <InlineCode>{paymentSession.gid}</InlineCode>
        </Text>
      )
    },
    {
      term: "Shop Domain",
      description: (
        <Link url={`https://${paymentSession.shop}`}>{paymentSession.shop}</Link>
      )
    },
    {
      term: "Group",
      description: (
        <Text as="span">
          <InlineCode>{paymentSession.group}</InlineCode>
        </Text>
      )
    },
    {
      term: "Amount",
      description: (
        <Text as="span">{amountString(paymentSession)}</Text>
      )
    },
    {
      term: "Test",
      description: (
        <LegacyStack>
          {
            paymentSession.test
              ? (<Icon source={TickMinor} color="primary"/>)
              : (<Icon source={CancelSmallMinor} color="critical"/>)
          }
        </LegacyStack>
      )
    },
    {
      term: "Payment Kind",
      description: (
        <Text as="span">
          <InlineCode>{paymentSession.kind}</InlineCode>
        </Text>
      )
    },
    {
      term: "Payment Method",
      description: (
        <Card>
          <DescriptionList
            items={[
              {
                term: "type",
                description: <Text as="span">{paymentMethod.type}</Text>
              },
              ...paymentMethodData
            ]}
          />
        </Card>
      )
    },
    {
      term: "Proposed At",
      description: (
        <Text as="span">{new Date(paymentSession.proposedAt).toString()}</Text>
      )
    },
    {
      term: "Status",
      description: (
        <Text as="span">{paymentSession.status || "Requires Resolution"}</Text>
      )
    }
  ]


  if (paymentSession.status === PENDING) {
    items.push({
      term: "Action",
      description: activator(paymentSession, PAYMENT)
    })
  }


  return items
}


const amountString = ({amount, currency}) => (amount.toString().concat(' ', currency))
```

## Explore buyer verification challenge (optional)

Alternative payment extensions can make a request for Shopify to present a verification challenge to the buyer during the checkout process, starting with API version `2025-07`. This feature enables partners to add an extra layer of verification after the payment session has started without redirecting the buyer to an external page.

This experience is optional and should only be used when the buyer needs to complete additional verification for the payment to succeed.

### Payment session flow with verification challenges

Once the payment session has been initiated, you can initiate the request for a buyer challenge using the `paymentSessionModal` mutation.

The overall flow is:

1. Shopify initiates a payment session with your payments extension.

2. Your payments extension sends a `paymentSessionModal` request to Shopify with challenge details.

3. Shopify presents a verification challenge to the buyer on their checkout.

4. The buyer completes the verification challenge (for example, scanning a QR code and following instructions).

5. Your payments extension resolves or rejects the payment request, or marks it as pending.

6. If the payment is successful or pending, Shopify creates an order and the buyer is redirected to the Thank You Page.

   If your payments extension rejects the payment, the buyer is redirected back to the Checkout page.

![Example of a buyer verification modal](https://shopify.dev/assets/assets/apps/payments/custom-payment-method-user-challenge-DiU2sXL8.png)

**Note:**

Shopify creates the order only after the buyer completes verification and your payment app resolves or marks the payment as pending.

***

[payment​Session​Modal](https://shopify.dev/docs/api/payments-apps/latest/mutations/paymentsessionmodal)

### Using the `paymentSessionModal` mutation

To initiate a request for Shopify to present a buyer verification challenge, your payments extension will need to use the `paymentSessionModal` mutation.

**Prerequisites:**

* Shopify needs to grant you the `write_payment_session_modals` and `read_payment_session_modals` access scopes. Check with your partnerships manager if these scopes are not already enabled for your app.

The `paymentSessionModal` mutation takes the following parameters:

## paymentSessionModal mutation

```graphql
mutation PaymentSessionModal($id: ID!, $expiresAt: DateTime, $action: ModalAction!) {
  paymentSessionModal(id: $id, expiresAt: $expiresAt, action: $action) {
    paymentSession {
      // PaymentSession fields
    }
    userErrors {
      code
      message
    }
  }
}
```

With variables:

## Variables example

```json
{
  "id": "gid://shopify/PaymentSession/<payment_id>",
  "expires_at": "2025-02-02T15:50:00Z",
  "action": {
    "qrCode": {
      "code": "your-verification-code",
      "data": {
        "additional-field-key": "additional-field-value"
      }
    }
  }
}
```

***

[payment​Session​Modal](https://shopify.dev/docs/api/payments-apps/latest/mutations/paymentsessionmodal) [Payment​Session](https://shopify.dev/docs/api/payments-apps/latest/objects/paymentsession)

### Timeout, expiry, cancellation, and re-attempts

Buyers are expected to complete the required challenge within the time limit set by partners using the `expiresAt` variable in the mutation, with a maximum limit defined by Shopify. If a buyer is unable to complete the action within this time, Shopify will mark the payment as expired. The buyer will then be redirected back to checkout, where they can attempt the checkout process again.

* Use the `expiresAt` variable to pass in the date and time when the buyer challenge expires.

* Shopify has set a default maximum timeout of `7 minutes` and adds an extra `5 seconds` buffer on top to account for network latency. After this time has elapsed, Shopify marks the payment session as expired and the buyer is shown an error message. You can set a shorter expiration time if needed for your use case.

* The `expiresAt` value must be less than or equal to the Shopify-defined maximum timeout into the future from the time the challenge is generated.

* If the buyer closes the payment modal, Shopify will redirect them back to the checkout page.

* Buyers are able to re-attempt the payment from the checkout page if it expires or if they close the modal. Buyers have the option to keep the same payment method selected or change it if necessary.

  If they choose to proceed with the same payment method, Shopify will initiate a new payment session with the payments extension.

  **Note:**

  If they choose to proceed with the same payment method, Shopify will initiate a new payment session with the payments extension.

***

[payment​Session​Modal](https://shopify.dev/docs/api/payments-apps/latest/mutations/paymentsessionmodal)

### Known limitations and restrictions

When implementing buyer verification challenges, be aware of these limitations:

1. The experience within the modal is currently controlled by Shopify and is not customizable by partners through UI extensions/etc.

## Explore confirm sessions

**Note:**

This feature is only available to merchants with access to one page checkout and/or who have upgraded to Shopify Extensions in Checkout.

If your app supports Inventory Confirmation, you must use the [`paymentSessionConfirm`](https://shopify.dev/docs/api/payments-apps/latest/mutations/paymentSessionConfirm) mutation to confirm with Shopify whether to proceed with the payment request, according to Shopify's business logic.

We enforce payment apps supporting this feature to have a `Refund session URL` in their [payment extension configuration](https://shopify.dev/docs/apps/build/payments#build-options). This requirement is triggered if more than 3 minutes elapse between confirmation and its resolution. After this length of time has passed, the confirmation process is re-initiated. If the re-initiated confirmation fails, then Shopify will issue a refund request on the payment.

If you support capture/void, then we will look at the payment to decide if we should void instead of refund.

### Confirm a payment

You must use the `paymentSessionConfirm` mutation to confirm with Shopify whether to proceed with the payment request, according to Shopify's business logic. For example, Shopify checks that inventory is still available and that discount codes are still valid. Call this mutation when the customer has completed the steps of the payment, and you are ready to process the payment.

The `id` argument corresponds to the global identifier (`gid`) of the payment.

Shopify will return a `Null` `nextAction` in the response, not expecting the payments app to redirect the buyer back to Shopify at this point.

Your app should indicate activity to the user and must wait for Shopify to confirm whether the payment request can proceed.

[payment​Session​Confirm](https://shopify.dev/docs/api/payments-apps/latest/mutations/paymentSessionConfirm) [next​Action](https://shopify.dev/docs/api/payments-apps/latest/objects/PaymentSessionNextAction)

### Process a confirm session

When Shopify determines that the payment request can proceed, Shopify sends a POST [request](https://shopify.dev/docs/apps/build/payments/request-reference#confirm-a-payment-3-d-secure-and-inventory-confirmation) to the confirm session URL of the alternative payments app extension, delivering the confirmation result:

Shopify must receive an HTTP `200` or `201` response for the payment session confirmation to be successful.

If the request fails, then it's retried several times. If the request still fails, then the customer needs to retry their payment through Shopify checkout.

If your app can't proceed with the payment, then reject it with the [`paymentSessionReject`](https://shopify.dev/docs/api/payments-apps/latest/mutations/paymentSessionReject) mutation rather than signalling the outcome in the HTTP response. Shopify doesn't read the response body, and any response other than `200` or `201` is treated as a provider error that fails the payment attempt.

### Complete a payment

After the confirmation callback has been received by your application, we expect you to either [resolve](https://shopify.dev/docs/apps/build/payments/offsite/use-the-cli#resolve-a-payment), [reject](https://shopify.dev/docs/apps/build/payments/offsite/use-the-cli#reject-a-payment), or [pend](https://shopify.dev/docs/apps/build/payments/offsite/use-the-cli#mark-the-payment-as-pending) the payment.

When Shopify indicates that the payment request can't proceed, the payments app must invoke the `paymentSessionReject` mutation using the `CONFIRMATION_REJECTED` reason code.

Shopify expects the payment to be resolved/rejected within 3 minutes of the successful confirmation callback being sent to the payments app. If the call takes longer than 3 minutes, we will attempt to re-run the confirmation process on resolve. As we no longer guarantee the confirmation result, after resolving the payment, Shopify may issue a refund/void request.

[payment​Session​Resolve](https://shopify.dev/docs/api/payments-apps/latest/mutations/paymentSessionResolve) [payment​Session​Pending](https://shopify.dev/docs/api/payments-apps/latest/mutations/paymentSessionPending) [payment​Session​Reject](https://shopify.dev/docs/api/payments-apps/latest/mutations/paymentSessionReject) [CONFIRMATION\_REJECTED](https://shopify.dev/docs/api/payments-apps/latest/enums/PaymentSessionStateRejectedReason#enums-CONFIRMATION_REJECTED)

## Test your payments app locally

Before submitting your app extension for review, you should test that your various endpoints work as expected locally.

### Submit a request to your local server

With the dev server you started [previously](#start-your-development-server), go through the relevant requests from our [reference](https://shopify.dev/docs/apps/build/payments/request-reference), and submit a request to your app with `cURL` or an API platform like Postman or Insomnia.

## Test your payments app with a Shop

Preview your app to make sure that it works as expected with Shopify.

**Info:**

The testing steps outlined in this section are specific to apps built with the template. The template provides a basic UI that lets you test the payment flows, but your app might have a UI stored outside of the Shopify admin.

### Start your server

If you're using a permanent tunnel with your app extension, you can use the Shopify CLI [`dev`](https://shopify.dev/docs/api/shopify-cli/app/app-dev) command to build your app and preview it on your dev store.

Otherwise, deploy your app to your server, and move to the next step.

1. In a terminal, navigate to your app directory.

2. Either start or restart your server to build and preview your app:

   ## Terminal

   ```bash
   shopify app dev --tunnel-url <tunnel>
   ```

3. Press `p` to open the App Home.

### Install and test the payments app

Follow these steps to test the payments app flows:

1. From the app splash page, enter an account name.

2. Select **Ready** > **Unstable** and click **Submit**.

You need to set your app as ready to process payments by calling the [`paymentsAppConfigure`](https://shopify.dev/docs/api/payments-apps/latest/mutations/paymentsAppConfigure) mutation.

**Caution:**

If you set `ready: false` after the app is installed and activated on the store, the request will fail.

1. In the banner, click **Return to Shopify**.

After calling the mutation, you must configure your app to redirect back to the Shopify admin using the following URL:

https://{shop}.myshopify.com/services/payments\_partners/gateways/${api\_key}/settings

1. Enable test mode.

2. Click **Activate**.

[payments​App​Configure](https://shopify.dev/docs/api/payments-apps/latest/mutations/paymentsAppConfigure)

## /app/payments-apps.schema.js

```javascript
/**
* This "schema" contains the mutations consumed by the client in payments-apps.graphql
*/
const paymentsAppConfigure = `
  mutation PaymentsAppConfigure($externalHandle: String, $ready: Boolean!) {
    paymentsAppConfigure(externalHandle: $externalHandle, ready: $ready) {
      userErrors{
        field
        message
      }
    }
  }
`;


const paymentSessionResolve = `
  mutation PaymentSessionResolve($id: ID!) {
    paymentSessionResolve(id: $id) {
      paymentSession {
        id
        state {
          ... on PaymentSessionStateResolved {
            code
          }
        }
        nextAction {
          action
          context {
            ... on PaymentSessionActionsRedirect {
              redirectUrl
            }
          }
        }
        authorizationExpiresAt
        pendingExpiresAt
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const paymentSessionReject = `
  mutation PaymentSessionReject($id: ID!, $reason: PaymentSessionRejectionReasonInput!) {
    paymentSessionReject(id: $id, reason: $reason) {
      paymentSession {
        id
        state {
          ... on PaymentSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
        nextAction {
          action
          context {
            ... on PaymentSessionActionsRedirect {
              redirectUrl
            }
          }
        }
        authorizationExpiresAt
        pendingExpiresAt
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const paymentSessionPending = `
  mutation PaymentSessionPending($id: ID!, $pendingExpiresAt: DateTime!, $reason: PaymentSessionStatePendingReason!) {
    paymentSessionPending(id: $id, pendingExpiresAt: $pendingExpiresAt, reason: $reason) {
      paymentSession {
        id
        state {
          ... on PaymentSessionStatePending {
            code
            reason
          }
        }
        nextAction {
          action
          context {
            ... on PaymentSessionActionsRedirect {
              redirectUrl
            }
          }
        }
        authorizationExpiresAt
        pendingExpiresAt
      }
      userErrors{
        field
        message
      }
    }
  }
`


const refundSessionResolve = `
  mutation RefundSessionResolve($id: ID!) {
    refundSessionResolve(id: $id) {
      refundSession {
        id
        state {
          ... on RefundSessionStateResolved {
            code
          }
        }
      }
      userErrors{
        field
        message
      }
    }
  }
`;


const refundSessionReject = `
  mutation RefundSessionReject($id: ID!, $reason: RefundSessionRejectionReasonInput!) {
    refundSessionReject(id: $id, reason: $reason) {
      refundSession {
        id
        state {
          ... on RefundSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const captureSessionResolve = `
  mutation CaptureSessionResolve($id: ID!) {
    captureSessionResolve(id: $id) {
      captureSession {
        id
        state {
          ... on CaptureSessionStateResolved {
            code
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const captureSessionReject = `
  mutation CaptureSessionReject($id: ID!, $reason: CaptureSessionRejectionReasonInput!) {
    captureSessionReject(id: $id, reason: $reason) {
      captureSession {
        id
        state {
          ... on CaptureSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const voidSessionResolve = `
  mutation VoidSessionResolve($id: ID!) {
    voidSessionResolve(id: $id) {
      voidSession {
        id
        state {
          ... on VoidSessionStateResolved {
            code
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const voidSessionReject = `
  mutation VoidSessionReject($id: ID!, $reason: VoidSessionRejectionReasonInput!) {
    voidSessionReject(id: $id, reason: $reason) {
      voidSession {
        id
        state {
          ... on VoidSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


export default {
  paymentsAppConfigure,
  paymentSessionResolve,
  paymentSessionReject,
  refundSessionResolve,
  refundSessionReject,
  captureSessionResolve,
  captureSessionReject,
  voidSessionResolve,
  voidSessionReject,
  paymentSessionPending,
};
```

### Set up your payments app to accept test payments

**Note:**

This step is applicable only to the template. If you've implemented the app yourself, then skip to the next step.

Onboard your app onto your dev store.

1. In the Shopify admin for your dev store, go to **Settings** > [**Apps and sales channels**](https://admin.shopify.com/admin/settings/apps).

2. Select your payments app, then click **Open app**. The App Home opens in a new window.

3. From the App Home, enter an account name, select **Ready?** and your `Payments Apps API` **API Version**, and then click **Submit**.

   In the banner, click **Return to Shopify**.

   You'll return to admin, where you can review the app's details prior to activation.

   ##### An error ocurred while onboarding your app

   Your app extension may not be set up properly. Ensure the URLs provided are accurate, and that the app extension is being updated in the active dev session. You might have to uninstall and reinstall your payments app to successfully onboard.

   You can uninstall and reinstall your app using one of the following methods:

   * **Restart your dev session**: Quit your dev session, run `shopify app dev clean`, then run `shopify app dev` to restart the session. This will uninstall and reinstall your app.
   * **Manually uninstall and reinstall**: Uninstall your app from **Settings** > [**Apps and sales channels**](https://admin.shopify.com/admin/settings/apps) in your app's admin, then reinstall it from your app's overview page in the [Dev Dashboard](https://dev.shopify.com/dashboard) by clicking **Install app** in the **Installs** section.

4. Enable test mode.

5. Click `Activate`.

Now that your app is installed, you can test payments, refunds, captures, and voids.

***

[Payments Apps API](https://shopify.dev/docs/api/payments-apps)

### Test payments

Make a payment, and create an order.

1. In your dev store, add a product to your cart, and then begin a checkout.

2. Complete the checkout as usual, until the **Payment** section.

   At this point, your payments app should be available under the **Payment** section.

3. Enter some test payments details, and then select **Pay now**.

   Shopify sends a request to the `payment session URL` specified in your [app extension configuration](#configure-your-payments-apps-extension).

   Your app should then begin, and complete processing the payment.

4. Verify the payment is complete by finding the order under [**Orders**](https://shopify.com/admin/orders) in your shop's admin.

### Test refunds

Make a refund on an order.

1. In the Shopify admin for your dev store, go to [**Orders**](https://shopify.com/admin/orders). Select an order with a completed payment.

2. In the top right corner, click **Refund**.

3. Select the item to refund, and then click **Refund \<amount>**.

   Shopify sends a request to the `refund session URL` specified in your [app extension configuration](#configure-your-payments-extension).

If you've customized your app, then the refund process should trigger.

If you're testing the app template, then your app receives this request, and saves a record of it. Perform the following additional steps:

1. Navigate to `/app/dashboard` in your app to find the relevant payment session. From the App Home, you can click **Dashboard**.

2. Click **Simulate**, and then scroll down to **Refunds**. On the relevant refund session, click **Open**.

3. Select **Resolve** or **Reject** to complete the refund.

4. Verify that the refund has completed by returning to the order under [**Orders**](https://shopify.com/admin/orders) in your store's admin. You should see that the order is now marked as **Refunded**.

### Test captures

Capture funds from an authorized payment.

1. In your dev store, [enable manual payment capture](https://help.shopify.com/en/manual/payments/payment-authorization#set-up-manual-payment-capture) to test captures.

2. Submit another [test payment](#test-payments).

   The order appears in the Shopify admin under [**Orders**](https://shopify.com/admin/orders).

3. Open the new order. In the top right corner, click **Capture payment**.

4. In the page that opens, click **Accept \<amount>**.

   Shopify sends a request to the `capture session URL` specified in your [app extension configuration](#configure-your-payments-apps-extension).

If you've customized your app, then the capture process should trigger.

If you're testing the app template, then your app receives this request, and saves a record of it. Perform the following additional steps:

1. Navigate to `/app/dashboard` in your app to find the relevant payment session. From the App Home, you can click **Dashboard**.

2. Click **Simulate**, and then scroll down to **Captures**. On the relevant capture session, click **Open**.

3. Select **Resolve** or **Reject** to complete the capture.

4. Verify that the capture has completed by returning to the order under [**Orders**](https://shopify.com/admin/orders) in your store's admin. You should see that the order is now marked as **Paid**.

### Test voids

Void an authorized payment.

1. In your dev store, [enable manual payment capture](https://help.shopify.com/en/manual/payments/payment-authorization#set-up-manual-payment-capture) to test voids.

2. Submit another [test payment](#test-payments).

   The order appears in the Shopify admin under [**Orders**](https://shopify.com/admin/orders).

3. Open the new order. In the top right, click **More actions**, then **Cancel order**. In the modal that opens, click **Cancel order**.

   Shopify sends a request to the `void session URL` specified in your [app extension configuration](#configure-your-payments-apps-extension).

If you've customized your app, then the void process should trigger.

If you're testing the app template, then your app receives this request, and saves a record of it. Perform the following additional steps:

1. Navigate to `/app/dashboard` in your app to find the relevant payment session. From the App Home, you can click **Dashboard**.

2. Click **Simulate**, and then scroll down to **Void**. Click **Open** on the void session.

3. Select **Resolve** or **Reject** to complete the void.

4. Verify that the void has completed by returning to the order under [**Orders**](https://shopify.com/admin/orders) in your store's admin. You should see that the order is now marked as **Voided**.

### Test error scenarios

If you want to test out an error scenario in the template, then set the last name in a checkout to any `PaymentSessionStateRejectedReason`, and then complete the checkout as normal. The app template receives this code and automatically rejects the payment with it.

***

[Payment​Session​State​Rejected​Reason](https://shopify.dev/docs/api/payments-apps/latest/enums/PaymentSessionStateRejectedReason#top)

## /payments-app-extension-custom-onsite/shopify.extension.toml.liquid

```liquid
# The version of APIs your extension will receive. Learn more:
# https://shopify.dev/docs/api/usage/versioning
api_version = "2026-01"


[[extensions]]
name = "{{ name }}"
handle = "{{ handle }}"
type = "payments_extension"
{% if uid %}uid = "{{ uid }}"{% endif %}


merchant_label = "Custom Onsite Payments App Extension"
payment_session_url = "https://example.com/payment"
refund_session_url = "https://example.com/refund"
# List of ISO 3166 (alpha-2) country codes your app is available for installation by merchants. Learn more:
# https://www.iso.org/iso-3166-country-codes.html
supported_countries = ["US"]
# List payment method names that your payment extension will support. Learn more:
# https://github.com/activemerchant/payment_icons/blob/master/db/payment_icons.yml
supported_payment_methods = ["visa"]
supports_3ds = false
supports_installments = false
supports_deferred_payments = false
test_mode_available = true


# buyer_label = ""
# [[extensions.buyer_label_translations]]
# label = "translation"
# locale = "fr"


# [[extensions.buyer_label_translations]]
# label = "translation"
# locale = "da"


# UI Extension
# ui_extension_handle = "ui-ext-handle"


# [[extensions.checkout_payment_method_fields]]
# key = "field1"
# type = "string"
# required = true


# [[extensions.checkout_payment_method_fields]]
# key = "field2"
# type = "string"
# required = true


[[extensions.targeting]]
target = "payments.custom-onsite.render"
```

## /app/routes/app.payment\_session.jsx

```jsx
import { createPaymentSession } from "~/payments.repository";


/**
 * Saves and starts a payment session.
 * Redirects back to shop if payment session was created.
 */
export const action = async ({ request }) => {
  const requestBody = await request.json();


  const shopDomain = request.headers.get("shopify-shop-domain");


  const paymentSession = await createPaymentSession(createParams(requestBody, shopDomain));


  if (!paymentSession) throw new Response("A PaymentSession couldn't be created.", { status: 500 });


  return { "redirect_url": buildRedirectUrl(request, paymentSession.id) };
}


const createParams = ({id, gid, group, amount, currency, test, kind, customer, payment_method, proposed_at, cancel_url}, shopDomain) => (
  {
    id,
    gid,
    group,
    amount,
    currency,
    test,
    kind,
    customer,
    paymentMethod: payment_method,
    proposedAt: proposed_at,
    cancelUrl: cancel_url,
    shop: shopDomain
  }
)


const buildRedirectUrl = (request, id) => {
  return `${request.url.slice(0, request.url.lastIndexOf("/"))}/payment_simulator/${id}`
}
```

## /app/payments-apps.graphql.js

```javascript
import schema from "./payments-apps.schema";


import {
  updatePaymentSessionStatus,
  updateRefundSessionStatus,
  updateCaptureSessionStatus,
  updateVoidSessionStatus,
  RESOLVE,
  REJECT,
  PENDING
} from "./payments.repository";


/**
 * Client to interface with the Payments Apps GraphQL API.
 *
 * paymentsAppConfigure: Configure the payments app with the provided variables.
 * paymentSessionResolve: Resolves the given payment session.
 * paymentSessionReject: Rejects the given payment session.
 * refundSessionResolve: Resolves the given refund session.
 * refundSessionReject: Rejects the given refund session.
 */
export default class PaymentsAppsClient {
  constructor(shop, accessToken, type) {
    this.shop = shop;
    this.type = type || PAYMENT; // default
    this.accessToken = accessToken;
    this.resolveMutation = "";
    this.rejectMutation = "";
    this.pendingMutation = "";
    this.dependencyInjector(type);
  }


  /**
   * Generic session resolution function
   * @param {*} session the session to resolve upon
   * @returns the response body from the Shopify Payments Apps API
   */
  async resolveSession({ id, gid }) {
    const response = await this.#perform(schema[this.resolveMutation], { id: gid });
    const responseData = response[this.resolveMutation]
    if (responseData?.userErrors?.length === 0) await this.update?.(id, RESOLVE);


    return responseData;
  }


  /**
   * Generic session rejection function
   * @param {*} session the session to reject upon
   * @returns the response body from the Shopify Payments Apps API
   */
  async rejectSession({ id, gid }) {
    const response = await this.#perform(schema[this.rejectMutation], {
      id: gid,
      reason: {
        code: "PROCESSING_ERROR",
        merchantMessage: "The session was rejected."
      }
    })
    const responseData = response[this.rejectMutation]
    if (responseData?.userErrors?.length === 0) await this.update?.(id, REJECT);


    return responseData;
  }




  /**
   * Generic session pending function
   * @param {*} session the session to pend
   * @returns the response body from the Shopify Payments Apps API
   */
  async pendSession({ id, gid }) {
    if (this.type !== PAYMENT) throw new Error("Cannot pend a session for this client");


    const tomorrow = new Date();
    tomorrow.setDate(tomorrow.getDate() + 1);


    const response = await this.#perform(schema[this.pendingMutation], {
      id: gid,
      pendingExpiresAt: tomorrow.toISOString(),
      reason: "PARTNER_ACTION_REQUIRED"
    });
    const responseData = response[this.pendingMutation];
    if (responseData?.userErrors?.length === 0) await this.update?.(id, PENDING);


    return responseData;
  }


  /**
   * Client perform function. Calls Shopify Payments Apps API.
   * @param {*} query the query to run
   * @param {*} variables the variables to pass
   * @returns
   */
  async #perform(query, variables) {
    const apiVersion = "unstable"


    const response = await fetch(`https://${this.shop}/payments_apps/api/${apiVersion}/graphql.json`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Shopify-Access-Token': this.accessToken
      },
      body: JSON.stringify({
        query,
        variables
      })
    })
    console.log(`[GraphQL] Making request for shop: "${this.shop}", api: "${apiVersion}"`)


    const responseBody = await response.json();
    console.log(`[GraphQL] response: ${JSON.stringify(responseBody)}`);


    return response.ok ? responseBody.data : null
  }


  async paymentsAppConfigure(externalHandle, ready) {
    const response = await this.#perform(schema.paymentsAppConfigure, { externalHandle, ready })
    return response?.paymentsAppConfigure
  }


  /**
   * Function that injects the dependencies for this client based on the session type
   * @param {'payment' | 'refund' | 'capture' | 'void'} type
   * @returns
   */
  dependencyInjector(type) {
    switch(type) {
      case PAYMENT:
        this.resolveMutation = "paymentSessionResolve"
        this.rejectMutation = "paymentSessionReject"
        this.pendingMutation = "paymentSessionPending"
        this.update = updatePaymentSessionStatus
        break;
      case REFUND:
        this.resolveMutation = "refundSessionResolve"
        this.rejectMutation = "refundSessionReject"
        this.update = updateRefundSessionStatus
        break;
      case CAPTURE:
        this.resolveMutation = "captureSessionResolve"
        this.rejectMutation = "captureSessionReject"
        this.update = updateCaptureSessionStatus
        break;
      case VOID:
        this.resolveMutation = "voidSessionResolve"
        this.rejectMutation = "voidSessionReject"
        this.update = updateVoidSessionStatus
        break;
    }
  }
}


export const PAYMENT = "payment"
export const REFUND = "refund"
export const CAPTURE = "capture"
export const VOID = "void"
```

## /app/routes/app.payment\_simulator.$paymentId.jsx

```jsx
import {
  Button,
  Card,
  FooterHelp,
  FormLayout,
  Layout,
  Page,
  Text,
  Select,
  BlockStack,
  Link,
  Banner,
} from "@shopify/polaris";
import { useEffect, useState } from "react";
import {
  Form,
  useLoaderData,
  useActionData,
} from "@remix-run/react";
import { json, redirect } from "@remix-run/node";


import { sessionStorage } from "../shopify.server";
import { getPaymentSession, RESOLVE, REJECT, PENDING } from "~/payments.repository";
import PaymentsAppsClient, { PAYMENT } from "~/payments-apps.graphql";


/**
 * Loads the payment session being simulated.
 */
export const loader = async ({ params: { paymentId } }) => {
  const paymentSession = await getPaymentSession(paymentId);
  return json({ paymentSession });
}


/**
 * Completes a payment session based on the simulator's form.
 */
export const action = async ({ request, params: { paymentId } }) => {
  const formData = await request.formData();
  const resolution = formData.get("resolution");


  const paymentSession = await getPaymentSession(paymentId);


  const session = (await sessionStorage.findSessionsByShop(paymentSession.shop))[0];


  const client = new PaymentsAppsClient(session.shop, session.accessToken, PAYMENT);
  let response;


  switch(resolution) {
    case RESOLVE:
      response = await client.resolveSession(paymentSession);
      break;
    case REJECT:
      response = await client.rejectSession(paymentSession);
      break;
    case PENDING:
      response = await client.pendSession(paymentSession);
      break;
  }


  const userErrors = response.userErrors;
  if (userErrors?.length > 0) return json({ errors: userErrors });


  return redirect(response.paymentSession.nextAction.context.redirectUrl);
}


export default function PaymentSimulator() {
  const action = useActionData();
  const { paymentSession } = useLoaderData();
  const [resolution, setResolution] = useState('resolve');
  const [errors, setErrors] = useState([]);


  useEffect(() => {
    if (action?.errors.length > 0) setErrors(action.errors);
  }, [action]);


  const errorBanner = () => (
    errors.length > 0 && (
      <Banner
        title={'😢 An error ocurred!'}
        status="critical"
        onDismiss={() => { setErrors([]) }}
      >
        {
          errors.map(({message}, idx) => (
            <Text as="p" key={idx}>{message}</Text>
          ))
        }
      </Banner>
    )
  )


  const resolutionOptions = [
    {value: RESOLVE, label: 'Resolve'},
    {value: REJECT, label: 'Reject'},
    {value: PENDING, label: 'Pending'}
  ];


  const cancelUrl = paymentSession.cancelUrl;


  return (
    <Page
      title="Payment Simulator"
      backAction={{ url: cancelUrl }}
    >
      <Layout>
        <Layout.Section>
          <BlockStack gap="4">
            {errorBanner()}
          </BlockStack>
        </Layout.Section>
        <Layout.Section>
          <Card>
            <BlockStack gap="5">
              <Form method="post">
                <FormLayout>
                  <Select
                    label="Resolution"
                    name="resolution"
                    options={resolutionOptions}
                    onChange={(change) => setResolution(change)}
                    value={resolution}
                  />
                  <Button submit>Submit</Button>
                </FormLayout>
              </Form>
            </BlockStack>
          </Card>
        </Layout.Section>
      </Layout>


      <FooterHelp>
        <Text as="span">Learn more about </Text>
        <Link url="https://help.shopify.com/en/api/guides/payment-gateway">
          payment sessions
        </Link>
      </FooterHelp>
    </Page>
  );
}
```

## /app/routes/app.refund\_session.jsx

```jsx
import { json } from "@remix-run/node";


import { createRefundSession } from "~/payments.repository";


/**
 * Saves and starts a refund session.
 */
export const action = async ({ request }) => {
  const requestBody = await request.json();


  const refundSessionHash = createParams(requestBody);
  const refundSession = await createRefundSession(refundSessionHash);


  if (!refundSession) throw new Response("A RefundSession couldn't be created.", { status: 500 });


  return json(refundSessionHash);
}


const createParams = ({id, gid, amount, currency, payment_id, proposed_at}) => (
  {
    id,
    gid,
    amount,
    currency,
    paymentId: payment_id,
    proposedAt: proposed_at,
  }
)
```

## /app/routes/app.dashboard\_simulator.$paymentId.jsx

```jsx
import {
  Button,
  Card,
  Layout,
  Page,
  Text,
  Banner,
  DescriptionList,
  InlineCode,
  Link,
  Icon,
  LegacyStack,
  DataTable,
  Modal,
} from "@shopify/polaris";
import { CancelSmallMinor, TickMinor } from "@shopify/polaris-icons";
import { useState, useCallback, useEffect } from "react";
import {
  useActionData,
  useLoaderData,
  useSubmit
} from "@remix-run/react";
import { json } from "@remix-run/node";


import { sessionStorage } from "../shopify.server";
import {getPaymentSession, PENDING} from "~/payments.repository";
import PaymentsAppsClient, { PAYMENT, REFUND, CAPTURE, VOID } from "~/payments-apps.graphql";


/**
 * Loads in the relevant payment session along with it's related refund, capture, and void sessions.
 */
export const loader = async ({ params: { paymentId } }) => {
  const paymentSession = await getPaymentSession(paymentId);


  return json({
    paymentSession,
    refundSessions: paymentSession.refunds,
    captureSessions: paymentSession.captures,
    voidSession: paymentSession.void
  });
}


/**
 * Action that performs resolution for payment, refund, capture, and void sessions
 *
 * formData: {
 *  resolution: {true/false},
 *  type: {"payment"/"refund"/"capture"/"void"},
 *  session: the payment/refund/capture/void session, as a string
 * }
 */
export const action = async ({ request }) => {
  const formData = await request.formData();
  const resolve = formData.get("resolve") === "true";


  const anySession = JSON.parse(formData.get("session"));
  const session = (await sessionStorage.findSessionsByShop(anySession.shop))[0];


  const client = new PaymentsAppsClient(session.shop, session.accessToken, formData.get("type"));


  const response = resolve
    ? await client.resolveSession(anySession)
    : await client.rejectSession(anySession);


  const userErrors = response.userErrors;
  if (userErrors?.length > 0) return json({ errors: userErrors });


  return json({ active: false })
}


export default function DashboardSimulator() {
  const {
    paymentSession,
    refundSessions,
    captureSessions,
    voidSession
  } = useLoaderData();
  const submit = useSubmit();
  const action = useActionData();


  const [errors, setErrors] = useState([]);
  const [active, setActive] = useState(false);
  const [simulatedType, setSimulatedType]= useState(REFUND);
  const [simulatedSession, setSimulatedSession] = useState({});


  useEffect(() => {
    if (action?.errors && action.errors?.length > 0) setErrors(action.errors);
  }, [action]);


  const errorBanner = () => (
    errors?.length > 0 && (
      <Banner
        title={'😢 An error ocurred!'}
        status="critical"
        onDismiss={() => { setErrors([]) }}
      >
        { errors.map(({message}, idx) => (<Text as="p" key={idx}>{message}</Text>)) }
      </Banner>
    )
  );


  // Resolution Modal


  const modalItems = useCallback(() => {
    const items = [];
    Object.keys(simulatedSession).forEach((key) => {
      if (['refunds', 'captures', 'void', 'test'].includes(key)) return;


      if (key == 'status' && !simulatedSession[key]) {
        items.push({ term: key, description: <Text as="span">Requires Resolution</Text>})
        return;
      }


      items.push({ term: key, description: <InlineCode>{simulatedSession[key]}</InlineCode> })
    })


    return items;
  }, [simulatedSession])


  const resolveModal = useCallback(() => {
    submit({
      resolve: true,
      type: simulatedType,
      session: JSON.stringify(simulatedSession)
    }, { method: "post" })
    setActive(!active)
  }, [simulatedType, simulatedSession, active, setActive]);


  const rejectModal = useCallback(() => {
    submit({
      resolve: false,
      type: simulatedType,
      session: JSON.stringify(simulatedSession)
    }, { method: "post" })
    setActive(!active)
  }, [simulatedType, simulatedSession, active, setActive]);


  const raiseModal = useCallback((session, type) => {
    setSimulatedSession(session)
    setSimulatedType(type)
    setActive(!active)
  }, [active, setSimulatedSession, setSimulatedType, setActive]);


  const activator = (session, type) => {
    if (!session.status || session.status === PENDING)
      return <Button onClick={() => raiseModal(session, type)}>Open</Button>
    return <Icon source={TickMinor} />
  }


  const refundRows = refundSessions.map((refund) => [
    refund.id,
    refund.gid,
    amountString(refund),
    refund.proposedAt,
    refund.status || 'Requires Resolution',
    activator(refund, REFUND),
  ])


  const captureRows = captureSessions.map((capture) => [
    capture.id,
    capture.gid,
    amountString(capture),
    capture.proposedAt,
    capture.status || 'Requires Resolution',
    activator(capture, CAPTURE),
  ])


  const voidItems = () => {
    if (!voidSession) return [];


    const items = Object.keys(voidSession).map((key) => {
      if (key === "status" && !voidSession[key]) {
        return { term: key, description: <Text as="span">Requires Resolution</Text> }
      }
      return {
        term: key,
        description: <Text as="span">{voidSession[key]}</Text>
      }
    })
    if (!voidSession.status) {
      items.push({
        term: "Action",
        description: activator(voidSession, VOID)
      })
    }


    return items
  }


  const paymentItems = buildPaymentItems(paymentSession, activator);


  return (
    <Page
      title="Simulator"
      backAction={{ url: "/app/dashboard" }}
    >
      <Layout>
        <Layout.Section>
          {errorBanner()}
        </Layout.Section>
        <Layout.Section>
          <Card>
            <Text variant="headingMd" as="h6">Payment Details</Text>
            <DescriptionList items={paymentItems}/>
          </Card>
        </Layout.Section>
        <Layout.Section>
          <Card>
            <Text variant="headingMd" as="h6">Refunds</Text>
            <DataTable
              truncate
              verticalAlign="middle"
              columnContentTypes={['text', 'text', 'text', 'text', 'text', 'text']}
              headings={['Refund Session ID', 'GraphQL ID', 'Amount', 'Proposed At', 'Status', 'Action']}
              rows={refundRows}
            />
          </Card>
        </Layout.Section>
        <Layout.Section>
          <Card>
            <Text variant="headingMd" as="h6">Captures</Text>
            <DataTable
              truncate
              verticalAlign="middle"
              columnContentTypes={['text', 'text', 'text', 'text', 'text', 'text']}
              headings={['Capture Session ID', 'GraphQL ID', 'Amount', 'Proposed At', 'Status', 'Action']}
              rows={captureRows}
            />
          </Card>
        </Layout.Section>
        <Layout.Section>
          <Card>
            <Text variant="headingMd" as="h6">Void</Text>
            {
              voidSession
                ? <DescriptionList items={voidItems()}/>
                : <Text as="span" color="subdued"> None found.</Text>
            }
          </Card>
        </Layout.Section>
        <Layout.Section/>
      </Layout>
      <Modal
        open={active}
        onClose={() => setActive(!active)}
        title={`Simulate a ${simulatedType}`}
        primaryAction={{ content: 'Resolve', onAction: resolveModal }}
        secondaryActions={[{ content: 'Reject', onAction: rejectModal }]}
      >
        <Modal.Section>
          <DescriptionList items={modalItems()} />
        </Modal.Section>
      </Modal>
    </Page>
  );
}


const buildPaymentItems = (paymentSession, activator) => {
  const paymentMethod = JSON.parse(paymentSession.paymentMethod)
  const paymentMethodData = Object.keys(paymentMethod.data).map((key) => {
    const value = paymentMethod.data[key]
    let component;
    if (key.includes("url")) {
      component = <Link url={value}>{value}</Link>
    } else {
      component = <Text as="span">{value}</Text>
    }


    return {
      term: key,
      description: component
    }
  })


  const items = [
    {
      term: "Payment Session ID",
      description: (
        <Text as="span">
          <InlineCode>{paymentSession.id}</InlineCode>
        </Text>
      )
    },
    {
      term: "GraphQL ID",
      description: (
        <Text as="span">
          <InlineCode>{paymentSession.gid}</InlineCode>
        </Text>
      )
    },
    {
      term: "Shop Domain",
      description: (
        <Link url={`https://${paymentSession.shop}`}>{paymentSession.shop}</Link>
      )
    },
    {
      term: "Group",
      description: (
        <Text as="span">
          <InlineCode>{paymentSession.group}</InlineCode>
        </Text>
      )
    },
    {
      term: "Amount",
      description: (
        <Text as="span">{amountString(paymentSession)}</Text>
      )
    },
    {
      term: "Test",
      description: (
        <LegacyStack>
          {
            paymentSession.test
              ? (<Icon source={TickMinor} color="primary"/>)
              : (<Icon source={CancelSmallMinor} color="critical"/>)
          }
        </LegacyStack>
      )
    },
    {
      term: "Payment Kind",
      description: (
        <Text as="span">
          <InlineCode>{paymentSession.kind}</InlineCode>
        </Text>
      )
    },
    {
      term: "Payment Method",
      description: (
        <Card>
          <DescriptionList
            items={[
              {
                term: "type",
                description: <Text as="span">{paymentMethod.type}</Text>
              },
              ...paymentMethodData
            ]}
          />
        </Card>
      )
    },
    {
      term: "Proposed At",
      description: (
        <Text as="span">{new Date(paymentSession.proposedAt).toString()}</Text>
      )
    },
    {
      term: "Status",
      description: (
        <Text as="span">{paymentSession.status || "Requires Resolution"}</Text>
      )
    }
  ]


  if (paymentSession.status === PENDING) {
    items.push({
      term: "Action",
      description: activator(paymentSession, PAYMENT)
    })
  }


  return items
}


const amountString = ({amount, currency}) => (amount.toString().concat(' ', currency))
```

## /app/routes/app.capture\_session.jsx

```jsx
import { json } from "@remix-run/node";


import { createCaptureSession } from "~/payments.repository";


/**
 * Saves and starts a capture session.
 */
export const action = async ({ request }) => {
  const requestBody = await request.json();


  const captureSessionHash = createParams(requestBody);
  const captureSession = await createCaptureSession(captureSessionHash);


  if (!captureSession) throw new Response("A CaptureSession couldn't be created.", { status: 500 });


  return json(captureSessionHash);
}


const createParams = ({id, gid, amount, currency, payment_id, proposed_at}) => (
  {
    id,
    gid,
    amount,
    currency,
    paymentId: payment_id,
    proposedAt: proposed_at,
  }
)
```

## /app/payments-apps.schema.js

```javascript
/**
* This "schema" contains the mutations consumed by the client in payments-apps.graphql
*/
const paymentsAppConfigure = `
  mutation PaymentsAppConfigure($externalHandle: String, $ready: Boolean!) {
    paymentsAppConfigure(externalHandle: $externalHandle, ready: $ready) {
      userErrors{
        field
        message
      }
    }
  }
`;


const paymentSessionResolve = `
  mutation PaymentSessionResolve($id: ID!) {
    paymentSessionResolve(id: $id) {
      paymentSession {
        id
        state {
          ... on PaymentSessionStateResolved {
            code
          }
        }
        nextAction {
          action
          context {
            ... on PaymentSessionActionsRedirect {
              redirectUrl
            }
          }
        }
        authorizationExpiresAt
        pendingExpiresAt
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const paymentSessionReject = `
  mutation PaymentSessionReject($id: ID!, $reason: PaymentSessionRejectionReasonInput!) {
    paymentSessionReject(id: $id, reason: $reason) {
      paymentSession {
        id
        state {
          ... on PaymentSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
        nextAction {
          action
          context {
            ... on PaymentSessionActionsRedirect {
              redirectUrl
            }
          }
        }
        authorizationExpiresAt
        pendingExpiresAt
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const paymentSessionPending = `
  mutation PaymentSessionPending($id: ID!, $pendingExpiresAt: DateTime!, $reason: PaymentSessionStatePendingReason!) {
    paymentSessionPending(id: $id, pendingExpiresAt: $pendingExpiresAt, reason: $reason) {
      paymentSession {
        id
        state {
          ... on PaymentSessionStatePending {
            code
            reason
          }
        }
        nextAction {
          action
          context {
            ... on PaymentSessionActionsRedirect {
              redirectUrl
            }
          }
        }
        authorizationExpiresAt
        pendingExpiresAt
      }
      userErrors{
        field
        message
      }
    }
  }
`


const refundSessionResolve = `
  mutation RefundSessionResolve($id: ID!) {
    refundSessionResolve(id: $id) {
      refundSession {
        id
        state {
          ... on RefundSessionStateResolved {
            code
          }
        }
      }
      userErrors{
        field
        message
      }
    }
  }
`;


const refundSessionReject = `
  mutation RefundSessionReject($id: ID!, $reason: RefundSessionRejectionReasonInput!) {
    refundSessionReject(id: $id, reason: $reason) {
      refundSession {
        id
        state {
          ... on RefundSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const captureSessionResolve = `
  mutation CaptureSessionResolve($id: ID!) {
    captureSessionResolve(id: $id) {
      captureSession {
        id
        state {
          ... on CaptureSessionStateResolved {
            code
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const captureSessionReject = `
  mutation CaptureSessionReject($id: ID!, $reason: CaptureSessionRejectionReasonInput!) {
    captureSessionReject(id: $id, reason: $reason) {
      captureSession {
        id
        state {
          ... on CaptureSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const voidSessionResolve = `
  mutation VoidSessionResolve($id: ID!) {
    voidSessionResolve(id: $id) {
      voidSession {
        id
        state {
          ... on VoidSessionStateResolved {
            code
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


const voidSessionReject = `
  mutation VoidSessionReject($id: ID!, $reason: VoidSessionRejectionReasonInput!) {
    voidSessionReject(id: $id, reason: $reason) {
      voidSession {
        id
        state {
          ... on VoidSessionStateRejected {
            code
            reason
            merchantMessage
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;


export default {
  paymentsAppConfigure,
  paymentSessionResolve,
  paymentSessionReject,
  refundSessionResolve,
  refundSessionReject,
  captureSessionResolve,
  captureSessionReject,
  voidSessionResolve,
  voidSessionReject,
  paymentSessionPending,
};
```

## /app/routes/app.void\_session.jsx

```jsx
import { json } from "@remix-run/node";


import { createVoidSession } from "~/payments.repository";


/**
 * Saves and starts a void session.
 */
export const action = async ({ request }) => {
  const requestBody = await request.json();


  const voidSessionHash = createParams(requestBody);
  const voidSession = await createVoidSession(voidSessionHash);


  if (!voidSession) throw new Response("A VoidSession couldn't be created.", { status: 500 });


  return json(voidSessionHash);
}


const createParams = ({id, gid, payment_id, proposed_at}) => (
  {
    id,
    gid,
    paymentId: payment_id,
    proposedAt: proposed_at,
  }
)
```

## Tutorial complete!

Congratulations! You set up an alternative payments extension.

### Next steps

[Onboard a merchant\
\
](https://shopify.dev/docs/apps/build/payments/onboard-a-merchant-payments-extension)

[Before a merchant can use your payments extension, they need to go through an onboarding experience.](https://shopify.dev/docs/apps/build/payments/onboard-a-merchant-payments-extension)

[This guide describes how a merchant discovers, installs, authorizes, and activates a payments extension.](https://shopify.dev/docs/apps/build/payments/onboard-a-merchant-payments-extension)

[Payments Apps API\
\
](https://shopify.dev/docs/api/payments-apps)

[After you've completed this tutorial to understand how Payments apps work, you can use the Payments Apps API to programmatically access your payments app's configuration data.](https://shopify.dev/docs/api/payments-apps)

[You can also use the Payments Apps API to resolve, pend, or reject payments sessions.](https://shopify.dev/docs/api/payments-apps)
