Development Shopify

Shopify Checkout Extensions: A Developer's Guide to Checkout Extensibility

12 min read

Shopify's checkout used to be a black box. You could change colours, upload a logo, and that was about it. If you needed custom logic — quantity-based discounts, payment method filtering, post-purchase upsells — you either hacked around it with Script Editor or accepted the limitation. With the rollout of Checkout Extensibility, that has fundamentally changed. Shopify now gives developers a proper toolkit for customising every meaningful part of the checkout flow, and the old workarounds are being retired.

I have been building checkout extensions for Shopify Plus merchants since the APIs left beta. This is the guide I wish existed when I started — covering Checkout UI Extensions, Shopify Functions, post-purchase extensions, and how they all fit together in a real production environment.


Checkout UI Extensions: What They Are

Checkout UI Extensions let you inject custom React-based components into specific locations within Shopify's checkout. These are not iframes or external scripts — they run inside Shopify's sandbox using a restricted set of UI components from the @shopify/ui-extensions-react/checkout library. You get access to checkout state (cart lines, customer data, shipping methods) through hooks, and you render using Shopify's component primitives: Banner, BlockStack, Text, Button, and so on.

The key concept is extension points — also called targets. Each target corresponds to a specific location in the checkout where your component can render. Here is how they map to the actual checkout page.

Checkout extension targets (simplified)

Order Summary purchase.checkout.header.render-after
Your UI Extension: Trust badges, loyalty points display
Contact & Delivery purchase.checkout.contact.render-after
Your UI Extension: Gift message field, delivery instructions
Shipping Methods purchase.checkout.shipping-option-list.render-after
Your UI Extension: Estimated delivery dates, carbon offset toggle
Payment purchase.checkout.payment-method-list.render-after
Your UI Extension: Instalment calculator, payment trust signals
Dynamic Checkout Block purchase.checkout.block.render
Your UI Extension: Upsells, free shipping progress bar, countdown timer

Each target defines where your React component renders within the checkout. The checkout editor in Shopify admin lets merchants reorder and toggle extensions.

The sandbox model is deliberate. Shopify controls the DOM, the styling, and the overall layout. Your extension receives a constrained rendering surface and a set of APIs to read and write checkout state. This means you cannot break the checkout with a rogue script, but it also means you cannot do arbitrary DOM manipulation. If you have worked with Shopify theme app extensions, the mental model is similar — but the component library and available APIs are checkout-specific.

A pattern I use frequently is the purchase.checkout.block.render target combined with the checkout editor. This target creates a block that merchants can position anywhere in the checkout using the drag-and-drop editor in Shopify admin. It is the most flexible target because you are not locked to a specific checkout section — the merchant decides where your component lives.


Shopify Functions: Server-Side Logic at the Edge

While UI Extensions handle the visual layer, Shopify Functions handle the logic layer. Functions are lightweight WebAssembly modules that execute on Shopify's infrastructure during specific checkout events. They receive a JSON input describing the current cart or checkout state, run your logic, and return a JSON output that tells Shopify what to change.

The execution model is strict: functions must complete within 5ms, cannot make network calls, and have a 256KB memory limit. This rules out anything that requires external API calls or heavy computation, but it is perfectly suited for discount calculations, payment method filtering, delivery option customisation, and cart line transformations.

Shopify Functions execution pipeline

IN
Input (GraphQL)
cart.lines[] → product, quantity, price
cart.buyerIdentity → customer tags
cart.attribute → custom metafields
fn
Your Function (Wasm)
Execute in ≤ 5ms
No network access
256KB memory limit
OUT
Output (JSON)
discounts[] → amount, targets
operations[] → add, remove, merge
hide[] → payment/delivery methods
Discounts Product & order discounts
Payment Hide/reorder methods
Delivery Filter/rename options
Cart Transform Merge, expand, update

Functions receive structured input via a GraphQL query you define, execute your logic in WebAssembly, and return structured operations for Shopify to apply.

The function types I use most frequently are discount functions and cart transform functions. Discount functions replace what Script Editor used to do — tiered pricing, buy-X-get-Y, customer-tag-based discounts — but with proper API support and without the fragility of Ruby scripts running in a deprecated runtime. Cart transform functions are newer and more powerful: they let you merge cart lines into bundles, expand a single line item into multiple components, or rewrite prices at the cart level.

One thing that catches developers off guard is the input query. Each function defines a GraphQL query that specifies exactly what data it needs. Shopify executes this query and passes the result as the function's input. This is not a standard GraphQL endpoint you call — it is a declaration of your data requirements. Getting this query right is half the battle.


Script Editor vs Functions vs UI Extensions

If you have been working with Shopify for a few years, you likely have experience with Script Editor. Understanding how the new extensibility model replaces it is critical for planning migrations and new builds.

Capability
Script Editor
Functions
UI Extensions
Runtime
Ruby (deprecated)
Wasm (Rust/JS/AS)
React (sandboxed)
Execution
Server-side
Edge (Shopify infra)
Client-side (browser)
Primary Use
Discounts, shipping, payments
Discounts, shipping, payments, cart
Custom UI in checkout
Network Calls
No
No
Yes (fetch API)
Merchant Config
Code only
Metafields + admin UI
Checkout editor + settings
Status
Deprecated Aug 2025
Active (GA)
Active (GA)
Plan Required
Plus only
All plans (some Plus only)
Plus only

Script Editor was deprecated in August 2025. All existing scripts must be migrated to Functions before Shopify removes runtime support entirely.

The most important difference is maintainability. Script Editor scripts lived in the Shopify admin as raw Ruby code with no version control, no deployment pipeline, and no testing framework. Functions are proper application code — you write them locally, version-control them in Git, test them with the Shopify CLI, and deploy them as part of your app. This alone is reason enough to migrate, even before considering the performance and capability improvements.

The catch is that Functions cannot do everything Script Editor could. Script Editor had access to the full cart object and could mutate it freely. Functions operate through a more structured input/output contract with specific operation types. Some complex Script Editor patterns — particularly those involving deeply nested conditional discounts — need to be rethought rather than directly ported.


Post-Purchase Extensions

Post-purchase extensions occupy a unique position in the checkout flow. They render on the thank-you page immediately after payment but before the order confirmation. This is a high-conversion moment — the customer has already committed to purchasing, their payment details are on file, and they are in a buying mindset. A well-designed post-purchase upsell can add 5-15% to average order value without adding friction to the initial checkout.

Technically, post-purchase extensions use the same React-based sandbox as checkout UI extensions, but with a different set of targets and APIs. The key API is useExtensionApi (now useApi in the latest SDK), which gives you access to the completed order, the customer's stored payment method, and the ability to create additional charges. The customer accepts the upsell with a single tap — no re-entering payment details.

One implementation pattern I have found effective is combining post-purchase extensions with metafield-driven configuration. The upsell offers are stored as product metafields, so merchants can update which products are offered without touching code. The extension reads the metafield on the purchased product, fetches the upsell product data, and renders the offer. This keeps the extension generic and reusable across product catalogues.


How It All Fits Together

In practice, a well-built checkout customisation often combines multiple extension types. Here is the architecture I typically implement for Shopify Plus merchants who need custom checkout logic alongside custom UI.

Full checkout extensibility architecture

Pre-Checkout (Cart)

Cart Page Cart Transform Function Modified Cart

Checkout (Server-Side Logic)

Discount Function Tiered pricing, BOGO, tag-based discounts
Payment Customisation Hide COD for high-value orders
Delivery Customisation Rename options, hide methods by region

Checkout (Client-Side UI)

UI Extension: Upsell Dynamic product recommendations
UI Extension: Trust Shipping guarantee, reviews badge
UI Extension: Custom Gift wrapping, delivery notes

Post-Checkout

Order Confirmed Post-Purchase Upsell Thank You Page

A typical Shopify Plus checkout combines cart transforms, discount/payment/delivery functions, UI extensions, and post-purchase extensions into a unified flow.

The important thing to understand about this architecture is that Functions and UI Extensions operate independently. Functions run server-side before the checkout renders. UI Extensions run client-side as the customer interacts with the checkout. They do not directly communicate with each other — but they can both read from the same data sources (metafields, cart attributes, customer tags) to create a cohesive experience.

For example, a cart transform function might merge individual items into a bundle line, a discount function might apply bundle pricing, and a UI extension might display a "Bundle savings" badge — all operating on the same cart data independently but producing a seamless result for the customer.


Implementation Patterns from the Field

Here are patterns I have used repeatedly across checkout extension projects. These are not theoretical — they come from production implementations.

1

Metafield-Driven Configuration

Hard-coding business logic into your function is a maintenance headache. Instead, store configuration in shop or product metafields and read them through your function's input query. For discount functions, I store discount tiers as a JSON metafield on the shop. The function reads the tiers from its input and applies the matching discount. When the merchant wants to change a threshold, they update the metafield — no redeployment required.

Pattern structure

Shop metafield (JSON config) Function input query reads metafield Function applies dynamic logic
2

Cart Attribute Handshake

When you need a UI extension to influence a function's behaviour, cart attributes act as the communication channel. The UI extension writes a cart attribute (for example, a selected gift-wrapping option), and the function reads that attribute from its input to adjust pricing or modify the cart. This is the closest thing to inter-extension communication that the platform supports.

Pattern structure

UI Extension writes cart attribute Checkout re-evaluates Function reads attribute, adjusts logic
3

Progressive Disclosure for Upsells

Avoid rendering your upsell extension immediately on checkout load. Use the useShippingAddress hook to wait until the customer has entered their shipping information, then render the upsell. This serves two purposes: the customer is further committed (reducing abandonment from distraction), and you have shipping data available to show relevant offers. I typically render a collapsed banner that expands on interaction rather than a full product card, which keeps the checkout feeling clean.

4

Validation Gates

Checkout validation extensions let you block checkout completion based on custom rules. I use these for B2B stores that require a purchase order number, stores with age-restricted products that need date-of-birth confirmation, and subscription brands that need customers to acknowledge terms before their first subscription order. The key is to use the BlockStack and Banner components to display clear error states — a validation that blocks checkout without explanation will tank your conversion rate.


Gotchas and Limitations

Checkout Extensibility is powerful, but it is not without rough edges. These are the issues I have encountered in production that the documentation does not always make obvious.

  • Function debugging is painful. There is no step-through debugger for Wasm functions. You test with the Shopify CLI's function run command using JSON fixtures, but reproducing edge cases from production requires capturing the exact input payload — which Shopify does not expose easily. Build comprehensive test fixtures from day one.
  • UI Extension rendering is not instant. Extensions load asynchronously, which means there can be a visible flash as the extension mounts. Use useExtensionCapability to check if your extension should render before returning JSX, and keep the initial render lightweight to minimise perceived latency.
  • Multiple functions of the same type stack unpredictably. If two apps install discount functions, the order in which they execute is not guaranteed. Shopify merges the outputs, but conflicts (two functions discounting the same line item) are resolved by Shopify's internal logic, not yours. Test with other apps installed.
  • The 5ms execution limit is real. If your function approaches this limit with complex logic, it will be terminated and the operation will be skipped silently. Profile your function with representative data volumes — a store with 50 cart lines behaves very differently from one with 3.
  • Post-purchase extensions have a conversion window. The upsell page is shown for a limited time, and if the customer navigates away or closes the tab, the opportunity is lost. There is no retry mechanism. Optimise for immediate clarity — the offer, the price, and the accept button should be visible without scrolling.

Migrating from Script Editor

If you are still running Script Editor scripts, the migration path is straightforward in concept but requires careful planning. Start by auditing every active script. Categorise each by type: discount scripts become Shopify Functions using the discounts API, shipping scripts become delivery-customization functions, and payment scripts become payment-customization functions.

The tricky part is that some Script Editor scripts combine multiple concerns — a single script might apply a discount and hide a payment method based on the same condition. In the new model, these need to be separated into distinct functions. Each function has a single type and a single responsibility.

I recommend running the new functions in parallel with existing scripts during a testing period. Use Shopify's built-in analytics for functions to verify that the discount amounts and operation counts match what your scripts were producing. Only disable the scripts once you have confirmed parity across a representative set of orders.

For the UI side of the migration — if you were using checkout.liquid customisations alongside scripts — those need to move to UI Extensions. Shopify has set firm deadlines for checkout.liquid deprecation, and the checkout editor is the only supported path forward for custom checkout UI on Plus stores.


Getting Started: Project Setup

The Shopify CLI is the entry point for all extension development. Running shopify app generate extension scaffolds the boilerplate for whichever extension type you choose. For Functions, you pick a language (JavaScript, TypeScript, or Rust) and a function type. For UI Extensions, you get a React project with the appropriate target pre-configured.

My recommendation for teams starting out: begin with a single discount function. It is the most commonly needed extension type, the input/output contract is well-documented, and you can verify the results immediately in the checkout. Once your team is comfortable with the development and deployment cycle, move on to UI extensions and cart transforms.

For language choice in Functions, JavaScript is the pragmatic default — most Shopify development teams are already comfortable with it. Rust produces smaller, faster Wasm binaries and is worth considering if you are building functions that need to process large carts within the 5ms limit. In practice, I have never hit the execution limit with JavaScript functions on carts under 100 lines, so the performance argument for Rust is mostly theoretical for typical stores.

If you are planning a checkout extensibility project or migrating from Script Editor and want hands-on help, get in touch. I have shipped checkout extensions across a range of Shopify Plus stores as part of our Shopify development work, and can help you avoid the pitfalls that are not in the documentation.