How to back up a Shopify store
Shopify runs the servers, but it does not give you a copy of your store or a button that puts it back the way it was yesterday. Its own backup page points you to CSV exports, a theme download and backup apps, and a deleted product cannot be restored. This guide covers what the admin exports and what it misses, how to copy the theme, how to export everything else nightly through the API, and what you can and cannot import again.
What Shopify keeps for you, and what it doesn't
Shopify's Help Center page on backups is called Backups and duplication. It says you can export CSV files to back up store information, download your theme, or use an app from the Shopify App Store. It describes no snapshot of your store and no way to roll it back to an earlier day. Mistakes are final: "When you delete a product, it's permanently removed from Shopify. Deleted products can't be restored." Deleted orders are removed from the admin and from reports.
So a Shopify backup means three copies you make yourself: the CSV exports, the theme files, and an API export of everything the CSVs leave out. Shopify's CSV help lists what the admin can export and import:
| Data | Export | Import back |
|---|---|---|
| Products | Yes | Yes, CSV up to 15 MB |
| Customers | Yes | Yes, CSV up to 15 MB, no passwords |
| Inventory | Yes | Yes, CSV up to 15 MB |
| URL redirects | Yes | Yes |
| Orders | Yes | No, not through the admin |
| Discounts | Yes | No |
| Gift cards | Yes, last 4 characters of each code only | No |
| Theme | Download as a .zip, or Shopify CLI | Yes, upload the .zip or push with the CLI |
| Pages, blog posts, menus, metaobjects, Content > Files, policies, translations | No CSV | No CSV |
Archive a product instead of deleting it when you might want it back; an archived product moves to the Archived tab and returns as a draft. A deleted product only comes back by importing it again from your own CSV, as a new product with a new ID.
Export the CSVs from the admin
Shopify emails most of these exports instead of downloading them: the file goes to you and, if you are staff, to the store owner too. Choose Plain CSV file where the dialog offers it. If you open a file in a spreadsheet program, save it back as UTF-8, the only encoding Shopify imports, and don't sort a product CSV: Shopify warns that sorting can separate images from their products.
| Export | Where | Notes |
|---|---|---|
| Products | Products > Export > All products > Plain CSV file > Export products | Emailed. Holds image URLs, not the image files. |
| Customers | Customers > Export > All customers | More than 50 is emailed. Tick Customer tags and Customer metafields. |
| Orders | Orders > Export > Orders by date > Export orders | Emailed when more than 50 or by date. Export transaction histories separately. |
| Inventory | Products > Inventory > Export, All states | One row per location: Incoming, Unavailable, Committed, Available, On hand. |
| Gift cards | Products > Gift cards > Export | More than 50 is emailed. Only the last 4 characters of each code. |
| URL redirects | Content > Menus > URL redirects > Export | Up to 100,000 redirects per store, 20,000,000 on Plus. |
Read the product CSV's gaps before you rely on it. Images are URLs in the Product image URL and Variant image URL columns; on import Shopify downloads them and uploads them again, so a URL that no longer works means a product without pictures. Download the image files too, or keep the API export below, which lists every media URL. Product metafields are included once they are defined, as product.metafields.namespace.key columns, but variant metafields are not. Collections are not exported at all, and the inventory column only works for a single location; multi-location stores need the inventory CSV.
The customer CSV has no passwords, and only some metafield types export: boolean, date, date and time, integer, decimal and single line text. The order CSV puts each extra line item on its own row, its Discount Code column leaves out automatic discounts, and its transaction history includes captured payments only. Shopify's estimate for a large order export is under an hour for fewer than 100,000 items and around 4 hours for 400,000.
Back up the theme
The theme's files hold more than code. In an Online Store 2.0 theme, config/settings_data.json and the JSON templates hold the colours, sections and text you set in the theme editor, so a theme copy brings those back. It does not include products, collections, menus, pages, blog posts, images or Content > Files.
- Duplicate: Online Store, open the theme's actions menu, Duplicate. The copy sits in your theme library, which holds 20 themes on Basic, Grow and Advanced and 100 on Plus; the Starter plan can only add the Spotlight theme. A duplicate lives inside the same store, so it protects you from your own edits, not from losing the store.
- Download theme file: Online Store, the theme's actions menu, Download theme file. Shopify emails a .zip to the address on your staff login.
- Timeline: the code editor keeps a per-file history you can restore one file at a time. The history is finite, and files in
assets(images, CSS, JavaScript, fonts) have none. - Shopify CLI: pulls every theme file into a folder on your machine, which you can commit to Git. It needs Node.js 22.12 or later and Git 2.28.0 or later.
npm install -g @shopify/cli@latest
shopify theme pull --store your-store --live --path ./theme-2026-10-29--store takes the store prefix or the full your-store.myshopify.com. --live pulls the published theme; use --theme with a theme's ID or name for another one. The first run signs you in through the browser. Without --live, --development or --theme the CLI asks which theme you mean, which a script cannot answer.
What no CSV covers
Check this list against your store. Anything on it that you have built by hand exists only in Shopify until you export it through the API:
- Pages and blog posts, and the store's legal policies.
- Navigation menus. URL redirects do have their own CSV.
- Collections, including smart collection rules. The product CSV can add products to a collection on import, but it doesn't export membership.
- Metaobjects and their definitions, and variant metafields.
- Uploaded files under Content > Files, and product images as files.
- Translations and Markets settings.
- Discount codes and issued gift cards as something you can restore. Shopify says both "can't be transferred".
- Store settings: shipping, taxes, checkout, notifications, staff. Note them by hand or with screenshots.
- Data that apps keep on their own servers. Shopify's exports only cover Shopify's own data, so check each installed app for its own export.
Get an API token from the Dev Dashboard
Since January 1, 2026, a new custom app can no longer be made in the store admin. Apps made there before that date keep working, with their static Admin API token, and are still managed from the admin. New ones are made in the Dev Dashboard at dev.shopify.com. Making one needs the organization-level App developer role; collaborator accounts cannot open the Dev Dashboard.
- In the Dev Dashboard, open Apps, click Create app, choose Start from Dev Dashboard, name it and click Create.
- On the app's Versions page, select the read scopes from the table below, then click Release. An app needs a released version before it can be installed.
- Click Install app and choose the store.
- In App settings, copy the Client ID and Client secret from the Credentials card. Treat the secret like a password.
| To export | Top-level query | Scope |
|---|---|---|
| Products, variants, product metafields, media | products | read_products |
| Collections | collections | read_products |
| Customers | customers | read_customers |
| Orders | orders | read_orders, plus read_all_orders for orders older than 60 days |
| Inventory and locations | locations | read_inventory, read_locations |
| Pages, blogs and articles | pages, blogs, articles | read_content |
| Navigation menus | menus | read_online_store_navigation |
| URL redirects | urlRedirects | read_online_store_navigation |
| Content > Files | files | read_files |
| Metaobjects | metaobjects(type: "…") | read_metaobjects, read_metaobject_definitions |
| Discounts | discountNodes | read_discounts |
| Gift cards | giftCards | read_gift_cards |
read_all_orders needs no approval from Shopify on an app made in the store's own organization; apps made under a Partner organization have to request it first. Add read_legal_policies, read_translations and read_markets if you want those too. A scope added later reaches the store only after you release a new version and approve the change on the store.
For a store in the same organization as the app, swap the client ID and secret for an access token with the client credentials grant. The request is form-encoded; Shopify ignores a scope parameter. The token lasts 24 hours: expires_in is always 86399.
export SHOP=your-store
export CLIENT_ID='client-id-from-app-settings'
export CLIENT_SECRET='client-secret-from-app-settings'
TOKEN=$(curl -fsS -X POST "https://$SHOP.myshopify.com/admin/oauth/access_token" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_id=$CLIENT_ID" \
--data-urlencode "client_secret=$CLIENT_SECRET" | jq -er .access_token)The response's scope field lists what the store actually granted, which can be fewer than you asked for. Send the token in the X-Shopify-Access-Token header. Owning a store does not by itself put it in your organization: "same organization" means both appear under the same organization in the Dev Dashboard. The grant cannot reach a client's store at all; see the checklist below.
Customer names, emails, phone numbers and addresses are protected customer data. Shopify's rules make them always available to custom apps, while public apps need a review. Without that access the API does not fail outright: it returns null for those fields with an entry in errors. Query one customer normally before trusting a customer or order export.
Export products with a bulk operation
Paging through a large store one request at a time runs into Shopify's rate limits. A bulk operation runs the query on Shopify's side and gives you one JSONL file, one object per line. API version 2026-10 is the latest stable version (supported until October 16, 2027), and from 2026-01 each app can run up to five bulk queries per store at once. The examples use curl and jq 1.6 or later (apt install curl jq). Save the query to a file first:
{
products {
edges {
node {
id
handle
title
status
vendor
productType
tags
descriptionHtml
seo { title description }
metafields { edges { node { namespace key type value } } }
variants {
edges {
node { id title sku price compareAtPrice inventoryQuantity selectedOptions { name value } }
}
}
media { edges { node { id alt mediaContentType preview { image { url } } } } }
}
}
}
}A bulk query has one top-level field, at most five connections and at most two levels of nested connections; this one uses four connections, one level deep. Start it with the TOKEN from the step above:
API="https://$SHOP.myshopify.com/admin/api/2026-10/graphql.json"
jq -nc --rawfile q products.graphql \
'{query: "mutation($q: String!) { bulkOperationRunQuery(query: $q) { bulkOperation { id status } userErrors { field message } } }", variables: {q: $q}}' \
| curl -fsS -X POST "$API" \
-H "Content-Type: application/json" \
-H "X-Shopify-Access-Token: $TOKEN" \
--data-binary @- | jq .The answer carries an ID such as gid://shopify/BulkOperation/720918 and the status CREATED. Ask for it with bulkOperation(id:); from API version 2026-01 that replaces the deprecated currentBulkOperation. Repeat until the status is COMPLETED; FAILED and CANCELED mean it stopped.
OP_ID='gid://shopify/BulkOperation/720918'
jq -nc --arg id "$OP_ID" \
'{query: "query($id: ID!) { bulkOperation(id: $id) { status errorCode objectCount url } }", variables: {id: $id}}' \
| curl -fsS -X POST "$API" \
-H "Content-Type: application/json" \
-H "X-Shopify-Access-Token: $TOKEN" \
--data-binary @- | jq .When it completes, url is a signed link to the JSONL file. Download it with no token header; the link expires 7 days after the operation completes, and it is null when the query matched nothing:
curl -fsS -o "products-$(date -u +%Y-%m-%d).jsonl" 'https://storage.googleapis.com/shopify/…the url from the answer…'Variants, metafields and media come out as their own lines after their product, each with a __parentId holding the product's ID, in the same order as the connection. A query has 10 days to finish before Shopify marks it failed. If it fails with errorCode ACCESS_DENIED, a field needs a scope the app doesn't have; run the same query as a normal request to see which, and check partialDataUrl for what was exported before the failure.
Run the export every night
On a Linux server, this script asks for a fresh token every run, so the 24-hour expiry never matters. It runs one bulk query per .graphql file in a folder, pulls the live theme, packs the lot into one dated archive under a temporary name, and keeps 30 days. Put products.graphql from above in the folder, then one file for each other top-level query in the scopes table, for example customers.graphql:
{
customers {
edges {
node {
id
firstName
lastName
defaultEmailAddress { emailAddress marketingState }
tags
createdAt
defaultAddress { address1 address2 city province zip country phone }
}
}
}
}{
orders {
edges {
node {
id
name
createdAt
email
displayFinancialStatus
displayFulfillmentStatus
totalPriceSet { shopMoney { amount currencyCode } }
customer { id }
lineItems { edges { node { title sku quantity originalUnitPriceSet { shopMoney { amount currencyCode } } } } }
}
}
}
}The theme pull uses a password from Shopify's free Theme Access app, which is made for the CLI in scripts: install it, click Create theme password, and open the emailed link within 7 days; the password is shown once. It carries write_themes, so guard it like the client secret. Keep all four secrets in one file only root can read:
SHOPIFY_SHOP=your-store
SHOPIFY_CLIENT_ID=client-id-from-app-settings
SHOPIFY_CLIENT_SECRET=client-secret-from-app-settings
SHOPIFY_CLI_THEME_TOKEN=password-from-theme-access#!/bin/bash
set -euo pipefail
set -a
. /etc/shopify-backup/env
set +a
API="https://$SHOPIFY_SHOP.myshopify.com/admin/api/2026-10/graphql.json"
QUERIES="/etc/shopify-backup/queries"
BACKUP_DIR="/var/backups/shopify"
KEEP_DAYS=30
STAMP=$(date -u +%Y-%m-%d_%H%M)
WORK="$BACKUP_DIR/.work-$STAMP"
OUT="$BACKUP_DIR/shopify-$SHOPIFY_SHOP-$STAMP.tar.gz"
mkdir -p "$WORK/theme"
trap 'rm -rf "$WORK" "$OUT.partial"' EXIT
# A new token on every run: each one lasts 24 hours.
TOKEN=$(curl -fsS -X POST "https://$SHOPIFY_SHOP.myshopify.com/admin/oauth/access_token" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_id=$SHOPIFY_CLIENT_ID" \
--data-urlencode "client_secret=$SHOPIFY_CLIENT_SECRET" | jq -er .access_token)
gql() {
curl -fsS -X POST "$API" \
-H "Content-Type: application/json" \
-H "X-Shopify-Access-Token: $TOKEN" \
--data-binary @-
}
for file in "$QUERIES"/*.graphql; do
name=$(basename "$file" .graphql)
started=$(jq -nc --rawfile q "$file" \
'{query: "mutation($q: String!) { bulkOperationRunQuery(query: $q) { bulkOperation { id } userErrors { field message } } }", variables: {q: $q}}' | gql)
id=$(jq -er '.data.bulkOperationRunQuery.bulkOperation.id' <<<"$started") \
|| { echo "$name: $started" >&2; exit 1; }
while :; do
sleep 10
op=$(jq -nc --arg id "$id" \
'{query: "query($id: ID!) { bulkOperation(id: $id) { status errorCode objectCount url } }", variables: {id: $id}}' | gql)
case $(jq -r '.data.bulkOperation.status' <<<"$op") in
COMPLETED) break ;;
CREATED|RUNNING) ;;
*) echo "$name: $op" >&2; exit 1 ;;
esac
done
url=$(jq -r '.data.bulkOperation.url // empty' <<<"$op")
if [ -n "$url" ]; then
curl -fsS -o "$WORK/$name.jsonl" "$url"
else
: > "$WORK/$name.jsonl" # the query matched nothing
fi
echo "$name: $(jq -r '.data.bulkOperation.objectCount' <<<"$op") objects"
done
# The live theme; the CLI reads the Theme Access password from SHOPIFY_CLI_THEME_TOKEN.
shopify theme pull --store "$SHOPIFY_SHOP" --live --path "$WORK/theme" --no-input
tar -czf "$OUT.partial" -C "$WORK" .
mv "$OUT.partial" "$OUT"
find "$BACKUP_DIR" -name "shopify-$SHOPIFY_SHOP-*.tar.gz" -type f -mtime +"$KEEP_DAYS" -deletechmod 600 /etc/shopify-backup/env
chmod 755 /usr/local/bin/shopify-backup.sh
/usr/local/bin/shopify-backup.shPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
30 2 * * * root flock -n /run/lock/shopify-backup.lock /usr/local/bin/shopify-backup.sh >> /var/log/shopify-backup.log 2>&1set -o pipefail makes a failed token request or download stop the run instead of leaving a half-written file, a failed bulk operation prints Shopify's answer to the log, and the archive only gets its real name once it is complete. flock -n stops two runs overlapping. The queries run one after another, which keeps well inside the five-at-once limit. A copy on the same server is not a backup of that server, so send the archives somewhere else, for example with rclone, and see cron backup schedules for picking the time.
What you can put back, and what you can't
| Data | How it goes back | What doesn't come back |
|---|---|---|
| Products | Products > Import > Add file, tick Overwrite products with matching handles, Import products | Collections membership, variant metafields, the original product IDs of deleted products |
| Inventory | Products > Inventory > Import, CSV up to 15 MB | Quantities only for locations that exist in the store |
| Customers | Customers > Import > Add file > Import customers | Passwords |
| Theme | Online Store > Draft themes > Import theme > Upload zip file, or shopify theme push --unpublished | Timeline history |
| URL redirects | Content > Menus > URL redirects > Import | Nothing |
| Orders | Not through the admin. Shopify points to apps or the API and calls moving orders "not supported by Shopify" | Keep the export as your record |
| Discount codes, issued gift cards | They can't be transferred | Recreate them by hand |
| Pages, posts, menus, metaobjects, files | By hand from the JSONL, or through the API with the matching write scopes | The original IDs; each item is created again |
With Overwrite products with matching handles ticked, an empty cell in a column that is present blanks that field on the live product, changing Option1 to Option3 values deletes the variants' IDs and makes new ones, and an import cannot be cancelled or undone. Import into a test store first, or import only the rows you need.
Customer imports keep the Accepts Email Marketing yes or no column, so marketing consent comes back. Ticking Overwrite existing customers that have the same email or phone number replaces all of that customer's data. With classic customer accounts, invite imported customers to set a new password; with new customer accounts, customers can sign in with a one-time 6-digit code instead of a password.
For a theme, push to a new unpublished theme, check it in the preview, then publish it. shopify theme push deletes files on the remote theme that are not in your folder, so add --nodelete when you are restoring a few files into an existing theme:
shopify theme push --store your-store --unpublished --theme "Restored 2026-10-29" --path ./theme-2026-10-29Test this on a development store before you need it: import the products and customers CSVs, push the theme, and compare product and customer counts with the live store. Testing restores shows how to make that routine.
Before you work on someone's store
Freelancers and agencies usually get a collaborator account, which cannot open the Dev Dashboard, and the client credentials grant cannot reach a store outside your own organization. An app made in your own organization with custom distribution, installed by the client from its install link, is different: the store approves it once, and you then get its token with the authorization code grant. Without either, the admin and the CLI are your tools. Before the first change:
- Duplicate the live theme and make your changes on the copy. Publish it only when it is ready, and keep the old theme unpublished as your way back.
- Export products, customers and orders as CSV, and inventory if the store has more than one location. Wait for the emailed files before you start.
- Pull the live theme with
shopify theme pull --liveinto a folder you commit to Git. - List every installed app and its settings, with screenshots, and note which ones add code to the theme.
- Export URL redirects, and copy any pages or menus you will touch.
- Keep all of it outside the store, in storage you or the client control. A duplicate theme is lost with the store.
For a regular backup of a client's store, the owner can give you a staff account with the App developer role so you can make the app in their organization, or make it themselves and send you the credentials.
Common errors
| Error | Fix |
|---|---|
Invalid API key or access token | You sent the client ID or secret, or a token older than 24 hours. Request a new token and send it in X-Shopify-Access-Token. |
| The token request fails for a client's store | Client credentials only work when the app and the store are in the same organization. Make the app in the store's organization. |
| Orders stop 60 days back | Add read_all_orders, release a new version and approve it on the store. |
A new scope isn't in the token's scope | Scope changes reach the store only through a released version the store approves. |
Bulk operation FAILED with ACCESS_DENIED | A field needs a missing scope. Run the query normally to find it. |
Customer names and emails are null | The app has no protected customer data access; the reason is in the response's errors. |
bulkOperationRunQuery returns no operation | Read its userErrors. One cause: five bulk queries are already running for this app on the store; wait for one to finish or cancel it. |
| The product import fails or times out | The CSV is over 15 MB. Split it into smaller files. |
shopify theme pull stops to ask which theme | In a script, pass --live, --development or --theme. |
Frequently asked questions
- Does Shopify back up my store?
- Not in a way you can restore from. Shopify's backup page tells you to export CSV files, download your theme or use an app, and deleted products can't be restored.
- Can I restore a deleted Shopify product?
- Not from Shopify. Import it again from a product CSV you exported earlier; it comes back as a new product with a new ID. Archive products you might want later instead of deleting them.
- Can I import orders into Shopify from a CSV?
- No. Orders can be exported but not imported through the admin. Keep the export as a record; moving orders needs an app or the API, which Shopify doesn't support for migrations.
- Do customer passwords survive a Shopify export?
- No. The customer CSV has no passwords. With classic customer accounts, invite customers to set a new one; with new customer accounts they sign in with an emailed code.
- Can I still create a custom app in the Shopify admin?
- Not since January 1, 2026. Existing ones keep working. New apps are made in the Dev Dashboard and get tokens with the client credentials grant, which last 24 hours.
- How often should I back up a Shopify store?
- Export nightly at least, and always just before a theme change, a bulk product import or a new app. Shopify gives you no earlier copy to fall back on, so your newest export is your restore point.
How this was checked
Commands, limits and prices were checked against these official pages, on October 29, 2026:
- Shopify Help Center: Backups and duplication
- Shopify Help Center: CSV files (supported imports and exports)
- Shopify Help Center: Exporting products
- Shopify Help Center: Using CSV files to import and export products
- Shopify Help Center: Importing products with a CSV file
- Shopify Help Center: Adding and updating products (deleting products)
- Shopify Help Center: Importing and exporting customers
- Shopify Help Center: New customer accounts
- Shopify Help Center: Exporting orders
- Shopify Help Center: Inventory CSV
- Shopify Help Center: Managing purchased gift cards
- Shopify Help Center: URL redirects
- Shopify Help Center: Duplicating themes
- Shopify Help Center: Downloading themes
- Shopify Help Center: Adding themes
- Shopify Help Center: Editing theme code (Timeline)
- Shopify Help Center: Custom apps
- Shopify Changelog: Legacy custom apps can't be created after January 1, 2026
- Shopify Dev: Generate access tokens for admin-created custom apps (legacy)
- Shopify Dev: Create apps using the Dev Dashboard
- Shopify Dev: Get API access tokens for Dev Dashboard apps (client credentials grant)
- Shopify Dev: Access scopes
- Shopify Dev: Work with protected customer data
- Shopify Dev: API versioning (2026-10 latest stable)
- Shopify Dev: GraphQL Admin API reference (endpoint and headers)
- Shopify Dev: Perform bulk operations with the GraphQL Admin API
- Shopify Dev: bulkOperationRunQuery mutation
- Shopify Dev: bulkOperation query
- Shopify Dev: Product object (2026-10)
- Shopify Dev: Customer object (2026-10)
- Shopify Dev: Order object (2026-10)
- Shopify Dev: Menu object (2026-10)
- Shopify Dev: Shopify CLI (install and requirements)
- Shopify Dev: shopify theme pull
- Shopify Dev: shopify theme push
- Shopify Dev: Theme Access app