Skip to content
Saleor
Esc
↑↓navigate↵open⌘Jpreview
On this page

Square App Storefront Integration

This guide shows how to connect the Square Web Payments SDK to Saleor checkout and pay with the Square App. It covers card payments, then explains what changes for wallets, gift cards, and saved cards.

Refer to the Square Web Payments SDK documentation for details on how to use the SDK.

Assumptions

  • This guide assumes a React-based UI. Follow the Square documentation for other frameworks.
  • For brevity, the code examples don’t handle errors and omit non-essential code. A real storefront needs more code for loading states, errors, and notifications.
  • The GraphQL client is request.

Prerequisites

  • Install and configure the Square App in the Saleor Dashboard
  • Assign a Square connection to your channel and set the channel’s transaction flow to CHARGE
  • Set up the Square webhook

To initialize the payment, you should also have a flow that:

  • Creates a checkout
  • Sets the required fields, like addresses and the shipping method
  • Has the final amount to pay calculated

In this guide, you will render the card form with a working PAY button.

Initialize gateway

First, fetch the browser-safe Square configuration from the app.

Prepare the following GraphQL mutation:

mutation InitializeSquareGateway($checkoutId: ID!, $amount: PositiveDecimal!) {
  paymentGatewayInitialize(
    id: $checkoutId
    paymentGateways: { id: "saleor.app.square" }
    amount: $amount
  ) {
    gatewayConfigs {
      id
      data
      errors {
        field
        message
        code
      }
    }
    errors {
      field
      message
      code
    }
  }
}

If the app is configured, you receive a response like this:

{
  "data": {
    "paymentGatewayInitialize": {
      "gatewayConfigs": [
        {
          "id": "saleor.app.square",
          "data": {
            "square": {
              "environment": "sandbox",
              "applicationId": "sandbox-sq0idb-...",
              "locationId": "L...",
              "countryCode": "US"
            },
            "captureMode": "CHARGE",
            "methods": ["card", "gift-card", "apple-pay", "cash-app-pay", "afterpay-clearpay", "saved-card"]
          },
          "errors": []
        }
      ],
      "errors": []
    }
  }
}

Only render the methods listed in methods.

Load the Web Payments SDK script that matches environment:

  • sandbox: https://sandbox.web.squarecdn.com/v1/square.js
  • production: https://web.squarecdn.com/v1/square.js

Then create the card form:

import { useEffect, useRef, useState } from "react";

const SQUARE_SDK_URLS = {
  sandbox: "https://sandbox.web.squarecdn.com/v1/square.js",
  production: "https://web.squarecdn.com/v1/square.js",
};

const loadSquareSdk = (environment: "sandbox" | "production") =>
  new Promise<void>((resolve, reject) => {
    const script = document.createElement("script");

    script.src = SQUARE_SDK_URLS[environment];
    script.onload = () => resolve();
    script.onerror = reject;
    document.head.appendChild(script);
  });

export const SquareCardForm = ({ squareConfig }) => {
  const cardRef = useRef(null);
  const [isReady, setIsReady] = useState(false);

  useEffect(() => {
    const init = async () => {
      await loadSquareSdk(squareConfig.environment);

      const payments = window.Square.payments(squareConfig.applicationId, squareConfig.locationId);
      const card = await payments.card();

      await card.attach("#square-card");
      cardRef.current = card;
      setIsReady(true);
    };

    void init();

    return () => {
      void cardRef.current?.destroy();
    };
  }, [squareConfig]);

  const handleSubmit = async (event) => {
    event.preventDefault();
    // We will implement this in the next step
  };

  return (
    <form onSubmit={handleSubmit}>
      <div id="square-card" />
      <button type="submit" disabled={!isReady}>
        PAY
      </button>
    </form>
  );
};

At this point, you should see:

  • The Square card form rendered
  • The PAY button visible, but doing nothing when clicked

Submitting payment

Next, tokenize the card and pass the token to Saleor. Prepare the following mutation:

mutation InitializeTransaction(
  $checkoutId: ID!
  $amount: PositiveDecimal!
  $idempotencyKey: String!
  $data: JSON
) {
  transactionInitialize(
    id: $checkoutId
    amount: $amount
    idempotencyKey: $idempotencyKey
    paymentGateway: { id: "saleor.app.square", data: $data }
  ) {
    transaction {
      id
    }
    transactionEvent {
      type
      message
    }
    data
    errors {
      field
      message
      code
    }
  }
}

Assume the mutation is assigned to the transactionInitializeDocument variable.

Now implement handleSubmit:

import request from "my-graphql-client";
import { transactionInitializeDocument } from "my-graphql-queries";

const handleSubmit = async (event) => {
  event.preventDefault();

  // Tokenize the card. Passing verification details lets Square run
  // buyer verification (3-D Secure) when the card issuer requires it.
  const tokenResult = await cardRef.current.tokenize({
    amount: checkoutTotal.toFixed(2),
    currencyCode: checkoutCurrency,
    intent: "CHARGE",
    customerInitiated: true,
    sellerKeyedIn: false,
    billingContact: {
      givenName: billingAddress.firstName,
      familyName: billingAddress.lastName,
      email: checkoutEmail,
      countryCode: billingAddress.country.code,
    },
  });

  if (tokenResult.status !== "OK") {
    // Show tokenResult.errors to the customer
    return;
  }

  // Keep the same key when retrying this payment attempt.
  // Generate a new one only when the customer starts a new attempt.
  const idempotencyKey = getOrCreateIdempotencyKey(checkoutId);

  const result = await request(transactionInitializeDocument, {
    checkoutId,
    amount: checkoutTotal,
    idempotencyKey,
    data: {
      version: "saleor.app.square.v1",
      method: "card",
      sourceId: tokenResult.token,
    },
  });

  const { transactionEvent, data } = result.transactionInitialize;

  // Handle the result, see below
};

The data field is plain JSON, so it isn’t type-checked. The app returns an error if it’s invalid. See webhooks for all shapes.

Handling the result

Unlike some other payment providers, Square charges the payment during transactionInitialize. There’s no separate confirmation step and you don’t need to call transactionProcess.

Check transactionEvent.type:

Type What to do
CHARGE_SUCCESS Payment is complete. Complete the checkout.
CHARGE_REQUEST Square accepted the payment but hasn’t completed it yet. Wait and re-read the checkout before completing it.
CHARGE_ACTION_REQUIRED The outcome is unknown (data.square.pending is true). Retry with the same idempotency key, or wait. Don’t let the customer start a new payment.
CHARGE_FAILURE Show the error from data.square.errors and let the customer try again with a new idempotency key.
AUTHORIZATION_SUCCESS A gift card covered part of the total. Collect the remaining amount, see gift cards.

Completing the checkout

When the checkout is fully paid, call checkoutComplete to create the order:

mutation CompleteCheckout($checkoutId: ID!) {
  checkoutComplete(id: $checkoutId) {
    order {
      id
    }
    errors {
      field
      message
      code
    }
  }
}

If no errors are returned, the checkout is completed and the order is created.

Paying with wallets

Apple Pay, Cash App Pay, and Afterpay/Clearpay use a Square payment request built from the checkout total and the location’s countryCode:

const paymentRequest = payments.paymentRequest({
  countryCode: squareConfig.countryCode,
  currencyCode: checkoutCurrency,
  total: { amount: checkoutTotal.toFixed(2), label: "Total" },
});

Then create the method with payments.applePay(paymentRequest), payments.cashAppPay(paymentRequest, options), or payments.afterpayClearpay(paymentRequest), tokenize it, and call transactionInitialize with the matching method:

{
  "version": "saleor.app.square.v1",
  "method": "apple-pay",
  "sourceId": "<token>",
  "verificationToken": "<token from payments.verifyBuyer()>"
}

Each wallet has its own requirements, for example Apple Pay domain verification and calling tokenize() directly from the click handler. Cash App Pay returns its token in the ontokenization event instead of from tokenize(). Follow the Square guides for Apple Pay, Cash App Pay, and Afterpay/Clearpay.

Paying with a Square gift card

Create the form with payments.giftCard(), tokenize it, and call transactionInitialize with method: "gift-card". Pass the full amount due: the app charges up to the gift card balance.

  • If the gift card covers the whole amount, the result is CHARGE_SUCCESS.
  • If it doesn’t, the result is AUTHORIZATION_SUCCESS and data.square.remainingAmount holds the amount left to pay.

To pay the rest, start a new transactionInitialize with a new idempotency key, the remaining amount, and another method, and set giftCardRemainder: true:

{
  "version": "saleor.app.square.v1",
  "method": "card",
  "sourceId": "<token>",
  "giftCardRemainder": true
}

When the remainder succeeds, the app charges both payments together and both transactions become CHARGE_SUCCESS. Re-read the checkout before calling checkoutComplete.

Square cancels the gift card authorization if the remainder isn’t paid within 30 minutes. Show the customer how much time they have left. Only one gift card can be used per checkout.

Saving cards

Saved cards require a logged-in customer. The storefront uses the Saleor stored payment methods API:

  1. Call paymentGatewayInitializeTokenization with the saleor.app.square gateway to get the Square configuration.

  2. Render the card form and tokenize it with intent: "STORE" in the verification details.

  3. Call paymentMethodInitializeTokenization with the following data:

    {
      "version": "saleor.app.square.v1",
      "method": "card",
      "sourceId": "<token>"
    }
  4. List saved cards with the storedPaymentMethods field on the checkout or user.

To pay with a saved card, call transactionInitialize with its ID:

{
  "version": "saleor.app.square.v1",
  "method": "saved-card",
  "paymentMethodId": "<storedPaymentMethods.id>"
}

You don’t need to tokenize anything when paying with a saved card.

Idempotency key

Both Saleor and Square support idempotency to ensure the same payment isn’t executed more than once.

Saleor generates an idempotency key if you don’t pass one, and the app derives the Square key from it. We recommend passing your own key to the mutation, so you can retry the same payment attempt after a network error without charging the customer twice.

Last updated on November 2, 2026

Was this page helpful?