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]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.
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.
- 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
- The Admin API rejects non-expiring offline access tokensApps must use expiring offline tokens with refresh.2027-01-01
- Shopify stops injecting script tags on storefrontsAnything still loaded by a script tag stops running.2027-03-01
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.
Who does what
| Stage | You (operator) | Merchant | Lead developer |
|---|---|---|---|
| Before you start | Get collaborator access, check scopes and markets | Approves access and scopes | — |
| 1 · Measure | Read the scan, write down the numbers | — | — |
| 2 · Activate | Send the activation link | Switches the embed on and clicks Save | — |
| 3 · Verify | Visit the storefront, re-scan, read the three signals | — | Only if the beacon never arrives |
| 4 · Remove | Queue and run tag removal, send the cleanup list | Approves theme cleanup in writing | Only on failed jobs |
| 5 · Prices by country | Run the migration, read the audit | Creates missing markets, removes legacy variants | — |
| Report | Download 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:
| Signal | Where the kit reads it | Green when |
|---|---|---|
| settings_data.json | The live theme's config/settings_data.json | The embed is Enabled |
| app.extensions() | Read while someone has the app open in the admin, then stored with a date | active, or not read yet (it doesn't block) |
| Beacon | A small call the embed makes from the storefront through the app proxy | Received in the last 72 hours |
The kit combines the signals with what's left of the old integration and shows one store status:
| Status | Meaning | Removal |
|---|---|---|
| Still registered | Not verified yet: at least one signal is missing. | Locked |
| Active | Verified. Script tags or legacy theme code still exist. | Open |
| Removed | Verified, 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.
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 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.
Before you start
Check every item before you open the app. Each one has stopped a migration before.
Stage 1 · Measure
Goal: know everything the old integration left behind in this store.
Do
- Open the section 1 · Measure.
- Read the three numbers: script tags registered, legacy markers in the live theme and themes scanned. Write them in the checklist.
- Read the table under each theme that has markers: File (file and line), What and Code.
- Optional, for the merchant or your lead: click Ask Shopify to create a script tag (live proof).
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 says | What it is in the theme |
|---|---|
Price wrapped in <span class="money"> for JS conversion | A price that old JavaScript rewrites in the browser. |
Old currency <select id="currencies"> | The old currency picker. |
Old .currency-picker widget | The old picker's container. |
| Theme includes a legacy currency snippet | A {% render %} or {% include %} of the old snippet. |
| Client-side currency conversion library | Old conversion code, for example Currency.convertAll. |
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 says "Shopify created the script tag". The platform is behaving differently from this manual. Don't continue: escalate.
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
- 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.
- 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.
- In the theme editor, under App embeds, find Currency switcher (Embed Migration Kit) and switch it on.
- 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.
- Click Save. Go back to the app and click Re-scan store.
Embed states you can see in the table:
| State | Meaning | What you do |
|---|---|---|
| Enabled | Switched on in this theme. The button reads Open in editor. | Go to Stage 3. |
| Disabled | Was on, and someone switched it off. | Ask the merchant to switch it on again (Template A). |
| Not added yet | Never switched on in this theme. | Normal for draft themes. On the Live row, activate it. |
| Unreadable | The 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.
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.
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.
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
- Open the storefront once, any page. This sends the beacon. If the store has a password page (development stores do), enter the password first.
- Look at the storefront: the Ship to selector appears in the corner the merchant chose, and the old currency pickers are gone.
- 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.
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, … }You visited the storefront and the app still says "No recent beacon from the storefront". Go to Troubleshooting: the first three rows cover it.
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
- Open the section 4 · Remove.
- 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."
- Click Run queue now. Each click processes up to 10 jobs that are due. Read the table: Script tag, Status, Attempts, Note.
- If a job shows retry, click Run queue now again later. The app waits on its own before it retries.
- 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."
- Send the checklist with Template B. Clean only a duplicated, unpublished theme. The merchant publishes it.
Queue statuses:
| Status | What it means | What you do |
|---|---|---|
| done | Deleted. "Script tag does not exist" also counts as done. | Nothing. |
| retry | Throttled, 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. |
| unreachable | 402 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). |
| failed | Any other error. | Escalate. |
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.
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
- 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.
- If you read "No active market with its own currency for: …", ask the merchant to create that market in Settings → Markets first.
- 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.
- Read the second table: Variant that stays, Legacy variant, Fixed price to set, Shopify charges in … and Audit.
- Click Migrate prices (market name). Wait for the banner "Prices migrated for …" and its log.
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.
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.
Charging in the local currency needs Shopify Payments or Adyen. Price rounding works only with Shopify Payments.
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.
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.
Messages to the merchant
Fill in the fields once. Both messages update as you type. Copy, paste into your email and send.
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.
Troubleshooting
Type what you see. The table filters as you type.
| Symptom | Likely cause | Fix |
|---|---|---|
| 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.
When to escalate
Handle everything else with this manual. Call the lead developer only when:
Do
- The live proof says "Shopify created the script tag".
- The embed is Enabled, you visited the storefront, the beacon setting is on, and
document.documentElement.dataset.emkis not"running". - The embed state is Unreadable.
- The token card is red: "Non-expiring offline token".
- A queue job is failed, or
401/403 token rejectedcomes back after the merchant re-authenticated. - "Migration for … stopped" shows an error you can't match in Troubleshooting.
- The theme cleanup needs more than removing the lines in the checklist (custom code around them).
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.
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.
The CSV columns:
| Column | Content |
|---|---|
shop | The store. |
section | script_tag, embed, legacy_marker or verdict. |
theme | The theme name (empty for script tags). |
role | MAIN for the live theme, or the theme's role. |
item | The script tag ID, the embed handle, or file:line of a marker. |
detail | The script source, the activation link, the marker label, or the verdict reasons. |
status | registered, 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.
Glossary
| Term | Meaning |
|---|---|
| Script tag | The old way an app loaded JavaScript on a storefront: a URL registered through the API that Shopify injected into every page. |
| Theme app extension | Code an app ships inside the merchant's theme. Shopify hosts it. |
| App embed | A theme app extension block that loads on every page, before </body>. The merchant switches it on per theme. |
| Live theme | The published theme buyers see. The API calls its role MAIN. |
| settings_data.json | The 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. |
| Beacon | A small call the embed makes from the storefront to the app, through the app proxy, to prove it ran. |
| App proxy | A path on the store's own domain that Shopify forwards to the app, with a signature. |
| Legacy marker | A line of old currency code the kit finds in a theme file. |
| Markets | Shopify's way to sell to several countries, each with its currency and prices. |
| Catalog / price list | Per-market product prices. The kit writes fixed prices into a price list in the market's currency. |
| contextualPricing | The API field that says what Shopify charges in a country. The kit's audit reads it. |
| Expiring offline token | An access token that expires and is refreshed. Required from January 1, 2027. |
| Unreachable store | A 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.