How to Automate Shopify Reports: Complete Python Guide
Set up fully automatic Shopify reports — daily sales summaries, inventory alerts, and customer analytics — using Python and the Shopify Admin API with scheduling and email delivery.
AI Generated ImageYou should not be making Shopify reports by hand. Not daily ones, not weekly ones, not any of them.
Every piece of data in those reports already exists in the Shopify Admin API. The problem is not access — it is that nobody has wired the API output to a formatted report that sends itself. This guide does exactly that: fetch the data, shape it into the metrics that matter, generate a formatted report, and deliver it on a schedule.
By the end you will have a Python script that runs daily, pulls your store data, and emails a polished sales report to your team.
# Who This Is For
- Store owners who log into Shopify every morning to check yesterday's numbers
- Ecommerce managers who build weekly reports in Google Sheets or Excel from exported CSV files
- Vibe coders who want to build a reporting pipeline that runs itself
- Operations teams who need consistent metrics delivered to stakeholders without manual work
If you can read a spreadsheet, you can follow this guide. Python knowledge helps but the concepts are straightforward.
# The Automation Architecture
flowchart TD CRON["Scheduler\n(cron / Prefect)"] --> FETCH FETCH["Fetch from\nShopify API"] --> CLEAN["Clean &\nValidate"] CLEAN --> AGG["Aggregate\nMetrics"] AGG --> REPORT["Generate\nFormatted Report"] AGG --> DB[(Store\nHistory)] REPORT --> EMAIL["Email to\nTeam"] DB --> TREND["Week-over-Week\nComparisons"]
# What You Will Need
pip install requests pandas openpyxl jinja2
- A Shopify store with a custom app (Admin API access token with
read_orders,read_products,read_customersscopes) - An SMTP-capable email account for delivery (Gmail app password works for small-scale use)
# Step 1: Set Up the Shopify API Client
The foundation is a clean API client that handles pagination and rate limiting.
import requests
import time
from datetime import datetime, timedelta
class ShopifyReporter:
"""Fetch and aggregate Shopify data for automated reporting."""
def __init__(self, shop_url: str, api_token: str):
"""Initialise the Shopify API connection."""
self.base_url = f"https://{shop_url}/admin/api/2026-01"
self.session = requests.Session()
self.session.headers["X-Shopify-Access-Token"] = api_token
self.session.headers["Content-Type"] = "application/json"
def _get_paginated(self, endpoint: str, params: dict = None) -> list:
"""Fetch all pages from a Shopify API endpoint."""
url = f"{self.base_url}/{endpoint}"
params = params or {}
results = []
while url:
response = self.session.get(url, params=params)
# Handle rate limiting
if response.status_code == 429:
retry_after = float(response.headers.get("Retry-After", 2))
print(f"Rate limited. Waiting {retry_after}s...")
time.sleep(retry_after)
continue
response.raise_for_status()
data = response.json()
# Extract items from response
key = list(data.keys())[0]
results.extend(data[key])
# Check for next page via Link header
link = response.headers.get("Link", "")
url = None
params = {}
if 'rel="next"' in link:
for part in link.split(","):
if 'rel="next"' in part:
url = part.split("<")[1].split(">")[0]
return results
# Step 2: Fetch and Aggregate Order Data
The report needs three views: daily totals, product performance, and customer metrics.
import pandas as pd
class DailyReportBuilder:
"""Build a daily Shopify report from raw API data."""
def __init__(self, shopify: ShopifyReporter):
"""Initialise with a connected Shopify client."""
self.shopify = shopify
def fetch_orders(self, days: int = 7) -> pd.DataFrame:
"""Fetch orders from the last N days."""
since = (datetime.now() - timedelta(days=days)).isoformat()
raw = self.shopify._get_paginated("orders.json", {
"created_at_min": since,
"status": "any",
"limit": 250,
})
print(f"Fetched {len(raw)} orders from last {days} days")
orders = []
for o in raw:
orders.append({
"order_id": o["id"],
"created_at": pd.to_datetime(o["created_at"]),
"total": float(o["total_price"]),
"subtotal": float(o.get("subtotal_price", 0)),
"currency": o["currency"],
"items": len(o.get("line_items", [])),
"status": o.get("financial_status", "unknown"),
"source": o.get("source_name", "unknown"),
"customer_email": o.get("email", ""),
})
df = pd.DataFrame(orders)
df["date"] = df["created_at"].dt.date
return df
def daily_summary(self, df: pd.DataFrame) -> pd.DataFrame:
"""Aggregate orders into daily summary metrics."""
return (
df.groupby("date")
.agg(
orders=("order_id", "count"),
revenue=("total", "sum"),
avg_order_value=("total", "mean"),
items_sold=("items", "sum"),
unique_customers=("customer_email", "nunique"),
)
.round(2)
.reset_index()
.sort_values("date")
)
def product_performance(self, days: int = 7) -> pd.DataFrame:
"""Get top-selling products by revenue."""
since = (datetime.now() - timedelta(days=days)).isoformat()
raw = self.shopify._get_paginated("orders.json", {
"created_at_min": since,
"status": "any",
"limit": 250,
})
items = []
for order in raw:
for item in order.get("line_items", []):
items.append({
"product": item["title"],
"variant": item.get("variant_title", "default"),
"quantity": item["quantity"],
"revenue": float(item["price"]) * item["quantity"],
})
df = pd.DataFrame(items)
return (
df.groupby("product")
.agg(units=("quantity", "sum"), revenue=("revenue", "sum"))
.sort_values("revenue", ascending=False)
.head(20)
.round(2)
.reset_index()
)
# Sample Output
Daily Sales Summary (Last 7 Days)
──────────────────────────────────
Date │ Orders │ Revenue │ AOV │ Items │ Customers
2026-05-03 │ 42 │ £2,940.00 │ £70.00 │ 68 │ 38
2026-05-04 │ 55 │ £4,125.00 │ £75.00 │ 89 │ 51
2026-05-05 │ 38 │ £2,660.00 │ £70.00 │ 57 │ 34
2026-05-06 │ 61 │ £4,575.00 │ £75.00 │ 95 │ 54
2026-05-07 │ 47 │ £3,525.00 │ £75.00 │ 72 │ 42
2026-05-08 │ 53 │ £3,975.00 │ £75.00 │ 84 │ 49
2026-05-09 │ 49 │ £3,430.00 │ £70.00 │ 76 │ 44
# Step 3: Generate a Formatted Report
Raw data is not a report. Use Jinja2 templates to produce a polished HTML email.
from jinja2 import Template
REPORT_TEMPLATE = """
<html>
<body style="font-family: Arial, sans-serif; max-width: 700px; margin: 0 auto;">
<h2>Daily Sales Report — {{ date }}</h2>
<h3>Summary</h3>
<table style="border-collapse: collapse; width: 100%;">
<tr style="background: #f3f4f6;">
<th style="padding: 8px; text-align: left;">Metric</th>
<th style="padding: 8px; text-align: right;">Value</th>
<th style="padding: 8px; text-align: right;">vs Last Week</th>
</tr>
{% for row in summary %}
<tr>
<td style="padding: 8px;">{{ row.metric }}</td>
<td style="padding: 8px; text-align: right;">{{ row.value }}</td>
<td style="padding: 8px; text-align: right; color: {{ row.color }};">{{ row.change }}</td>
</tr>
{% endfor %}
</table>
<h3>Top Products</h3>
<table style="border-collapse: collapse; width: 100%;">
<tr style="background: #f3f4f6;">
<th style="padding: 8px; text-align: left;">Product</th>
<th style="padding: 8px; text-align: right;">Units</th>
<th style="padding: 8px; text-align: right;">Revenue</th>
</tr>
{% for p in products %}
<tr>
<td style="padding: 8px;">{{ p.product }}</td>
<td style="padding: 8px; text-align: right;">{{ p.units }}</td>
<td style="padding: 8px; text-align: right;">£{{ "%.2f"|format(p.revenue) }}</td>
</tr>
{% endfor %}
</table>
</body>
</html>
"""
class ReportFormatter:
"""Format aggregated data into a polished HTML report."""
def __init__(self, daily: pd.DataFrame, products: pd.DataFrame):
"""Initialise with aggregated data."""
self.daily = daily
self.products = products
def render_html(self) -> str:
"""Render the report as HTML."""
today = self.daily.iloc[-1] if len(self.daily) > 0 else {}
last_week = self.daily.iloc[-8] if len(self.daily) > 7 else today
summary = [
{
"metric": "Revenue",
"value": f"£{today.get('revenue', 0):,.2f}",
"change": self._pct_change(today.get("revenue", 0), last_week.get("revenue", 1)),
"color": "#22c55e" if today.get("revenue", 0) >= last_week.get("revenue", 0) else "#ef4444",
},
{
"metric": "Orders",
"value": str(int(today.get("orders", 0))),
"change": self._pct_change(today.get("orders", 0), last_week.get("orders", 1)),
"color": "#22c55e" if today.get("orders", 0) >= last_week.get("orders", 0) else "#ef4444",
},
{
"metric": "Avg Order Value",
"value": f"£{today.get('avg_order_value', 0):,.2f}",
"change": self._pct_change(today.get("avg_order_value", 0), last_week.get("avg_order_value", 1)),
"color": "#22c55e",
},
]
template = Template(REPORT_TEMPLATE)
return template.render(
date=today.get("date", ""),
summary=summary,
products=self.products.head(10).to_dict("records"),
)
def _pct_change(self, current, previous) -> str:
"""Calculate percentage change as a formatted string."""
if previous == 0:
return "—"
change = ((current - previous) / previous) * 100
prefix = "+" if change > 0 else ""
return f"{prefix}{change:.1f}%"
# Step 4: Automate Email Delivery
Send the formatted report via SMTP. This works with Gmail, Amazon SES, or any SMTP provider.
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import os
def send_report(html_content: str, recipients: list[str]):
"""Send the HTML report via email."""
smtp_host = os.environ["SMTP_HOST"]
smtp_port = int(os.environ.get("SMTP_PORT", 587))
smtp_user = os.environ["SMTP_USER"]
smtp_pass = os.environ["SMTP_PASS"]
msg = MIMEMultipart("alternative")
msg["Subject"] = f"Daily Sales Report — {datetime.now().strftime('%Y-%m-%d')}"
msg["From"] = smtp_user
msg["To"] = ", ".join(recipients)
msg.attach(MIMEText(html_content, "html"))
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.starttls()
server.login(smtp_user, smtp_pass)
server.send_message(msg)
print(f"Report sent to {len(recipients)} recipients")
# Step 5: Schedule the Full Pipeline
Wire everything together and run it on a schedule. Use cron for simplicity or Prefect for monitoring.
def run_daily_report():
"""Execute the full daily reporting pipeline."""
shop_url = os.environ["SHOPIFY_SHOP_URL"]
api_token = os.environ["SHOPIFY_API_TOKEN"]
shopify = ShopifyReporter(shop_url, api_token)
builder = DailyReportBuilder(shopify)
# Fetch and aggregate
orders = builder.fetch_orders(days=7)
daily = builder.daily_summary(orders)
products = builder.product_performance(days=7)
# Format and send
formatter = ReportFormatter(daily, products)
html = formatter.render_html()
recipients = os.environ.get("REPORT_RECIPIENTS", "").split(",")
send_report(html, recipients)
print("Daily report complete")
if __name__ == "__main__":
run_daily_report()
# Cron Schedule
# Run daily at 8am
0 8 * * * cd /app && python shopify_report.py
# What This Replaces
| Manual Process | Automated Equivalent |
|---|---|
| Log into Shopify admin every morning | API fetches data automatically |
| Export CSV, open in Excel | Data loaded directly into pandas |
| Calculate daily totals manually | Aggregation runs in seconds |
| Format a spreadsheet for stakeholders | HTML template generates polished report |
| Copy-paste into email | SMTP sends automatically |
| Compare to last week from memory | Historical data enables precise comparisons |
# Next Steps
For building a unified reporting layer across multiple ecommerce platforms, see Ecommerce Reporting API: Automate Store Data Collection. To pull more granular data from the Shopify API, see Shopify Reporting API: Pull Sales, Inventory, and Customer Data.
If your store also has performance issues, see How to Fix Slow Shopify Stores. For scheduling this pipeline with retries and monitoring, see Schedule and Orchestrate Workflows with Prefect.
For securing the API tokens used in this pipeline, see Secure Python Automation: Managing Secrets and Keys. Once reports are running reliably, set up Shopify performance monitoring with automated alerts to catch revenue drops or speed regressions the moment they occur.
Ecommerce optimisation services include building automated reporting pipelines for Shopify and other platforms.
Get in touch to discuss automating your Shopify reports.
Enjoyed this article?
Get notified when I publish new articles on automation, ecommerce, and data engineering.