Skip to main content

Sessions and events

Learn how to create payment request sessions, attach event listeners, and handle the payment lifecycle with the Shop Pay Wallet API.


Use the shopId and clientId from your onboarding email to configure the API. For the full list of options, see configure parameters.

window.ShopPay.PaymentRequest.configure({
shopId: 1,
clientId: "[REPLACE-ME]",
});

Create a session to make a payment request. shop_id (integer) can be retrieved from the shop object in the Admin API.

PaymentRequest fields are defined here.

const initialPaymentRequest = window.ShopPay.PaymentRequest.build({
lineItems: [
{
label: "T-Shirt",
originalItemPrice: {
amount: 10.00,
currencyCode: "USD"
},
itemDiscounts: [
{
label: "10% off",
amount: {
amount: 1.00,
currencyCode: "USD"
}
}
],
finalItemPrice: {
amount: 9.00,
currencyCode: "USD"
},
quantity: 2,
sku: "t-shirt",
requiresShipping: true,
originalLinePrice: {
amount: 20.00,
currencyCode: "USD"
},
lineDiscounts: [
{
label: "10% off",
amount: {
amount: 2.00,
currencyCode: "USD"
}
}
],
finalLinePrice: {
amount: 18.00,
currencyCode: "USD"
},
}
],
discountCodes: [],
deliveryMethods: [],
subtotal: {
amount: 18.00,
currencyCode: "USD"
},
totalTax: {
amount: 1.25,
currencyCode: "USD"
},
total: {
amount: 19.25,
currencyCode: "USD"
},
presentmentCurrency: "USD",
locale: 'en',
});

const session = window.ShopPay.PaymentRequest.createSession({
paymentRequest: initialPaymentRequest
});

Anchor to Attach event listenersAttach event listeners

Use ShopPayPaymentRequestSessionCreate on your server to create a session.

session.addEventListener("sessionrequested", (ev) => {
// Shop Pay Payment Request Session on your server
const response = fetch('/replace_with_your_endpoint', {
method: 'POST',
body: JSON.stringify({
payment_request: initialPaymentRequest
}),
headers: {
'Content-Type': 'application/json',
},
}).then(response => response.json()).then(data => {
const {token, checkoutUrl, sourceIdentifier} = data;
// optionally update the payment request if it has changed since it was created
const updatedPaymentRequest = window.ShopPay.PaymentRequest.build({YOUR_UPDATED_PAYMENT_REQUEST});
session.completeSessionRequest({token, checkoutUrl, sourceIdentifier, updatedPaymentRequest});
});
});

Listen to events that may change calculations such as when a delivery method type, shipping address, delivery method, pickup location, pickup location filter, or discount code changes. Recalculate the payment request and update the session.

session.addEventListener("deliverymethodtypechanged", async (ev) => {
const currentPaymentRequest = session.paymentRequest;
const deliveryMethodType = ev.deliveryMethodType;

let pickupLocations = [];
if (deliveryMethodType === 'PICKUP') {
pickupLocations = await fetchPickupLocations();
}

// Update the payment request based on the delivery method type change
const updatedPaymentRequest = window.ShopPay.PaymentRequest.build({
...currentPaymentRequest,
pickupLocations,
});

session.completeDeliveryMethodTypeChange({ updatedPaymentRequest: updatedPaymentRequest });
});
session.addEventListener("shippingaddresschanged", async (ev) => {
const currentPaymentRequest = session.paymentRequest;
const selectedAddress = ev.shippingAddress;

// Update the payment request based on the shipping address change
const updatedPaymentRequest = window.ShopPay.PaymentRequest.build({
...currentPaymentRequest,
deliveryMethods: [
{
label: "Standard",
amount: {
amount: 10.00,
currencyCode: "USD"
},
code: "STANDARD",
minDeliveryDate: '2024-01-01',
maxDeliveryDate: '2027-01-01',
},
{
label: "Express",
amount: {
amount: 20.00,
currencyCode: "USD"
},
code: "EXPRESS",
minDeliveryDate: '2024-01-01',
maxDeliveryDate: '2026-01-01',
}
]
});

session.completeShippingAddressChange({ updatedPaymentRequest: updatedPaymentRequest });
});

If you can't ship to the selected address, then return a shippingAddressError. Refer to Handle errors.

session.addEventListener("deliverymethodchanged", async (ev) => {
const currentPaymentRequest = session.paymentRequest;
const selectedDeliveryMethod = ev.deliveryMethod;

let updatedRequestValues;

if (selectedDeliveryMethod) {
updatedRequestValues = {shippingLines: [{
label: selectedDeliveryMethod.label,
amount: selectedDeliveryMethod.amount,
code: selectedDeliveryMethod.code
}],
totalShippingPrice: {
finalTotal: {
amount: selectedDeliveryMethod.amount.amount,
currencyCode: "USD",
},
},
total: {
amount: 20 + selectedDeliveryMethod.amount.amount,
currencyCode: "USD"
}}
} else {
updatedRequestValues= {total: {
amount: 20,
currencyCode: "USD"
}}
}

// Update the payment request based on the delivery method change
// and update the totals accordingly
const updatedPaymentRequest = window.ShopPay.PaymentRequest.build({
...currentPaymentRequest,
...updatedRequestValues,
});

session.completeDeliveryMethodChange({ updatedPaymentRequest: updatedPaymentRequest });
});
session.addEventListener("pickuplocationchanged", async (ev) => {
const currentPaymentRequest = session.paymentRequest;
const pickupLocation = ev.pickupLocation;

// Update the payment request based on the pickup location change
// and update the totals accordingly
const updatedPaymentRequest = window.ShopPay.PaymentRequest.build({
...currentPaymentRequest,
totalShippingPrice: {
finalTotal: {
amount: pickupLocation.amount.amount,
currencyCode: "USD",
},
},
total: {
amount: 20 + pickupLocation.amount.amount,
currencyCode: "USD"
},
});

session.completePickupLocationChange({ updatedPaymentRequest: updatedPaymentRequest });
});
session.addEventListener("pickuplocationfilterchanged", async (ev) => {
const currentPaymentRequest = session.paymentRequest;
const buyerLocation = ev.buyerLocation;

// Update the payment request based on the pickup location filter change
// by filtering the available pickup locations based on a customer's location
const updatedPaymentRequest = window.ShopPay.PaymentRequest.build({
...currentPaymentRequest,
pickupLocations: [{
label: "620 King Street West",
code: "PICK_UP-KING-STREET-WEST",
detail: "620 King Street West, Toronto, ON",
amount: {
amount: 10.00,
currencyCode: "USD"
},
readyExpectationLabel: "Ready in 1 hour",
proximityLabel: "Less than 1 km away",
}]
});

session.completePickupLocationFilterChange({ updatedPaymentRequest: updatedPaymentRequest });
});
session.addEventListener("discountcodechanged", async (ev) => {
const currentPaymentRequest = session.paymentRequest;
const selectedDiscountCodes = ev.discountCodes; // Array of discount codes ["example-code-1"]

// Update the payment request based on the discount code change
// Let's assume the discount code is valid and the discount is 15% off
const updatedPaymentRequest = window.ShopPay.PaymentRequest.build({
...currentPaymentRequest,
discountCodes: selectedDiscountCodes,
lineItems: [
{
label: "T-Shirt",
finalItemPrice: {
amount: 10.00,
currencyCode: "USD"
},
quantity: 2,
sku: "t-shirt",
requiresShipping: true,
finalLinePrice: {
amount: 20.00,
currencyCode: "USD"
},
}
],
subtotal: {
amount: 20.00,
currencyCode: "USD"
},
discounts: [
{
label: "example-code-1",
amount: {
amount: 3.00, // Discounts must be passed to Shopify as a positive value
currencyCode: "USD"
}
}
],
totalTax: {
amount: 1.06,
currencyCode: "USD"
},
total: {
amount: 18.06,
currencyCode: "USD"
}
});

session.completeDiscountCodeChange({ updatedPaymentRequest: updatedPaymentRequest });
});

Confirm the payment once the user clicks the Pay now button in the Shop Pay popup.

The server confirmation must invoke the ShopPayPaymentRequestSessionSubmit mutation to confirm that the payment is to be processed. Use ShopPayPaymentRequestSessionSubmit on your server to submit the session.

session.addEventListener("paymentconfirmationrequested", async (ev) => {
// The customer's billing address contains relevant contact details such as email & phone number (if available)
const billingAddress = ev.billingAddress;

// Before submitting the payment request for processing, a final check should be done on your server
// to make sure the payment request (total price, inventory available, etc.) is still valid.
const response = fetch('/replace_with_your_endpoint', {
method: 'POST',
body: JSON.stringify({
token: session.token,
payment_request: session.paymentRequest
}),
headers: {
'Content-Type': 'application/json',
},
}).then(response => response.json()).then(data => {
if (data.errors) {
// Handle errors here.
// For example: if an item is no longer in stock, you can send a new paymentRequest without that lineItem by building a new payment request and including it in completePaymentConfirmationRequest along with the errors.
session.completePaymentConfirmationRequest({
errors: [
{
"type": "generalError",
"message": "Something went wrong. Please try again."
}
// Optionally build an updated paymentRequest and include it here
]
})
} else {
// confirm the payment request is processing
session.completePaymentConfirmationRequest();
}
});
});

This event is dispatched when the payment is complete. Close the Shop Pay popup and redirect the user to the order confirmation page.

session.addEventListener("paymentcomplete", async (ev) => {
console.log(ev.processingStatus.status);

session.close(); // close the Shop Pay popup
window.location.href = "/thank-you";
});

This event is dispatched when a payment attempt fails. The event contains information about why the payment failed.

session.addEventListener("paymentattemptfailed", async (ev) => {
const { reason, errorCode } = ev.error;
console.log(`Payment attempt failed: ${reason}`);
console.log(`Error code: ${errorCode}`);
});
Caution

Buyers have the option to select a different payment method to retry their checkout after a failure. Don't call session.close() so that users can retry with a new payment method without disrupting their session.

This event is dispatched when the checkout window is closed.

session.addEventListener("windowclosed", async () => {
// handle window closed event
});
Warning

Only call the corresponding complete call once for each event.


Every complete call accepts an optional array of ShopPayUserError objects. Use it to tell the buyer what went wrong without ending the session. The type of each error controls where its message appears in the Shop Pay dialog:

  • generalError: Displayed at the top of the checkout.
  • discountCodeError: Displayed near the discount code field.
  • shippingAddressError: Displayed near the shipping address selection.

Always set a message. A shippingAddressError or generalError without one isn't displayed to the buyer, and a discountCodeError without one falls back to an unlocalized "Invalid discount code".

You can return errors on their own, or alongside an updatedPaymentRequest. If you omit the updated payment request, then the current one continues to be used, including any values that the error invalidates.

Anchor to Reject a shipping addressReject a shipping address

Return a shippingAddressError when you can't ship to the selected address. Also return an updated payment request with an empty deliveryMethods array, so that the rates calculated for the previous address aren't offered for this one:

session.addEventListener("shippingaddresschanged", async (ev) => {
const currentPaymentRequest = session.paymentRequest;
const shippingAddress = ev.shippingAddress;

// Replace this with your own check for whether you ship to the address
const canShipToAddress = shippingAddress.countryCode === "US";

if (!canShipToAddress) {
const updatedPaymentRequest = window.ShopPay.PaymentRequest.build({
...currentPaymentRequest,
deliveryMethods: [],
});

session.completeShippingAddressChange({
updatedPaymentRequest: updatedPaymentRequest,
errors: [
{
type: "shippingAddressError",
message: "We don't ship to this address. Select a different address.",
}
]
});
return;
}

// Otherwise, complete the change with the delivery methods for this address,
// as shown in Attach event listeners
});

Anchor to Reject a discount codeReject a discount code

Return a discountCodeError when a code is invalid or expired. Set discountCodes on the updated payment request to only the codes that you accepted, because Shop Pay treats the codes you return as the applied set:

session.addEventListener("discountcodechanged", async (ev) => {
const currentPaymentRequest = session.paymentRequest;
const enteredDiscountCodes = ev.discountCodes;

// Replace this with your own validation
const validDiscountCodes = enteredDiscountCodes.filter((code) => code === "10OFF");
const hasRejectedCode = validDiscountCodes.length < enteredDiscountCodes.length;

const updatedPaymentRequest = window.ShopPay.PaymentRequest.build({
...currentPaymentRequest,
discountCodes: validDiscountCodes,
// Recalculated discounts, subtotal, totalTax, and total go here
});

session.completeDiscountCodeChange({
updatedPaymentRequest: updatedPaymentRequest,
errors: hasRejectedCode
? [
{
type: "discountCodeError",
message: "That discount code isn't valid.",
}
]
: []
});
});

Anchor to Show a general errorShow a general error

Use generalError for a failure that isn't tied to the discount code field or the shipping address, such as a call to your server that fails. Omit updatedPaymentRequest to leave the current one in place:

session.addEventListener("deliverymethodchanged", async (ev) => {
try {
// Recalculate the totals for ev.deliveryMethod, then complete the change
// as shown in Attach event listeners
} catch (error) {
session.completeDeliveryMethodChange({
errors: [
{
type: "generalError",
message: "We couldn't update the shipping rates. Try again.",
}
]
});
}
});

The same pattern applies to a validation failure at payment confirmation, as shown in the paymentconfirmationrequested example. If you pass an updatedPaymentRequest to completePaymentConfirmationRequest(), then you must also pass at least one error, otherwise the update is ignored.


Was this page helpful?