Checkout extensibility is really two products
UI extensions render and Functions decide. Almost every disappointing checkout project comes from asking one of them to do the other one's job.
AnalysisJuly 8, 20267 min read
"Checkout extensibility" is a single phrase covering two pieces of technology that share almost nothing. One renders things a buyer can see. The other makes decisions the buyer never sees. They run in different places, under different rules, with different failure modes, and they are gated differently by plan.
Nearly every checkout project that ends in disappointment ends there because someone asked one half to do the other half's job. The merchant asked for a fee and got a message. The merchant asked for an explanation and got a silent price change. Neither party was wrong about what they wanted; they were using one word for two things.
So it is worth separating them properly, because once you can tell them apart you can also tell, from a single sentence of a merchant request, which one you are actually being asked for.
One name, two products
A checkout UI extension is a small application that Shopify renders into a named slot in the checkout. It is your code, running in a sandbox, inside the buyer's session, drawing an interface. It can show a gift-message field, a delivery-date picker, a trust badge, a line of copy explaining a restriction.
A Function is a WebAssembly module Shopify executes on its own servers while it prices the cart and validates the order. It has no interface and cannot produce one. It returns a list of operations and disappears.
Scroll the figure sideways to see all of it
Put crudely: the UI half is a tenant in Shopify's interface, and the logic half is a subroutine in Shopify's pricing engine. The first is a guest and behaves like one. The second is code the platform runs on its own behalf.
The UI half is advisory, and the API says so out loud
This is the part that surprises people, and Shopify is admirably direct about it. A checkout UI extension does not simply change the checkout. It asks, and the checkout can say no.
Before an extension adds a cart line, it is expected to check instructions.lines.canAddCartLine. Before it writes a cart metafield, instructions.metafields.canSetCartMetafields. Before it edits the shipping address, instructions.delivery.canSelectCustomAddress. Those instructions are not decoration; they flip to false in real situations — a draft order checkout, a Shop Pay session, a subscription — and an extension that ignores them fails at the worst moment.
The rate limiting is even more revealing. Shopify rate limits extensions that apply too many changes, and what the documentation says happens next is that the extension can no longer make changes during that buyer's session. Read that back: the platform is built so a misbehaving extension gets stepped around, not so it can stop a sale. That is the correct trade-off for a checkout, and it is fatal to any plan that treats a UI extension as an enforcement mechanism.
Add the last constraint, the one that decides scope more often than any other: checkout UI extensions that render on the information, shipping and payment steps are available only on Shopify Plus. Extensions elsewhere in the flow are more broadly available. A quote written without knowing which side of that line a request falls on is a guess.
The logic half is authoritative, and mute
A Function has the opposite profile. It cannot be skipped, rate limited into irrelevance, or bypassed by a buyer who posts directly to the cart API — it runs inside the pricing pass, on Shopify's infrastructure, every time. If you need a rule to be true, this is the only place it can live.
And it cannot say a word. It returns operations. A validation Function can return an error message tied to a field, and a discount Function attaches a message to the discount it created, but neither of those is a UI you control. A Function cannot render a tooltip explaining that the import fee skipped the gift card. It cannot put a badge next to a line. It changes the number and moves on.
The trade is deliberate. The half that can be trusted is the half that cannot be seen, and the half that can be seen is the half that cannot be trusted.
Scroll the figure sideways to see all of it
Four sentences, and what each one is really asking for
Here is where the distinction pays for itself. Take four requests that arrive worded almost identically and sort them.
"Add a five dollar gift-wrap fee at checkout." This is a Function — Cart Transform, expanding the line. An extension could add a gift-wrap product as a cart line, but it would be adding a product at its own price, not a fee, and it would be doing it from a surface that can be rate limited. If the money has to be right, the money is a Function.
"Let the customer type a gift message." This is a UI extension, all of it. There is no decision to make. Something has to be drawn, collected, and attached to the order as an attribute or a cart metafield. A Function has nothing to contribute.
"Show the customer why their discount did not apply." This is both, and this is the request that most often gets built as one. The rule lives in a Function. The explanation lives in an extension. They do not share memory, so somebody has to design the handoff — usually a cart attribute or a cart metafield the Function can read and the extension can display, written by whichever side actually knows.
"Do not let anyone in that region complete an order." This is a validation Function and nothing else. Building it as an extension produces a checkout that blocks polite buyers and lets through anyone with a slow connection or an ad blocker, which is worse than not building it.
The pattern underneath: if the wrong answer costs money or breaks a commitment, it is a Function. If the wrong answer costs a moment of confusion, it is an extension. Most real briefs contain both, and the useful project plan names them separately with separate acceptance criteria. That is how we structured our checkout extension work, and it is why a rules engine app keeps the merchant-facing configuration in the admin rather than in the checkout itself.
Why the split is right, even when it is annoying
It would be more convenient if an extension could set a price. It would also mean that checkout pricing could be changed by code running in a browser, which is a category of problem no ecommerce platform survives at scale. The separation is not an oversight to be worked around; it is the reason Shopify can run one checkout for everyone and still let strangers extend it.
The useful consequence for anyone scoping this work is that the boundary is a real seam in your project, not an implementation detail. It changes the plan requirement, the testing strategy (a Function is unit-testable in isolation, an extension needs a real checkout), the deployment story, and who is on the hook when something looks wrong in production. Treating it as one thing called "checkout customization" hides all four.
How to phrase the request
Before asking anyone to build checkout work, split your own sentence in two. Write down what must be true — the price, the rule, the refusal — and separately what must be visible — the field, the message, the badge. Then ask what happens if the visible half never renders.
If the answer is "the buyer is confused", you have a healthy design. If the answer is "we lose money", the rule is in the wrong half, and it should move before anyone writes code. Our pages on checkout extensibility and Shopify Functions development cover the two halves separately for the same reason, and the decision between a Function, an app and a theme change is the next thing to read once the split is clear.