Skip to main content

Build a redeemables payment extension

Beta

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

Plus

This payment method is only available to be installed by Shopify Plus plans.

Redeemables payments extension enable merchants to integrate their external gift card provider as a payment method at checkout. Your payment extension enables customers to apply gift cards directly on the checkout page, without having to redirect them to an offsite page.

Your payments extension will provide a Checkout UI extension and Balance Request endpoint to enable Shopify to check the balance of gift cards during checkout.

Example gift card payment method

Anchor to Step 1: Scaffold an appStep 1: Scaffold an app

Tip

Most of the steps outlined in this document serve as an extension of the offsite payments extension tutorial.

To build a redeemables payments extension you will need to create a new app, with a redeemables payments extension and checkout UI extension correctly applied.

Create your new app using the Shopify CLI.

Tip

Ensure you have the latest version of Shopify CLI installed to access all payment extension features.


Anchor to Step 2: Create a checkout UI extensionStep 2: Create a checkout UI extension

After creating your app, generate a checkout UI extension and deploy your app to Shopify. This extension will be used to collect gift card details from buyers directly on checkout.

  1. Use the Shopify CLI to scaffold a checkout UI extension for your app.

  2. Name your extension

  3. Choose to work in TypeScript Preact.

    Note

    Refer to the sample extension, written in TypeScript Preact, and ensure that you use the extension target and APIs as demonstrated.

  4. After you generate the extension, deploy your app. This enables you to link the checkout UI extension with your payments app extension in the next section.


Anchor to Step 3: Implement the checkout UI extensionStep 3: Implement the checkout UI extension

Your UI extension must use the purchase.checkout.gift-card.render extension target. This ensures that your UI renders in the correct location for gift card payments and has access to the applyRedeemableChange API method to apply a gift card to a checkout.

Note

The purchase.checkout.gift-card.render extension target can only be used for payments apps that process gift cards. You must specify the extension point your are rendering to (purchase.checkout.gift-card.render) in the shopify.extension.toml file in your extension directory.

applyRedeemableChange is a new API method which can be pulled from the useApplyRedeemableChange hook on @shopify/ui-extensions/checkout/preact.

Anchor to [object Object]applyRedeemableChange

This method applies a redeemable (i.e. gift card) to the checkout your extension is rendered on. This method triggers a Fetch Balance request explained later.

The attributes provided to this method should mirror the definitions in the UI Extension Field Definitions configuration on your payments app extension. If the attributes provided don't match your field definitions, the call will fail and an error will be returned.

Example applyRedeemableChange payload:

{
type: 'redeemableAddChange'
attributes: [
{ 'key': 'card_number', 'value': '123456789' },
{ 'key': 'pin', 'value': 1234 }
],
identifier: '123456789'
}

Example applyRedeemableChange usage:

import {
useApplyRedeemableChange
} from '@shopify/ui-extensions/checkout/preact';

function Extension() {

const applyRedeemableChange = useApplyRedeemableChange();

async function handleSubmit() {
const change = {
type: 'redeemableAddChange'
attributes: [
{ 'key': 'card_number', 'value': '123456789' },
{ 'key': 'pin', 'value': 1234 }
],
identifier: '123456789'
}

const result = await applyRedeemableChange(change);
}

// ...
}

When applyRedeemableChange successfully completes, the following will be returned:

{ type: 'success' }

A number of errors may rise when applyRedeemableChange is run and fails. The output would look like:

{
type: 'error',
message: 'Could not apply redeemable'
}

message is human-readable, and can be any of:

  • Could not apply redeemable
  • Access denied: the extension does not have the required approval scopes
  • Invalid RedeemableChange type
  • Could not apply redeemable change: the buyer journey is completed

These messages are for debugging and should not be displayed to the buyer. Shopify owns displaying errors to buyers within checkout.

Refer to the Sample checkout UI extension for an example implementation of what your gift card form will look like.

While implementing the UI extension you may want to consider the following:

  • Localization - The labels and visible text displayed to the buyer should be localized. We provide the useTranslate hook through the StandardAPI for this purpose.
  • Field validation - Before making the applyRedeemableChange request, it is advisable to validate the gift card input fields. To present field validation messages, you can utilize the error property provided by the TextField component.
  • Loading, Error, and Success states - When the buyer submits a gift card, your UI extension needs to handle loading, error, and success states. To accomplish this:
    • Loading state: Display a loading spinner in the "Apply" button while the request is pending. To achieve this, you can use the Spinner component.
    • Success state: Reset the form fields to their initial state after successfully applying the gift card to the checkout.
    • Error state: Do not reset the form fields in case of an error. Instead, you may want to indicate which fields need to be corrected by utilizing field validation errors.
  • Responsive UI - Use StyleHelper to make your UI extension responsive to different viewport sizes, focus states, and hover states.

Anchor to Step 4: Create a payments extensionStep 4: Create a payments extension

Anchor to Generate the extensionGenerate the extension

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

Redeemables describes a payment method that can be redeemed. Examples are gift cards, store credit, etc.

Note

Shopify will need to enable the beta for your Shopify Partners account to see the redeemables payments extension. Shopify will also need to enable a beta for the shop you intend to test your payment method on.

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

    pnpm

    shopify app generate extension
  2. When prompted, choose your organization & create this as a new app
  3. When prompted for "Type of extension", select Payments extensions > Redeemable and name your extension

Anchor to Step 5: Configure your payments extensionStep 5: Configure your payments extension

Configuration of a redeemables payments extension is similar to a Offsite payments extension.

Your payments extension configures the following fields:

Property nameDescription
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.
supported_countries
required
The countries where your payments app is available. Refer to the list of ISO 3166 (alpha-2) country codes 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.
supported_payment_methods
required
The payment methods (for example, Visa) that are available with your payments app. Learn more.
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
required
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
required
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 can't use the unstable version of the API in production. API versions are updated in accordance with Shopify's general API versioning timelines.
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.
balance_url
required
The URL that balance requests are sent to.
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.
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.
Note

The balance_url, ui_extension_handle, and checkout_payment_method_fields are new app extension configurations, specifically for payments apps that process gift cards. You'll want to select the UI extension you created in Create a Checkout UI Extension as the value for the ui_extension_handle field.

The UI extension generated in Create a checkout UI extension will determine what fields, validation, and form submission behavior is presented to buyers during checkout.

This is where you would identify the checkout extension you built prior to creating your payment extension to tie them into one another.

Property nameDescription
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.

Anchor to UI Extension Field DefinitionsUI Extension Field Definitions

The fields you are looking to accept from your UI extensions so the payment method can validate the correct data is sent from the front end.

Property nameDescription
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

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

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

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

Image of a gift card payment method UI extension

Anchor to Step 6: Set up your payments appStep 6: Set up your payments app

Anchor to Update your app configurationUpdate 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 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.


Anchor to Step 7: Deploy your extensionStep 7: Deploy your extension

Create and release an app version with the 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

    shopify app deploy

    An app version created using Shopify CLI contains the following:

  • The app configuration from the local configuration file.

  • The local version of the app's extensions. 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 command, or through the Dev Dashboard.


Anchor to Step 8: Start your development serverStep 8: Start your development server

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

    Terminal

    shopify app dev

    In your terminal, select your dev store. You can use the generated URL to test your payments app by using it in your payments app configuration. 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. This brings you to your development store's admin.


Anchor to Step 9: Test your payments app with a storeStep 9: Test your payments app with a store

Once this version has been released, follow these steps to install your app on your dev store:

You need to set your app as ready to process payments by calling the paymentsAppConfigure mutation.

POST https://{shop_domain}/payments_apps/api/{api_version}/graphql.json

Mutation

mutation PaymentsAppConfigure($externalHandle: String, $ready: Boolean!) {
paymentsAppConfigure(externalHandle: $externalHandle, ready: $ready) {
userErrors{
field
message
}
}
}

There are two required arguments that you need to provide in the mutation:

  • externalHandle: A account name or identifier associated with the account that the merchant has used with the Partner.
  • ready: A signal that the provider is ready to process payments. Accepted values are true or false.
    • true: Allows your app to process payments.
    • false: Indicates your app isn't yet able to process payments.
Caution

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

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 on Shopify admin.

  2. Click Activate.


Anchor to Implement the payments appImplement the payments app

Payments apps for gift cards are implemented very similarly to the existing offsite payments apps. The key difference is that, for gift cards, payments apps have an operation that occurs prior to a checkout completing - fetching the balance of a gift card.

Anchor to Step 1: Fetch BalanceStep 1: Fetch Balance

The first step in a gift card payment is to retrieve the balance, so that it can be applied to a checkout. In order to do so, we require that you set up a Balance URL in the payments app extension. Shopify will make a request to that URL to retrieve the balance of a gift card during checkout. If the request fails or times out, it's retried several times.

Example request payload:

{
"id": "uuid",
"currency": "USD",
"test": false,
"merchant_locale": "en",
"payment_method": {
"type": "redeemable",
"data": {
"attributes": {
"card_number": "123456789",
"pin": 1234
}
}
}
}

Attribute Description Type
id Globally unique identifier for the fetch balance attempt. Used as the idempotency key. Ensures that repeated requests with the same ID are treated the same, preventing duplicate fetch balance sessions. String
currency Three-letter ISO 4217 currency code. For example USD or CAD. String
test Indicates whether the payment is in test or live mode. Boolean
merchant_localeIETF BCP 47 language tag representing the language used by the merchant. For example, en-US or fr-FR.String
payment_method Indicates the type and data relevant for the chosen payment method. Refer to payment_method hash for more information. Hash

Attribute Description Type
type Indicates the type of payment method. Currently only "redeemable" is supported. String
data Contains data relevant for the chosen payment method. Refer to payment_method.data hash for more information. Hash

Anchor to [object Object], hashpayment_method.data hash

Attribute Description Type
attributes Contains the gift card details associated with the buyer. For example, card_number and pin. These fields are defined by the UI Extension Field Definitions configuration in your payments extension. String

If the balance can be successfully fetched, send a HTTP 2xx response to the above request with a JSON body in the following format, including the balance:

{
"result": "success",
"balance": {
"value": "123.12",
"currency": "CAD"
}
}

The provided value for currency can be any ISO 4217 currency code (i.e. USD, CAD).

In the event a balance fetch fails, there are a number of predefined codes you can respond with to surface an error to buyers.

Error CodeDescription
REDEEMABLE_INVALIDThe redeemable's parameters are invalid. This is a generic error response.
REDEEMABLE_INSUFFICIENT_BALANCEThe redeemable has insufficient balance.
REDEEMABLE_EXPIREDThe redeemable has expired.
REDEEMABLE_DISABLEDThe redeemable was disabled.

Example failure response (HTTP 2xx):

{
'result': 'failure',
'reason': 'REDEEMABLE_INSUFFICIENT_BALANCE',
'message': 'The redeemable has insufficient funds.',
}

Anchor to Fetch balance sequence diagramFetch balance sequence diagram

Sequence diagram showing fetch balance flow
  1. The buyer enters their gift card details into the UI Extension.
  2. The UI extension, with the gift card details, calls applyRedeemableChange, available through @shopify/ui-extensions/checkout/preact.
  3. Shopify will call the payments app's endpoint located at their configured Balance URL, with a request to fetch the balance of the provided gift card. If this request times out or fails, it's retried several times.
  4. The payments app returns the balance for the gift card.
  5. Shopify applies the gift card to the buyer's checkout.
  6. The buyer is shown that the gift card has been applied to their checkout.

Anchor to Step 2: Process PaymentStep 2: Process Payment

Sequence diagram showing payment processing flow

Payments with payments apps are processed asynchronously. When the buyer completes their checkout, a request will be sent from Shopify to the Payment session URL defined in Configure your payments app extension, with the checkout and payment details. The Payments app should respond with HTTP 2xx to indicate that the payment session was started, and should begin processing the gift card payment at this point.

Example request body:

{
"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": "redeemable",
"data": {
"attributes" : {
"card_number": "123456789",
"pin": 1234
},
},
},
"proposed_at": "2020-07-13T00:00:00Z",
"customer": {
"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"
},
"email": "buyer@example.com",
"phone_number": "5555555555",
"locale": "fr"
},
"kind": "sale"
}

Details on each attribute can be found here.

The payment method data object in the payment session request body is defined by the aforementioned UI Extension Field Definitions configuration in your app extension, just like for Fetch Balance.

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 don't respond with an HTTP 2xx. Use an appropriate error status code instead.

Once the payments app has responded to the initial start payment session request, it should begin processing the payment. Since this is an asynchronous process, the payments app will be performing the next step independently, through the paymentSessionResolve mutation on the Payments Apps GraphQL API. This mutation will resolve the payment session, indicating that the payment was successful.

Example GraphQL mutation:

mutation PaymentSessionResolve($id: ID!) {
paymentSessionResolve(id: $id) {
paymentSession {
id
state {
... on PaymentSessionStateResolved {
code
}
}
}
userErrors {
field
message
}
}
}

With this input:

{ "id": "gid://shopify/PaymentSession/u0nwmSrNntjIWozmNslK5Tlq" }

Example JSON response:

{
"data": {
"paymentSessionResolve": {
"paymentSession": {
"id": "gid://shopify/PaymentSession/u0nwmSrNntjIWozmNslK5Tlq",
"state": {
"code": "RESOLVED"
}
},
"userErrors": []
}
}
}

After this, the payment will be marked as resolved in Shopify.

If a payment was unsuccessful for any reason, the payments app must use the paymentSessionReject mutation.

Example GraphQL mutation:

mutation PaymentSessionReject(
$id: ID!,
$reason: PaymentSessionRejectionReasonInput!
) {
paymentSessionReject(
id: $id,
reason: $reason
) {
paymentSession {
id
state {
... on PaymentSessionStateRejected {
code
reason
merchantMessage
}
}
}
userErrors {
field
message
}
}
}

With this input:

{
"id": "gid://shopify/PaymentSession/u0nwmSrNntjIWozmNslK5Tlq",
"reason": {
"code": "PROCESSING_ERROR",
"merchantMessage": "the payment didn't work"
}
}

Example JSON response:

{
"data": {
"paymentSessionReject": {
"paymentSession": {
"id": "gid://shopify/PaymentSession/u0nwmSrNntjIWozmNslK5Tlq",
"state": {
"code": "REJECTED",
"reason": "PROCESSING_ERROR",
"merchantMessage": "the payment didn't work"
},
},
"userErrors": []
}
}
}

There are some defined errors that you can reject a paymentSession with.

Error CodeDescription
PROCESSING_ERRORThere was an error processing the payment.

Refunds operate the same as they do with regular payments. Documentation can be found here.

Anchor to Optional: Authorizations, Captures, and VoidsOptional: Authorizations, Captures, and Voids

Authorizations, captures, and voids allow merchants to use your payment app to initiate separate transactions to authorization and capture payments. These will operate the same as they do with regular payment apps. More context about authorizations can be found here.

Merchants control whether or not they use "sale or "authorization" payment transactions by changing their Payment capture method payment setting to "Manual" or "Automatically when order is fulfilled".

For details on how to implement and respond to capture and void session requests see:


Anchor to Sample Checkout UI ExtensionSample Checkout UI Extension

Note

Update to the latest version of checkout UI extensions so you can use web components. The legacy checkout-ui-extensions-react package is no longer required.

The following example demonstrates a checkout UI extension for a redeemable payments app. It uses hooks, such as useApplyRedeemableChange from the Preact-specific package.

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

import '@shopify/ui-extensions/preact';
import {render} from "preact";
import { useApplyRedeemableChange } from '@shopify/ui-extensions/checkout/preact';
import { useState } from 'preact/hooks';

export default async () => {
render(<Extension />, document.body)
};

function Extension() {
const [cardNumber, setCardNumber] = useState("");
const [pin, setPin] = useState("");
const [loading, setLoading] = useState(false);

const applyRedeemableChange = useApplyRedeemableChange();

async function handleSubmit() {
setLoading(true);

/** @type {{type: 'redeemableAddChange', attributes: Array<{key: string, value: string}>, identifier: string}} */
const change = {
type: "redeemableAddChange",
attributes: [
{ key: "card_number", value: cardNumber },
{ key: "pin", value: pin },
],
identifier: cardNumber,
};

try {
const result = await applyRedeemableChange(change);

if (result.type === "success") {
setCardNumber("");
setPin("");
} else {
console.log("Error:", result.message);
}
} finally {
setLoading(false);
}
}

const isFormValid = cardNumber.length > 0 && pin.length > 0;

return (
<s-stack direction="block" gap="base">
<s-grid gridTemplateColumns="70% 30%" gap="base">
<s-text-field
label="Gift card number"
value={cardNumber}
onInput={(event) => setCardNumber(/** @type {any} */ (event.currentTarget).value)}
/>
<s-text-field
label="PIN"
value={pin}
onInput={(event) => setPin(/** @type {any} */ (event.currentTarget).value)}
/>
</s-grid>
<s-button
onClick={() => {
handleSubmit();
}}
accessibilityLabel="Apply gift card"
disabled={!isFormValid}
loading={loading}
inlineSize='fill'
>
Apply
</s-button>
</s-stack>
);
}

Congratulations! You set up a redeemable payments extension.


Was this page helpful?