Convert Shopify Orders JSON to CSV
Shopify orders are nested three levels deep — orders wrap line items, which wrap variant details. Getting a flat CSV for analysis means choosing whether you want one row per order or one row per line item.
Shopify's Orders API is the most common Shopify-to-spreadsheet task: order exports for accounting, inventory reconciliation, customer analysis, and returns processing. The response wraps all orders in an 'orders' key, and each order contains a line_items array, a shipping_address object, tax_lines, discount_codes, and fulfillments. This guide covers the two most useful output shapes: one row per order (summarized) and one row per line item (expanded).
Want to skip the code? Paste your JSON directly into the converter — it handles nested objects, arrays, and large files automatically.
Open JSON to CSV ConverterWhat the API returns
Shopify GET /admin/api/2024-01/orders.json — single order trimmed
{
"orders": [
{
"id": 5678901234,
"order_number": 1042,
"email": "alice@example.com",
"financial_status": "paid",
"fulfillment_status": "fulfilled",
"total_price": "129.95",
"subtotal_price": "119.95",
"total_tax": "10.00",
"currency": "USD",
"created_at": "2025-03-15T10:23:45-05:00",
"line_items": [
{
"id": 11111,
"title": "Classic T-Shirt",
"variant_title": "Blue / Large",
"quantity": 2,
"price": "29.99",
"sku": "TSHIRT-BL-LG",
"vendor": "Acme Apparel"
},
{
"id": 11112,
"title": "Canvas Tote Bag",
"variant_title": null,
"quantity": 1,
"price": "59.97",
"sku": "TOTE-NAT",
"vendor": "Acme Apparel"
}
],
"shipping_address": {
"name": "Alice Chen",
"city": "Austin",
"province": "Texas",
"country": "United States",
"zip": "78701"
}
}
]
}Field mapping: JSON path → CSV column
| JSON path | CSV column | Notes |
|---|---|---|
| id | order_id | Shopify internal order ID |
| order_number | order_number | Human-readable (#1042) |
| customer_email | ||
| financial_status | financial_status | paid, pending, refunded |
| fulfillment_status | fulfillment_status | fulfilled, partial, null |
| total_price | total_price | String — cast to float for math |
| currency | currency | |
| created_at | created_at | ISO 8601 with timezone |
| line_items[].title | product_title | One row per item in expanded mode |
| line_items[].sku | sku | |
| line_items[].quantity | quantity | |
| line_items[].price | unit_price | String — cast to float |
| shipping_address.city | ship_city | |
| shipping_address.zip | ship_zip | Keep as string (leading zeros) |
total_price, subtotal_price, and line item prices are strings in the Shopify API, not numbers. Cast with float() or pd.to_numeric() before arithmetic.
Python conversion
One row per order — summarized view
import pandas as pd
import json
with open("shopify_orders.json") as f:
data = json.load(f)
orders = data["orders"]
rows = []
for order in orders:
rows.append({
"order_id": order["id"],
"order_number": order["order_number"],
"email": order.get("email", ""),
"financial_status": order.get("financial_status"),
"fulfillment_status": order.get("fulfillment_status"),
"total_price": float(order.get("total_price", 0)),
"total_tax": float(order.get("total_tax", 0)),
"currency": order.get("currency"),
"item_count": sum(li["quantity"] for li in order.get("line_items", [])),
"ship_city": order.get("shipping_address", {}).get("city"),
"ship_zip": str(order.get("shipping_address", {}).get("zip", "")),
"created_at": order.get("created_at", "")[:10], # date only
})
pd.DataFrame(rows).to_csv("orders.csv", index=False, encoding="utf-8-sig")
print(f"Exported {len(rows)} orders")One row per line item — expanded view (best for inventory / product analysis)
import pandas as pd
import json
with open("shopify_orders.json") as f:
data = json.load(f)
rows = []
for order in data["orders"]:
base = {
"order_id": order["id"],
"order_number": order["order_number"],
"email": order.get("email", ""),
"financial_status": order.get("financial_status"),
"order_total": float(order.get("total_price", 0)),
"currency": order.get("currency"),
"created_at": order.get("created_at", "")[:10],
"ship_city": order.get("shipping_address", {}).get("city"),
}
for item in order.get("line_items", []):
rows.append({
**base,
"line_item_id": item["id"],
"product_title": item.get("title"),
"variant": item.get("variant_title"),
"sku": item.get("sku"),
"quantity": item.get("quantity"),
"unit_price": float(item.get("price", 0)),
"line_total": float(item.get("price", 0)) * item.get("quantity", 0),
"vendor": item.get("vendor"),
})
pd.DataFrame(rows).to_csv("order_line_items.csv", index=False, encoding="utf-8-sig")
print(f"Exported {len(rows)} line items")Common issues with this API
Prices are strings, not numbers
Unlike Stripe (which uses integer cents), Shopify returns prices as decimal strings: '29.99', '0.00'. You must cast them before any arithmetic. Use float() or pd.to_numeric() — failing to do so means Excel will sort them as text.
df["total_price"] = pd.to_numeric(df["total_price"], errors="coerce")fulfillment_status is null (not 'unfulfilled') for new orders
Shopify returns null for orders that haven't been fulfilled yet — not the string 'unfulfilled'. Filter accordingly.
# Filter unfulfilled orders
unfulfilled = [o for o in orders if o.get("fulfillment_status") is None]
# In pandas
df["fulfillment_status"] = df["fulfillment_status"].fillna("unfulfilled")Pagination: 250 orders per page maximum
Shopify's REST API returns at most 250 orders per request and uses Link headers for pagination. Use page_info cursors (not page numbers) for large exports.
import requests
headers = {"X-Shopify-Access-Token": "shpat_..."}
url = "https://mystore.myshopify.com/admin/api/2024-01/orders.json?limit=250&status=any"
orders = []
while url:
r = requests.get(url, headers=headers)
orders.extend(r.json()["orders"])
link = r.headers.get("Link", "")
# Parse next page cursor from Link header
url = None
for part in link.split(","):
if 'rel="next"' in part:
url = part.split(";")[0].strip().strip("<>")
break
print(f"Fetched {len(orders)} orders")Frequently asked questions
Should I use one row per order or one row per line item?
Use one row per order for financial reporting, customer analysis, or anything where the order total is the unit. Use one row per line item for inventory analysis, product performance, or when you need SKU-level data. The two shapes serve different purposes — pick based on what the recipient will do with the CSV.
How do I export Shopify orders without writing code?
Shopify's admin has a built-in export under Orders → Export. For the API JSON format, paste it into the JSON to CSV converter — it handles the nested line_items by expanding them to multiple rows automatically.
Why are some orders missing an email address?
Shopify allows guest checkouts without email, and some B2B orders use a company email on the billing address rather than the order email field. Check billing_address.email as a fallback: order.get('email') or order.get('billing_address', {}).get('email', '').
How do I include discount codes in the CSV?
Discount codes are in the discount_codes array on each order. Each entry has a code and amount field. For one-row-per-order output, join the codes: ', '.join(d['code'] for d in order.get('discount_codes', [])).