Skip to main content

Build a credit card payments extension with a checkout UI extension

Beta

UI extensibility for payments extensions is currently in invite-only beta, and Shopify will need to enable the beta for your extension.

Checkout UI extensions enable Partners to define additional fields required for processing credit cards with a payments extension. Partners can then collect all essential information upfront, such as an installments payment plan, directly on the checkout page before a payment is initiated. This improvement simplifies the checkout process and offers a smoother buyer experience.

An image of example credit card payment method with a UI extension

Note

This document builds upon the credit card payments extension tutorial, which you may refer to as needed for additional guidance.

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

  • Create a checkout UI extension
  • Create a credit card payments extension with extensibility
  • Explore the payment, refund, void, reject and capture session flows, and how to implement them yourself


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

To build an extensible credit card payments extension, you will need to create a new credit card app or use an existing credit card app with extensibility features turned on by Shopify.

If you opt to create a new app, begin by using the Shopify CLI to set up your app. As a first step, we recommend deploying a skeleton app to establish a base for customization.


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

Caution

Your checkout UI extension should not request scopes that enable network access. For more details on the restrictions applicable to access scopes, please refer to the restrictions section.

After creating your app, generate a checkout UI extension and deploy your app to Shopify. This extension will be used to collect additional information that's required to process a payment.

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

  2. Name your extension and choose to work in TypeScript React.

    Note

    A sample extension, written in Typescript React, is shown below in the Sample Checkout UI Extension section. Please make sure to use the extension target and APIs as shown in the example.

  3. After you generate the extension, deploy your app to Shopify Partners. This will allow you to link the checkout UI extension with your payments app extension in the next section.

  4. Navigate to your app in Shopify Partners (Apps > Your App).


Anchor to Step 3: Create a payments extensionStep 3: 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 payments extension:

    Terminal

    npm shopify app generate extension
    yarn shopify app generate extension
    pnpm shopify app generate extension
  2. When prompted, choose your organization and create a new app.

  3. When prompted for Type of extension, select Payments App Extension > Credit Card and name your extension.


Anchor to Step 4: Configure your payments app extensionStep 4: Configure your payments app extension

Configuration of an extensible credit card payments app extension is similar to a Credit Card payments app extension.

Your payments app extension configures the following fields:

Field
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
required
The URL that capture session requests are sent to. This is only used if your payments app supports merchant manual capture.
void_session_url
required
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 3-D Secure authentication.
supported_countries
required
The countries where your payments app is available. Includes list of ISO 3166 (alpha-2) country codes where your app is available for merchants to install.
supports_moto
required
Enables Mail Order/Telephone Order (MOTO), allowing merchants to manually process transactions using a customer's credit card information. The moto attribute in payment method data is only available in API version 2024-07 and later.
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.
supports_installments
required
Enables installments
supports_deferred_payments
required
Enables deferred payments
merchant_label
required
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.
test_mode_available
required
Enables merchants using your payments app to test their setup by simulating transactions. To test your app on a development 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, closed beta
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.
encryption_certificate_fingerprint
required
The certificate that Shopify uses to generate the ephemeral key and encrypt the credit card information of the customer. Refer to manage encryption certificates section to learn more.
ui_extension_handle
required
The UI extension that will be used to render 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, as well as the data type, that restricts the input the buyer can provider.
Note

The UI Extension and UI Extension Field Definitions attributes are new app extension configurations. You'll want to select the UI extension you created in Create a Checkout UI Extension as the value for the UI Extension 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 link the checkout extension you previously built with your new payment app extension to tie them together.

Property nameDescription
ui_extension_handle
required
The UI extension that will be used to render 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

Specify the fields your UI extensions should collect to ensure the payment method validates and receives the correct data 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, as well as the data type, that restricts the input the buyer can provider.
[[extensions.checkout_payment_method_fields]]
key = "bank_name"
type = "string"
required = true

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

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

Create and release an app version with the deploy command.

  1. Navigate to your app directory.
  2. Run the following command:

    Terminal

    shopify app deploy

An app version created using Shopify CLI contains the following:

  • The app configuration from the local configuration file. If the include_config_on_deploy flag is not set or false, the configuration from the active app version will be used instead.

  • The local version of the app's CLI-managed 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.

Tip

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.


Anchor to Step 6: Preview payment extensions on a development storeStep 6: Preview payment extensions on a development store

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

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

  2. Select Ready > Unstable and click Submit.

  3. In the banner, click Return to Shopify.

  4. Enable test mode.

  5. Click Activate.

  6. You can select Resolve to complete the payment, or Reject to cancel and go back.


Anchor to Explore the payment processing flowsExplore the payment processing flows

The payments app functions similarly to the credit card payment method, allowing you to gather additional information from the buyer at the outset to facilitate payment processing. The primary distinction is that checkout UI extension data is included in the start_payment_session body.

Outlined below is a comprehensive diagram depicting the potential flow for processing an extensible credit card payment. It's important to note that the pending state is optional; you can directly proceed to either resolve or reject the payment if there is no need to place it in a pending state. For further details on processing credit card payments, please refer to this resource.

An image of credit card payment flow

Once we start the payment session with your payments app, that initiation will also contain the metadata in a shape similar to what was specified within the field definitions. A sample payment session payload of what is to be expected can be seen below.

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 extensible credit card payment at this point.

The new metadata we are passing through payment session would be contained within the payment_method request parameters under attributes:

Example payment method request parameters

"payment_method":{
"type":"credit_card"
"data":{
"attributes": [
{
"key": "payment_plan",
"value": "pay-in-full"
}
],
},

},

Anchor to Payment session with localized fieldsPayment session 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.

Note

Make sure you're using the 2024-07 version or higher in your payments app extension to access this feature.

Example transaction metadata for localized fields

"transaction_metadata": {
"localized_fields": [
{
"key": "shipping_credential",
"country_code": "BR",
"value": "06305371008"
}
]
},

After 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.

POST https://{shop}.myshopify.com/payments_apps/api/unstable/graphql.json

Mutation

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

Input variables

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

JSON response

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

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

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

POST https://{shop}.myshopify.com/payments_apps/api/unstable/graphql.json

Mutation

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

Input variables

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

JSON response

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

This section describes the reasons you can use to reject a payment 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. For more information about refund sessions and how to reject or resolve refund sessions, refer to Explore refund sessions.

Example request body:

Example request body

"request_params": {
"id": "reItEndH7tKB4sGkjronhdEgv",
"gid": "gid://shopify/RefundSession/reItEndH7tKB4sGkjronhdEgv",
"payment_id": "reItEndH7tKB4sGkjronhdEgv",
"amount": "39.90",
"currency": "CAD",
"merchant_locale": "en",
"proposed_at": "2024-04-22T17:10:03Z",
"test": true
}

After the app has successfully processed the refund request, it's resolved by using the refundSessionResolve mutation. The id argument corresponds to the gid of the refund.

Example GraphQL mutation:

POST https://{shop}.myshopify.com/payments_apps/api/unstable/graphql.json

Mutation

mutation refundSessionResolve($id: ID!) {
refundSessionResolve(id: $id) {
refundSession {
# RefundSession fields
}
userErrors {
field
message
}
}
}

Input variables

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

JSON response

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

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

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

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.

POST https://{shop}.myshopify.com/payments_apps/api/unstable/graphql.json

Mutation

mutation refundSessionReject(
$id: ID!,
$reason: RefundSessionRejectionReasonInput!
) {
refundSessionReject(
id: $id,
reason: $reason
) {
refundSession {
# RefundSession fields
}
userErrors {
field
message
}
}
}

Input variables

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

JSON response

{
"data": {
"refundSessionReject": {
"refundSession": {
"id": "gid://shopify/RefundSession/reItEndH7tKB4sGkjronhdEgv",
"state": {
"code": "REJECTED",
"reason": "PROCESSING_ERROR",
"merchantMessage": "the refund didn't work"
}
},
"userErrors": []
}
}
}

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 funds and then replies to Shopify's capture request.

The capture flow begins with an HTTP POST request sent from Shopify to the payments app's capture session URL. You can read more about capture sessions and how to reject or resolve capture sessions here.

Example request body:

Example request body

"request_params": {
"id": "reItEndH7tKB4sGkjronhdEgv",
"gid": "gid://shopify/CaptureSession/reItEndH7tKB4sGkjronhdEgv",
"payment_id": "reItEndH7tKB4sGkjronhdEgv",
"amount": "39.90",
"currency": "CAD",
"merchant_locale": "en",
"proposed_at": "2024-04-22T17:10:03Z",
"test": true
}

After the app has successfully processed the capture request from Shopify, it's resolved using the captureSessionResolve mutation on the Payments Apps GraphQL API.

Example GraphQL mutation:

POST https://{shop}.myshopify.com/payments_apps/api/unstable/graphql.json

Mutation

mutation captureSessionResolve($id: ID!) {
captureSessionResolve(id: $id) {
captureSession {
# CaptureSession fields
}
userErrors {
field
message
}
}
}

Input variables

{
"id": "gid://shopify/CaptureSession/rh60PS44WpmEgki4D6IK1Mu63"
}

JSON response

{
"data": {
"captureSessionResolve": {
"captureSession": {
"id": "gid://shopify/CaptureSession/rh60PS44WpmEgki4D6IK1Mu63",
"state": {
"code": "RESOLVED"
}
},
"userErrors": []
}
}
}

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

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.

Example GraphQL mutation:

POST https://{shop}.myshopify.com/payments_apps/api/unstable/graphql.json

Mutation

mutation captureSessionReject(
$id: ID!,
$reason: CaptureSessionRejectionReasonInput!
) {
captureSessionReject(
id: $id,
reason: $reason
) {
captureSession {
# CaptureSession fields
}
userErrors {
field
message
}
}
}

Input variables

{
"id": "gid://shopify/CaptureSession/reItEndH7tKB4sGkjronhdEgv",
"reason": {
"code": "AUTHORIZATION_EXPIRED",
"merchantMessage": "the authorization didn't work"
}
}

JSON response

{
"data": {
"captureSessionReject": {
"captureSession": {
"id": "gid://shopify/CaptureSession/reItEndH7tKB4sGkjronhdEgv",
"state": {
"code": "REJECTED",
"reason": "AUTHORIZATION_EXPIRED",
"merchantMessage": "the authorization didn't work"
}
},
"userErrors": []
}
}
}

A void can only be performed when the payment initiated by Shopify has a kind property with a value of authorization.

The void flow begins with an HTTP POST request sent from Shopify to the payments app's void session URL. You can read more about void sessions and how to reject or resolve void sessions here.

Example request body:

Example request body

"request_params": {
"id": "reItEndH7tKB4sGkjronhdEgv",
"gid": "gid://shopify/VoidSession/reItEndH7tKB4sGkjronhdEgv",
"payment_id": "reItEndH7tKB4sGkjronhdEgv",
"amount": "39.90",
"currency": "CAD",
"merchant_locale": "en",
"proposed_at": "2024-04-22T17:10:03Z",
"test": true
}

After the app has successfully processed the void request, it is resolved using the voidSessionResolve mutation on the Payments Apps GraphQL API.

Example GraphQL mutation:

POST https://{shop}.myshopify.com/payments_apps/api/unstable/graphql.json

Mutation

mutation voidSessionResolve($id: ID!) {
voidSessionResolve(id: $id) {
voidSession {
# VoidSession fields
}
userErrors {
field
message
}
}
}

Input variables

{
"id": "gid://shopify/VoidSession/rh60PS44WpmEgki4D6IK1Mu63"
}

JSON response

{
"data": {
"voidSessionResolve": {
"voidSession": {
"id": "gid://shopify/VoidSession/rh60PS44WpmEgki4D6IK1Mu63",
"state": {
"code": "RESOLVED"
}
},
"userErrors": []
}
}
}

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

If your app 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 attempt to resolve the void again.

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.

Example GraphQL mutation:

POST https://{shop}.myshopify.com/payments_apps/api/unstable/graphql.json

Mutation

mutation voidSessionReject(
$id: ID!,
$reason: VoidSessionRejectionReasonInput!
) {
voidSessionReject(
id: $id,
reason: $reason
) {
voidSession {
# VoidSession fields
}
userErrors {
field
message
}
}
}

Input variables

{
"id": "gid://shopify/VoidSession/reItEndH7tKB4sGkjronhdEgv",
"reason": {
"code": "PROCESSING_ERROR",
"merchantMessage": "the void didn't work"
}
}

JSON response

{
"data": {
"voidSessionReject": {
"voidSession": {
"id": "gid://shopify/VoidSession/reItEndH7tKB4sGkjronhdEgv",
"state": {
"code": "REJECTED",
"reason": "PROCESSING_ERROR",
"merchantMessage": "the void didn't work"
}
},
"userErrors": []
}
}
}

Anchor to Sample Checkout UI ExtensionSample Checkout UI Extension

Note

Make sure you're using the 2024-04 version or higher of checkout-ui-extensions-react in your package.json that contains the useApplyPaymentMethodAttributesChange and PaymentMethodAttributesUpdateChange types. Regenerate your lockfile with these versions, or manually upgrade @shopify/ui-extensions* using yarn/npm.

This sample code shows what checkout UI extension could look like for a credit card payments app. This code would go in the Checkout.tsx file of your checkout UI extension:

Sample Checkout UI Extension

import {
reactExtension,
Form,
Select,
useApplyPaymentMethodAttributesChange,
usePaymentMethodAttributeValues,
} from '@shopify/ui-extensions-react/checkout';

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

export default reactExtension(
'purchase.checkout.payment-option-item.hosted-fields.render-after',
() => <Extension />
);

const LOG_PREFIX = '[Checkout UI Extension]';

function Extension() {
const [paymentPlanValue] = usePaymentMethodAttributeValues(['payment_plan']);

const applyPaymentMethodAttributesChange = useApplyPaymentMethodAttributesChange();

function apply({paymentPlan = paymentPlanValue}) {
const change = {
type: 'updatePaymentMethodAttributes',
attributes: [{key: 'payment_plan', value: paymentPlan}],
} as PaymentMethodAttributesUpdateChange;

applyPaymentMethodAttributesChange(change)
.then((result) => {
console.log(`${LOG_PREFIX} Applied change`, change, result);
})
.catch((error) => {
console.error(`${LOG_PREFIX} Failed to apply`, change, error);
}
);
}

return (
<Form onSubmit={() => {}}>
<Select
label="Payment plan"
onChange={(newValue: string) => apply({ paymentPlan: newValue })}
options={[
{value: 'pay-in-full', label: 'Pay in full'},
{value: 'pay-half', label: 'Pay Half'},
]}
value={paymentPlanValue as string}
/>
</Form>
);
}
Note

You must also specify the extension point you are rendering to (you can use purchase.checkout.payment-option-item.hosted-fields.render-after) in the shopify.extension.toml file. The standardAPI is a good starting point to see what is available for your checkout extension. A list of available components can be found here.

Anchor to BIN Range ConfigurationBIN Range Configuration

This example exposes the BIN number entered by buyers to the checkout UI extension so the extension can make decisions about what to display. To get the BIN number entered for the credit card on checkout, the following code can be used:

BIN Range Example

const bankIdNumber = useSubscription(useApi().bankIdNumber);

  • App deployments from the Partner Dashboard only pickup checkout UI extensions if they have previously been deployed and released. If you want to use the Partner Dashboard UI to deploy new versions of your app, make sure you've previously deployed and released the corresponding checkout UI extension from the CLI first.

  • Payments extensions using checkout UI extensions should not request scopes enabling network access. This will result in the extension being rejected during review. Instead of fetching data using an external network call, consider storing the data using Metafields for your app & accessing the same later.

  • Companion app:

    • If access to order data is needed for any reason, a companion app may be used to request and gain access to the data from the merchant by requesting read_all_orders scope for that app. The only scopes that payments app extension should not request for are write_checkout_extension_payments and write_payment_sessions.
    • If the companion app is using web pixels, you will need to request access to the consent feature to ensure that the pixel fires consistently. Please ask partner manager to get you the access.

Congratulations! You set up a credit card payments extension with UI extensibility.


Was this page helpful?