If you just bumped your checkout UI extension to Shopify API version 2026-04 and your shopify.metafields reads started returning undefined, you've hit the biggest breaking change in this release.
Checkout metafields in the metafields API are gone. They've been replaced by order metafields in the appMetafields API. The rename isn't cosmetic — it's a different data source, a different access pattern, and a different set of timing semantics. The old code doesn't error; it just silently returns nothing.
What the failure looks like
The code that worked on 2026-01:
import { extension } from '@shopify/ui-extensions/checkout';
export default extension('purchase.checkout.block.render', (root, api) => {
const { metafields } = api;
const myField = metafields.current.find(
m => m.namespace === 'custom' && m.key === 'delivery_window'
);
// myField.value used to be "2026-05-01T09:00:00"
root.append(root.createText(myField?.value ?? 'unknown'));
});
After the 2026-04 upgrade, myField is undefined. No deprecation warning. No network error. The extension renders 'unknown' in production and the fallback path becomes the default behavior. If the ternary hid a real problem — a missing delivery window, a missing subscription flag, a missing audit tag — your orders flow through without it.
The symptom in Shopify's developer console, if you're lucky enough to be looking there at the right moment, is either a missing metafield in the api.metafields.current array or the array itself being empty. Neither is an error condition, just a change in what's served.
Why it breaks silently
Shopify's UI extensions surface APIs as plain JS properties. When the platform removes a property at a given API version, the consuming code reads undefined — the same as if the field weren't set for this buyer. There's no distinction between "metafield doesn't exist on this order" and "this API doesn't expose metafields anymore," because both are the same runtime shape.
The 2026-04 release moves this data to a different surface: shopify.appMetafields. The old call site keeps compiling because metafields is still a property on api — it's just the thing it references has shrunk.
The exact migration
The replacement property is appMetafields, read from the global shopify object rather than a callback parameter:
import '@shopify/ui-extensions/preact';
import { render } from 'preact';
export default async () => {
render(<Extension />, document.body);
};
function Extension() {
const entries = shopify.appMetafields.value;
const myField = entries.find(
e =>
e.target.type === 'order' &&
e.metafield.namespace === 'custom' &&
e.metafield.key === 'delivery_window'
);
return <s-text>{myField?.metafield.value ?? 'unknown'}</s-text>;
}
Three differences worth internalizing:
-
appMetafieldsis an app-scoped surface. Your extension only sees metafields written under your app's reserved namespace. Anything your app wrote to acustom.*namespace before is visible on the compatibility shim today, but new writes should use the app-reserved namespace going forward. -
Entries are wrapped, not bare. Each entry has
{ target, metafield }. Thetargetis the resource the metafield is attached to (order,customer, etc.). The old flat-array shape is gone — filter ontarget.typebefore filtering on namespace/key. -
It's a reactive value, not a snapshot.
shopify.appMetafieldsis a Preact-signals-style reactive value. Reading.valuegives the current array. Inside a component it re-renders when the underlying entries change, which matches the new 2026-01 UI model (Preact web components replacing React).
Where the old pattern is hiding in your code
This migration hits more than the obvious metafield calls. Places to grep:
git grep -n "api\.metafields"
git grep -n "useMetafields"
git grep -n "shopify\.metafields"
git grep -n "ExtensionPoint.*Checkout"
The useMetafields React hook is an older API that also routed through the deprecated surface. Anywhere you called it, the replacement is the shopify.appMetafields.value read above.
Server-side writers don't need to change — the Admin GraphQL API's metafieldsSet mutation still works. It's only the read path from checkout extensions that moves.
The cart-metafields path, if you're writing fresh code
Separately from the appMetafields migration, Shopify is steering new code toward cart metafields as the canonical place for per-buyer data that needs to survive checkout. As of 2026-04, cart metafields automatically copy to the resulting order at checkout completion, which closes a long-standing gap where checkout-time state quietly didn't make it to fulfillment.
If you're building something new today, the decision tree is:
- Per-buyer data that needs to outlive checkout → cart metafields. They'll auto-copy to the order.
-
Per-app configuration that needs to be read at checkout → app-reserved namespace metafields, read via
shopify.appMetafields. -
Legacy
custom.*-namespace metafields your app has historically written → still readable on the compatibility shim, but plan a migration to your app-reserved namespace.
Detecting the break before your QA does
The obvious version check:
grep -r "api_version" extensions/ | grep -v node_modules
If anything returns 2026-04 or later and the extension still reads api.metafields, that extension is broken.
The less-obvious version: extensions can be bumped independently, and an app with multiple extensions can have one on 2026-04 and another on 2026-01. The one on 2026-04 silently fails; the one on 2026-01 works. You get half your data on the order and half missing — exactly the confusing support ticket that's hard to reproduce.
A small synthetic order run in your staging shop is the cheapest catch. Place an order that exercises each extension, pull the resulting order via the Admin API, and diff the metafields array against what you expect. Any missing key is a lead.
The broader pattern
What's happening here is a specific case of a general problem: the SDK you depend on changes the shape of the data it returns, not the shape of its function signatures. Types still line up. The code still compiles. The values have just moved, and the old read path becomes undefined.
The ways this usually bites:
-
TypeScript doesn't catch it. The types got updated on the SDK, your code got updated with
@shopify/ui-extensions2026-04, andapi.metafieldsis still typed — it's just a narrower universe of things now. - Unit tests don't catch it. Mocked responses in a test fixture don't know the real platform shape changed.
- CI doesn't catch it. Nothing in CI is hitting Shopify's edge. The break only shows up in a real checkout.
The way to catch this kind of drift before buyers do is a scheduled synthetic check against the real vendor surface — place an order in a staging shop, read it back, diff the metafields array against a known baseline. Same pattern as monitoring any other third-party API contract, applied to the specific surface each platform exposes.
Minimum-viable fix for today
-
git grep -E "api_version.*2026-04"across every extension and confirm which ones bumped -
git grep -n "api\.metafields\|useMetafields"in every bumped extension — each hit is a broken read - Migrate those call sites to
shopify.appMetafields.valuewith.target.type === 'order'filtering - Move any app-owned metafield writes to your app-reserved namespace
- Place a synthetic test order in a staging shop and verify the expected metafield keys land on the resulting order
- If you have multiple extensions on mixed API versions, align them or at minimum document which are on which version
If the migration reveals that checkout metafields were load-bearing for fulfillment, consider whether cart metafields — now auto-copied to the order at checkout completion — are the better home going forward.
FlareCanary monitors REST and GraphQL APIs — including Shopify Admin and Storefront endpoints — for schema drift. Free tier covers 5 endpoints with daily checks.
Top comments (0)