Shopify URL redirect CSV format: headers, rules and why imports fail
Everything Shopify documents about the bulk redirect file, in one place, with the source next to each rule. Where the docs are silent, this page says so instead of guessing. When you have a file, run it through the linter before you upload.
The file
Shopify's own template is two lines. The header is Redirect from,Redirect to, exactly that, and each row after it is one redirect:
Redirect from,Redirect to /example_product.php,/products/example-product
A realistic file looks like this. Paths in the first column, paths or full URLs in the second, fields with commas wrapped in double quotes, UTF-8, one redirect per line:
Redirect from,Redirect to /about-us,/pages/about /old-shirts,/collections/shirts /products/blue-tee-2019,/products/blue-tee /blog/2019/03/launch,https://blog.example.com/launch "/search?q=red,blue",/collections/red-and-blue
There is no status column. Every Shopify URL redirect is served as a 301. There is no wildcard column either: each row matches one exact path. Import it from Online Store, Navigation, URL redirects, Import (in newer admins, Content, Menus, URL redirects), then Add file, Upload file, review, Import redirects.
Every rule, with its source
Sources: [H] Shopify help, URL redirects. [T] Shopify's sample CSV template. [R] Shopify Admin REST API, Redirect resource. [G] Shopify Admin GraphQL API, UrlRedirectImport object. Links at the bottom.
Header is Redirect from,Redirect to
The template [T] uses this header and nothing else. Shopify does not document whether case or quoting matters, so "Redirect From","Redirect To" gets a warning, not an error, and the linter writes the template header in the fixed file.
Redirect from is a path on your store, starting with /
[H]: use "a relative URL, such as /collection/shirts". [T]'s row is /example_product.php. The importer matches the path of the request on your store's domain, so https://shop.example.com/old or old-page in this column can never match. The linter strips the domain and adds the slash.
Redirect to is a path or a full URL
[H]: "The new URL can be a relative URL, such as /collection/shirts, or a full URL, such as http://www.example.com/collection/shirts." A relative target without its slash, a bare domain without a scheme, or a // URL is not one of the documented forms; each gets a warning and a fix.
Blocked prefixes: /apps, /application, /cart, /carts, /orders, /services, /shop
[H]: "You can't redirect URLs that begin with the following prefixes: /apps, /application, /cart, /carts, /orders, /services, or /shop". The docs say "begin with", and do not say whether that means the whole path segment or just the letters. The linter treats /cart and /cart/anything as the documented error and /cartoon or /shopping-guide as a warning, so a valid row is never dropped on a guess.
Fixed paths: /products, /collections, /collections/all
[H]: "You can't redirect URLs that use fixed Shopify paths: /products, /collections, /collections/all." Paths under them, such as /products/old-handle, are fine and are the most common redirect there is.
Reserved URL paths: /collections/vendors, /collections/types, /a/, /community/, /tools/
Listed under "Reserved URL paths" in [H]. The last three are written with a trailing slash, so the linter flags paths under them (/a/proxy) and leaves /a itself alone.
Not on Shopify's list: /admin, /checkout, /account
These are Shopify system routes, but [H] does not name them. The importer may accept the row; the redirect will not fire while Shopify owns the path. Warning only.
Length: path 1,024 characters, target 255
[R]: path "(maximum: 1024 characters)", target "(maximum: 255 characters)". The CSV importer feeds the same objects, so a longer field cannot be created.
No blank fields, no redirect to itself
[R] returns 422 with "path can't be blank", "target can't be blank" and "target can't be the same as path". A .html page cannot redirect to the same path without .html either: [H], "URLs ending in .html cannot be redirected to the same URL without .html."
One target per path
A path can only redirect to one place. [G] reports createdCount and updatedCount for an import, which means a row whose path already exists updates it; which of two rows in the same file wins is not documented. The linter drops the exact duplicate and flags the conflict so you decide.
Chains and loops
Nothing in [H] stops you importing /a to /b and /b to /c. Visitors and crawlers then pay two 301s. The linter points /a straight at /c. A loop (/a to /b to /a) gives visitors a browser error and is dropped.
Query strings: kept, but may not work
[H]: "URLs containing query strings (such as /brands/dell.html?_bc_fsnf=1&interface=interface_type) might not work as expected". The row stays; add one for the path without the query too. A #fragment never reaches the server and is removed.
Limits: 100,000 redirects per store, 20,000,000 on Plus
[H]: "You can create a maximum of 100,000 URL redirects unless your store is on the Plus plan, which has a maximum of 20,000,000 URL redirects." Existing redirects count. No per-file or per-import row limit is documented.
Only broken URLs get redirected
[H]: "You can redirect only from broken URLs." A row whose Redirect from still returns a page does nothing until that page is gone. The file cannot tell you this; the Pro live check requests each old URL and reports the ones that still answer 200.
Not documented: trailing slashes, letter case, spaces, byte-order marks, failed-row reporting
Shopify does not say whether /old/ and /old are one path, whether matching is case-sensitive, how a raw space or a UTF-8 BOM from Excel is treated, or how failed rows are reported beyond the import counts in [G]. The linter warns on each of these and applies only the harmless fix: trim, encode spaces as %20, drop the BOM.
What the usual failures mean
The import finished but the count did not go up by the number of rows. Rows failed one by one and the rest went in. Shopify emails the result to the account that ran the import; the admin only shows a total. The rows that fail are almost always a domain or a missing slash in Redirect from, a reserved path, or a blank field.
The redirects imported but do not redirect. Three things to check. The old URL still returns a page: Shopify redirects only broken URLs, so the redirect waits until the page is deleted or unpublished. The path is one Shopify owns (/cart, /products, /account). The path has a query string, which Shopify says "might not work as expected".
Some rows imported, others are missing, no error. Two rows had the same Redirect from. One target per path: the importer kept one of them. Look for the conflict, pick the right target, remove the other row.
The file is refused before the import starts. The header is not Redirect from,Redirect to, the file is not a CSV (an Excel workbook saved as .xlsx, or a CSV with a different delimiter), or the encoding is not UTF-8. Save again as CSV UTF-8 and check the first line.
All of these are visible before you upload. Paste the file into the linter; it lists each row with the line number and the fix, and gives you a cleaned file.
Sources, fetched 9 September 2026: [H] Shopify help: URL redirects · [T] sample redirect CSV template · [R] Admin REST API: Redirect · [G] Admin GraphQL API: UrlRedirectImport.