← Back to the package
Embed Migration Manual
Embed Migration Manualv1.0
Operator manual · 1.0 · October 13, 2026

Embed Migration Manual

How to move a merchant store from script tags to a theme app embed, one store at a time, with the Embed Migration Kit.

For Developers and support engineers · 27 min to read · Prepared by David

What this manual is for. Shopify is retiring script tags. Every store that still loads our storefront code through a script tag has to move to a theme app embed before March 1, 2027. This manual shows any developer or support engineer how to do that move for one store, from the first scan to the final report, without calling the lead developer except in the seven cases listed in When to escalate.

How to use it. Read Part 1 once (15 minutes). Keep Part 2 open while you migrate a store and tick the per-store checklist as you go. Go to Part 3 when something doesn't match the photos. Your ticks, your quiz score and the template fields stay in this browser only.

  1. Shopify rejects new script tags on every API version"This app can't create or update script tags. Use theme app extensions instead."2026-10-01
  2. The Admin API rejects non-expiring offline access tokensApps must use expiring offline tokens with refresh.2027-01-01
  3. Shopify stops injecting script tags on storefrontsAnything still loaded by a script tag stops running.2027-03-01
Part 1Understand the change

Why stores must change

Since October 1, 2026, no app can create or update a script tag. The kit asks Shopify for one on every store as a live proof, and the answer is always the same refusal. From March 1, 2027, Shopify stops loading the script tags that already exist. On that day, any currency selector or price logic that still depends on a script tag disappears from the storefront without an error.

The modern replacement has two parts:

  • A theme app embed that loads our code inside the merchant's theme. The merchant switches it on once, per theme.
  • Shopify Markets for prices. The buyer picks a country, and Shopify prices the store on the server. No exchange rates, and no price rewriting in the browser.

The work for each store is the same five stages: Measure, Activate, Verify, Remove and, only when the store fakes local prices with one variant per country, Prices by country.

Embed running
Legacy code running
Legacy code runningEmbed running
Drag the handle. The same storefront page, photographed twice. Left: the embed's script blocked in the browser, so only the legacy code runs: its picker (top right) rewrites every price to EUR in JavaScript, with hard-coded rates. Right: the embed running: one "Ship to" selector (bottom left), the legacy picker hidden, and prices as Shopify rendered them for the buyer's market.

Who does what

StageYou (operator)MerchantLead developer
Before you startGet collaborator access, check scopes and marketsApproves access and scopes—
1 · MeasureRead the scan, write down the numbers——
2 · ActivateSend the activation linkSwitches the embed on and clicks Save—
3 · VerifyVisit the storefront, re-scan, read the three signals—Only if the beacon never arrives
4 · RemoveQueue and run tag removal, send the cleanup listApproves theme cleanup in writingOnly on failed jobs
5 · Prices by countryRun the migration, read the auditCreates missing markets, removes legacy variants—
ReportDownload JSON and CSV, update the board—Reads the board

Two steps always belong to the merchant: switching the embed on, and approving any change to their theme. The platform requires the first. The second is our rule.

Three signals, three statuses

"Enabled isn't running." A merchant can switch the embed on and the storefront can still not run it. The kit only calls a store verified when three signals agree:

SignalWhere the kit reads itGreen when
settings_data.jsonThe live theme's config/settings_data.jsonThe embed is Enabled
app.extensions()Read while someone has the app open in the admin, then stored with a dateactive, or not read yet (it doesn't block)
BeaconA small call the embed makes from the storefront through the app proxyReceived in the last 72 hours

The kit combines the signals with what's left of the old integration and shows one store status:

StatusMeaningRemoval
Still registeredNot verified yet: at least one signal is missing.Locked
ActiveVerified. Script tags or legacy theme code still exist.Open
RemovedVerified, with 0 script tags and 0 legacy markers in the live theme.Nothing left

Try it: set the signals the way you see them in a store, and the decoder shows the status the app will show and what to do next. It uses the same rules as the app.

Status decoderSame rules as the app
Store status: Still registered
  • Embed on the live theme: never added
  • No recent beacon from the storefront

The app at a glance

Open the store admin → Apps → Embed Migration Kit. The app is one page. Click any part of the map to jump to its stage in this manual.

The whole app page on the development store, photographed after a full run. Each box links to its stage.

The button Re-scan store sits in the admin's title bar, at the top right of the app. Click it after every change you make outside the app: a theme edit, a storefront visit, a new market.

Part 2Migrate one store

Before you start

Check every item before you open the app. Each one has stopped a migration before.

Before you start0 of 7

Stage 1 · Measure

Goal: know everything the old integration left behind in this store.

Do

  1. Open the section 1 · Measure.
  2. Read the three numbers: script tags registered, legacy markers in the live theme and themes scanned. Write them in the checklist.
  3. Read the table under each theme that has markers: File (file and line), What and Code.
  4. Optional, for the merchant or your lead: click Ask Shopify to create a script tag (live proof).
1 · Measure on the development store: 0 script tags, 6 legacy markers in the live theme, each one with its file and line, and 5 themes scanned.
1 · Measure on the development store: 0 script tags, 6 legacy markers in the live theme, each one with its file and line, and 5 themes scanned.

The kit scans layout/theme.liquid, snippets/* and sections/* in every theme (up to six). These are the legacy markers it knows:

What the table saysWhat it is in the theme
Price wrapped in <span class="money"> for JS conversionA price that old JavaScript rewrites in the browser.
Old currency <select id="currencies">The old currency picker.
Old .currency-picker widgetThe old picker's container.
Theme includes a legacy currency snippetA {% render %} or {% include %} of the old snippet.
Client-side currency conversion libraryOld conversion code, for example Currency.convertAll.
Only this app's tags

The Script Tag API only returns the script tags of the app that asks. The kit can't see or touch tags that other apps registered. "0 script tags" means zero of ours.

The live proof: Shopify's own answer when the app asks for a new script tag.
The live proof: Shopify's own answer when the app asks for a new script tag.
Stop if

The live proof says "Shopify created the script tag". The platform is behaving differently from this manual. Don't continue: escalate.

Done when

You wrote down the three numbers and the list of markers in the store's checklist, and the live proof (if you ran it) says "Shopify refused the script tag".

Stage 2 · Activate

Goal: the embed is switched on in the live theme.

An app can't switch an embed on by itself. A person has to click it in the theme editor, normally the merchant. The kit opens the editor on the right theme with the embed ready.

Do

  1. Open the section 2 · Activate. There is one row per theme: Theme, Role (Live or Draft), Embed (settings_data.json), Legacy markers, Last beacon and Action.
  2. On the Live row, click Activate embed. It opens the theme editor on that exact theme, by its ID. If the merchant does this step, copy the link and send it with Template A.
  3. In the theme editor, under App embeds, find Currency switcher (Embed Migration Kit) and switch it on.
  4. Leave the settings like this: Position as the merchant prefers (Bottom left or Bottom right), Neutralize legacy currency code left in the theme on, Report to the app that the embed is running on.
  5. Click Save. Go back to the app and click Re-scan store.
2 · Activate after the merchant's click: the Live theme shows Enabled and a recent beacon, and its button changed to Open in editor. The draft themes show Not added yet.
2 · Activate after the merchant's click: the Live theme shows Enabled and a recent beacon, and its button changed to Open in editor. The draft themes show Not added yet.
The theme editor, App embeds panel, with Currency switcher on and its three settings. This admin is in Spanish: "Incrustaciones de apps" means App embeds. Photographed by hand, because the admin needs the merchant's login.
The theme editor, App embeds panel, with Currency switcher on and its three settings. This admin is in Spanish: "Incrustaciones de apps" means App embeds. Photographed by hand, because the admin needs the merchant's login.

Embed states you can see in the table:

StateMeaningWhat you do
EnabledSwitched on in this theme. The button reads Open in editor.Go to Stage 3.
DisabledWas on, and someone switched it off.Ask the merchant to switch it on again (Template A).
Not added yetNever switched on in this theme.Normal for draft themes. On the Live row, activate it.
UnreadableThe kit couldn't read settings_data.json.Escalate.

Manual path if the link doesn't work: Online Store → Themes → Customize (on the live theme) → App embeds → "Currency switcher" → On → Save.

Stop if

The editor opens a theme that is not the live one. Close it and use the manual path on the live theme. The kit builds its link with the theme ID because Shopify's generic themes/current link has opened the draft theme after a theme switch.

The embed lives per theme

If the merchant publishes another theme later, the new theme starts without the embed. The table shows it as Not added yet on the new Live row: activate it again.

Done when

The Live row reads Enabled after Re-scan store.

Stage 3 · Verify

Goal: prove the embed is running on the storefront, not only enabled.

Do

  1. Open the storefront once, any page. This sends the beacon. If the store has a password page (development stores do), enter the password first.
  2. Look at the storefront: the Ship to selector appears in the corner the merchant chose, and the old currency pickers are gone.
  3. Go back to the app, click Re-scan store and read the banner and the section 3 · Verify.

You should see the banner Store status: Active (or Removed) with the reasons "Embed enabled on the live theme (settings_data.json)" and "Storefront beacon received recently". In the beacon row: when it arrived, the page, and how many legacy elements the embed neutralized.

The status banner after verification. Active means verified, with legacy code still in the theme.
The status banner after verification. Active means verified, with legacy code still in the theme.
3 · Verify: the three signals. On the development store the embed reports 14 legacy elements neutralized on the home page.
3 · Verify: the three signals. On the development store the embed reports 14 legacy elements neutralized on the home page.
Check it yourself in the browser

On any storefront page, open the developer tools console and run the line below. "running" means the embed's script ran on this page. The second line shows its version and how many legacy elements it neutralized.

document.documentElement.dataset.emk   // "running"
window.__emkEmbed                       // { version: "1.0.0", neutralized: 14, … }
Stop if

You visited the storefront and the app still says "No recent beacon from the storefront". Go to Troubleshooting: the first three rows cover it.

Done when

The banner reads Store status: Active or Removed, and the beacon row shows a visit from the last 72 hours.

Stage 4 · Remove

Goal: delete this app's script tags, one at a time, only after verification. Then the theme cleanup, by a person, with written approval.

Do

  1. Open the section 4 · Remove.
  2. Click Queue script tag removal. It only works when the store is verified and has script tags. Otherwise you read "Removal stays locked until all three signals are green."
  3. Click Run queue now. Each click processes up to 10 jobs that are due. Read the table: Script tag, Status, Attempts, Note.
  4. If a job shows retry, click Run queue now again later. The app waits on its own before it retries.
  5. Read the Theme cleanup checklist. Each line has the file, the line and what it is, and says either "Already neutralized by the embed on the storefront." or "Not neutralized yet: activate the embed first."
  6. Send the checklist with Template B. Clean only a duplicated, unpublished theme. The merchant publishes it.
4 · Remove on the development store. Its 0 script tags leave both queue buttons disabled. The cleanup checklist lists the 6 legacy lines, all neutralized by the embed.
4 · Remove on the development store. Its 0 script tags leave both queue buttons disabled. The cleanup checklist lists the 6 legacy lines, all neutralized by the embed.

Queue statuses:

StatusWhat it meansWhat you do
doneDeleted. "Script tag does not exist" also counts as done.Nothing.
retryThrottled, 423 locked (usually temporary), a server error or a network error. A 423 waits 15 minutes and doubles each time, up to 6 hours.Click Run queue now later. Never mark a 423 store as lost.
unreachable402 frozen store, 404 store closed or app uninstalled, 401/403 token rejected, or a missing scope.Add it to the unreachable list with its note (Troubleshooting).
failedAny other error.Escalate.
Don't

Remove anything before the three signals are green. The app refuses it anyway: "The embed is not verified on the live theme yet." After March 1, 2027, leftover script tags have no effect, but delete them when you can: they still count as usage.

Done when

Every queue job is done or on the unreachable list with its cause, and the merchant approved and published the clean theme. If the store had 0 script tags and 0 markers, the status already reads Removed.

Stage 5 · Prices by country

Do this stage only if the store fakes a local price with one variant per country: an option such as "Country" with values like United States and Mexico. Under Markets, the old variant's local price gets converted again, so the buyer sees a wrong price.

Do

  1. Open the section 5 · Prices by country. The first table lists the products with one variant per country: Product, Option, Countries, Stock split by country, Variants.
  2. If you read "No active market with its own currency for: …", ask the merchant to create that market in Settings → Markets first.
  3. For each market, read the plan: the market, its catalog, the price list in the market's currency, the number of fixed prices (sent in batches of up to 250) and the audit.
  4. Read the second table: Variant that stays, Legacy variant, Fixed price to set, Shopify charges in … and Audit.
  5. Click Migrate prices (market name). Wait for the banner "Prices migrated for …" and its log.
5 · Prices by country after the migration: the market México sells in MXN, the fixed price is 15,999.00 MXN and the audit reads "✓ fixed price applied".
5 · Prices by country after the migration: the market México sells in MXN, the fixed price is 15,999.00 MXN and the audit reads "✓ fixed price applied".

You should see "✓ fixed price applied" in the Audit column for every row. The migration is safe to repeat: the button changes to Run the … migration again and reuses the catalog and the price list.

Ship to Mexico
Ship to United States
Ship to United StatesShip to Mexico
The same product, one buyer in each market. United States: the base price, $885.95. Mexico: the fixed price from the price list, $ 15,999.00 MXN. Before the migration Shopify charged 16,558.00 MXN (the USD price converted), and the old "Mexico" variant alone would show 299,006 MXN because Markets converts its local price again. The "Country" option is still there: removing it is the merchant's call. "Sold out" in Mexico comes from the test store's stock, not from the kit.

Left for the merchant (the kit never deletes variants or products): check open orders, remove the legacy variant, move its stock back to the variant that stays and, when no legacy variants are left, remove the "Country" option.

Payment limits

Charging in the local currency needs Shopify Payments or Adyen. Price rounding works only with Shopify Payments.

Done when

The Audit column shows "✓ fixed price applied" on every row, and the merchant has the list of what's left for them.

Tokens and report

Tokens · January 1, 2027. The card shows one of two banners:

  • "This app uses an expiring offline token" (green): ready for January 1, 2027. Nothing to do.
  • "Non-expiring offline token" (red): Shopify rejects these from January 1, 2027. It's an app-level change: escalate.
The token card on the development store: an expiring offline token with its refresh token.
The token card on the development store: an expiring offline token with its refresh token.

Report. Click Download JSON and Download CSV. The files are named migration-<store>.json and migration-<store>.csv. Add the CSV to the migration board.

The report buttons. The same report runs for every store.
The report buttons. The same report runs for every store.

Messages to the merchant

Fill in the fields once. Both messages update as you type. Copy, paste into your email and send.

Fill in onceUsed by both messages
Template AAsk for the activation click
Subject: One click to keep your currency selector working after March 1, 2027
Hi [Merchant name],

Shopify is retiring the old way apps load on storefronts. From March 1, 2027 it stops loading "script tags", and the currency selector on [Store name] still uses the old method.

We moved the selector to Shopify's new method, called an "app embed". Shopify only lets the store owner switch it on, so we need one click from you:

1. Open this link while you are logged in to your Shopify admin: [Activation link]
2. Make sure "Currency switcher" is on.
3. Click Save.

That's all. We check your storefront afterwards and confirm that everything works before we remove anything old.

Thank you,
[Your name]
Template BAsk for approval to clean the theme
Subject: Approval to remove old currency code from your theme
Hi [Merchant name],

The new currency selector is running on [Store name], and it already hides the old code on your storefront. Your theme still has [Number of lines] old lines of code from the previous setup:

[Cleanup checklist]

We would like to remove them. This is how we work:

1. We make a copy of your live theme and clean only the copy.
2. We send you a preview link so you can check it.
3. You publish the clean copy when you are happy with it. Your current theme stays as a backup.

Please reply "Approved" to this email so we can start. Nothing changes on your live theme without your approval.

Thank you,
[Your name]

Per-store checklist

One checklist per store. Type the store's name, tick as you go, and copy it into the store's ticket when you finish. Each store keeps its own ticks in this browser.

Per-store checklist0 of 10
Part 3When something goes wrong

Troubleshooting

Type what you see. The table filters as you type.

17 of 17
SymptomLikely causeFix
Embed Enabled, but "No recent beacon from the storefront".Nobody opened the storefront in the last 72 hours.Open any storefront page, then Re-scan store.
Same, after visiting the storefront."Report to the app that the embed is running" is off.Switch it on in the theme editor, Save, visit the storefront again.
Same, and the setting is on.Embeds with target: body are inserted before the first literal </body>. If the theme has </body> inside a comment, the embed lands inside the comment and never runs, without any error.Run document.documentElement.dataset.emk in the storefront console. If it isn't "running", escalate (theme structure).
The theme editor opens a draft theme.Shopify's generic themes/current link has opened the draft theme after a theme switch. The deep link has also failed for all apps for about a day before.Use Activate embed on the Live row (it uses the theme ID), or the manual path on the live theme.
The beacon arrives but there is no Ship to selector.The store has only one market.Expected. The merchant adds a second market if they want the selector.
The old currency picker is still visible."Neutralize legacy currency code left in the theme" is off, or the old picker uses markup the kit doesn't know.Switch the setting on. If it's already on, add the element to the cleanup list.
Old and new code both load during the transition.The old script tag still exists.Expected until Stage 4. The embed loads itself once, hides old pickers and keeps the price Shopify rendered. Finish Stage 4.
A product shows a huge price in one market, such as 299,006 MXN.The old per-country variant stores a local price, and Markets converts it again.Run Stage 5. Then the merchant removes the legacy variant.
A product shows Sold out.Its variants have no available stock (common on test stores and on legacy country variants whose stock was split).A store setting, outside the kit. The merchant sets the quantity per variant (Products → the product → Inventory).
"Prices by country can't be read yet".The store hasn't granted read_products, write_products or read_markets.The merchant approves the app's updated scopes, then Re-scan store.
Queue: 423 locked (usually temporary).Shopify locked the store for a while.The app waits 15 minutes, doubling up to 6 hours. Click Run queue now later. Don't count the store as lost.
Queue: 402 frozen store.The store is frozen (unpaid balance). Its tags can't be deleted.Unreachable list, with the cause, for a Shopify support ticket.
Queue: 404 store closed or app uninstalled.The store closed or uninstalled the app.Unreachable list.
Queue: 401/403 token rejected.The token is invalid, or it's a non-expiring token after January 1, 2027.The merchant opens the app again to re-authenticate. If it repeats, escalate.
Queue: missing scope: …write_script_tags….The store re-authorized without write_script_tags.The merchant approves the scope again. Never drop this scope before the audit reads zero.
Embed state Unreadable.The app couldn't read config/settings_data.json.Escalate.
app.extensions() reads "not read yet (needs the merchant in the app)".Nobody opened the app in the admin since the change.Open the app in the store admin. It doesn't block verification.

What the kit can't do

Say these limits early, to the merchant and to your team. The app shows the same list on its right-hand column.

  • Switch the embed on for the merchant. The platform requires their click. The kit opens the right theme.
  • Rewrite theme code from a public app. There is no write_themes, and Shopify doesn't grant that exemption to public apps in general. Cleanup is human, on a duplicated theme, with written approval.
  • Delete script tags on frozen (402), locked or closed stores. They are listed with the cause.
  • Run in bulk across stores. Bulk operations are per store, and the queue is per store by design.
  • Charge in the local currency without Shopify Payments or Adyen. Rounding works only with Shopify Payments.
  • Delete legacy country variants. The merchant decides, after checking orders.
The column What this kit can't do, as the app shows it.
The column What this kit can't do, as the app shows it.

When to escalate

Handle everything else with this manual. Call the lead developer only when:

Do

  1. The live proof says "Shopify created the script tag".
  2. The embed is Enabled, you visited the storefront, the beacon setting is on, and document.documentElement.dataset.emk is not "running".
  3. The embed state is Unreadable.
  4. The token card is red: "Non-expiring offline token".
  5. A queue job is failed, or 401/403 token rejected comes back after the merchant re-authenticated.
  6. "Migration for … stopped" shows an error you can't match in Troubleshooting.
  7. The theme cleanup needs more than removing the lines in the checklist (custom code around them).
What to send with an escalation

The store's JSON report, a screenshot of the section that shows the problem, the number of the case above, and what you already tried. One message, so nobody has to ask twice.

Part 4Many stores

The migration board

Every store gives one report. Merge the CSV files of all stores into one sheet: that's the migration board. The tool below does it in your browser. Drop or paste CSV files, and it shows one row per store. It opens with the development store's real report as an example.

Migration boardRuns in your browser
Drop the migration-<store>.csv files here, or
StoreStatusEmbed on live themeScript tagsLegacy markersReasons

The CSV columns:

ColumnContent
shopThe store.
sectionscript_tag, embed, legacy_marker or verdict.
themeThe theme name (empty for script tags).
roleMAIN for the live theme, or the theme's role.
itemThe script tag ID, the embed handle, or file:line of a marker.
detailThe script source, the activation link, the marker label, or the verdict reasons.
statusregistered, the embed state, the marker type, or the store status.

Useful views: filter section = verdict to see every store's status in one column; count section = script_tag per store to see what's left to delete; group unreachable stores into frozen (402), locked (423, temporary) and closed (404). Deleting is rarely the bottleneck. The state of the stores is.

Knowledge check

Ten questions. Answer them before your first store on your own. 8 out of 10 is a pass.

Knowledge check10 questions · pass at 8
  1. The merchant says "I switched the embed on." Is the store verified?

    Enabled isn't running. Visit the storefront, re-scan and read the three signals.

  2. Which theme must have the embed switched on?

    The storefront only runs the live theme. Drafts can stay at "Not added yet".

  3. The queue shows 423 locked (usually temporary). What do you do?

    A 423 is temporary. Never count it as lost.

  4. Can the kit delete the legacy code it found in the theme?

    The app has no write_themes scope. Cleanup is human, on a copy, with approval.

  5. In Mexico a product shows 299,006 MXN. What happened?

    Run Stage 5 to set a fixed price in the market's price list. The merchant then removes the legacy variant.

  6. When can the app stop asking for write_script_tags?

    Stores that re-authorize without that scope can't have their tags deleted anymore.

  7. What's the difference between Active and Removed?

    Active stores still have script tags or legacy theme code to clean.

  8. You click Activate embed. What happens?

    No app can switch an embed on. The click and the Save belong to a person, normally the merchant.

  9. The store has one market. There's no Ship to selector on the storefront. Is something broken?

    The selector appears with two or more markets.

  10. The embed is Enabled, the beacon setting is on, you visited the storefront, and there's still no beacon. Next step?

    The theme may have </body> inside a comment, which hides the embed. That's a theme structure case for the lead developer.

Glossary

TermMeaning
Script tagThe old way an app loaded JavaScript on a storefront: a URL registered through the API that Shopify injected into every page.
Theme app extensionCode an app ships inside the merchant's theme. Shopify hosts it.
App embedA theme app extension block that loads on every page, before </body>. The merchant switches it on per theme.
Live themeThe published theme buyers see. The API calls its role MAIN.
settings_data.jsonThe theme file that stores which app embeds are on. The kit reads it to know if the embed is enabled.
app.extensions()An App Bridge call that tells the app which of its extensions are active. It only answers while someone has the app open in the admin.
BeaconA small call the embed makes from the storefront to the app, through the app proxy, to prove it ran.
App proxyA path on the store's own domain that Shopify forwards to the app, with a signature.
Legacy markerA line of old currency code the kit finds in a theme file.
MarketsShopify's way to sell to several countries, each with its currency and prices.
Catalog / price listPer-market product prices. The kit writes fixed prices into a price list in the market's currency.
contextualPricingThe API field that says what Shopify charges in a country. The kit's audit reads it.
Expiring offline tokenAn access token that expires and is refreshed. Required from January 1, 2027.
Unreachable storeA store whose tags can't be deleted now: frozen (402), closed (404) or with a rejected token.

About this manual

  • Photos. Every photo of the storefront and of the app was taken with Playwright on October 13, 2026, on a Shopify development store with test data, after a full run of the five stages. The app photos are the app's own page, built from its own code with that store's real data, rendered outside the admin frame. That's why the admin's title bar, with Re-scan store, isn't in them. The theme editor photo was taken by hand, because the admin needs the merchant's login.
  • Nothing identifies a store. The store name reads "Your store" and IDs are generic. No merchant data appears.
  • Timings. The kit's own work for one store takes minutes. Most of the time is waiting for the merchant: the activation click and the written cleanup approval. Real merchant timings aren't measured yet; write yours in the report.
  • Versions. Embed Migration Kit with the embed "Currency switcher", Admin API 2026-10.
  • Edition. 1.0, October 13, 2026. First edition, built on the Embed Migration Runbook (same stages, labels and escalation cases). Send corrections to the lead developer: the next edition keeps this structure.
Embed Migration Manual · 1.0 · October 13, 2026Prepared by David