Export Campaign Reports to CSV
Export Hardal campaign reports to CSV with Python, complete API pagination, and preserve attribution and currency context.
Use the Acquisition API when you need campaign aggregates in a spreadsheet or another reporting tool. This exports report rows, not raw events or individual visitor histories.
Keep one report definition
Use a tenant-scoped API key. Fix the tenant, optional source, dates, timezone, grouping, and attribution settings across all requests. Use a completed period to reduce changes while paging; offset pagination is not a frozen data snapshot.
The example groups by campaign and uses the tenant's configured attribution settings. Review the effective controls in the response. For an explicit override, use the supported parameters in the Acquisition reference.
Export with pagination
Set HARDAL_API_KEY, HARDAL_TENANT_ID, HARDAL_START_DATE, and HARDAL_END_DATE in your server environment. The example uses Python's standard library and the API's trafficDistribution.rows and trafficDistribution.pagination fields.
import csv
import json
import os
from datetime import date
from pathlib import Path
from urllib.parse import urlencode
from urllib.request import Request, urlopen
start = date.fromisoformat(os.environ["HARDAL_START_DATE"])
end = date.fromisoformat(os.environ["HARDAL_END_DATE"])
if end < start:
raise ValueError("End date must not precede start date")
params = {
"tenantId": os.environ["HARDAL_TENANT_ID"],
"timeframe": "custom", "startDate": str(start), "endDate": str(end),
"timezone": "Europe/Istanbul", "groupBy": "campaign",
"sortBy": "campaign", "sortDirection": "asc", "limit": 500,
}
if os.environ.get("HARDAL_SOURCE_ID"):
params["sourceId"] = os.environ["HARDAL_SOURCE_ID"]
rows, offset, context = [], 0, None
while True:
if offset > 10000:
raise RuntimeError("Pagination limit reached; narrow the report before exporting")
request = Request(
"https://api.hardal.ai/v1/reports/acquisition?"
+ urlencode({**params, "offset": offset}),
headers={"Authorization": "Bearer " + os.environ["HARDAL_API_KEY"]},
)
with urlopen(request, timeout=60) as response:
result = json.load(response)
if result.get("success") is not True:
raise RuntimeError("The report request did not succeed")
data = result["data"]
current = {"range": data["range"], "controls": data["controls"],
"currency": data.get("currency")}
if context is not None and context != current:
raise RuntimeError("Report settings changed during export; run it again")
context = current
table = data["trafficDistribution"]
batch = table["rows"]
pagination = table["pagination"]
if pagination["offset"] != offset:
raise RuntimeError("Unexpected page offset; export stopped")
total = pagination["total"]
if not isinstance(total, int) or total < 0:
raise RuntimeError("The response has no valid total row count")
rows.extend(batch)
offset += len(batch)
if offset >= total:
break
if not batch:
raise RuntimeError("Incomplete result; no further rows were returned")
def csv_value(value):
if isinstance(value, str) and value.lstrip().startswith(("=", "+", "-", "@")):
return "'" + value
return value
fields = ["campaign", "sessions", "conversions", "conversion_rate", "revenue"]
output = Path(f"hardal-campaigns-{start}-{end}.csv")
with output.open("w", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=fields)
writer.writeheader()
writer.writerows({field: csv_value(row.get(field)) for field in fields} for row in rows)
metadata = {"request": params, **context}
output.with_suffix(".json").write_text(json.dumps(metadata, indent=2), encoding="utf-8")
print("Saved", len(rows), "rows to", output)
Read the output correctly
Keep the JSON metadata beside the CSV: it records the effective period, controls, and currency. Conversion rate is the API's fraction, such as 0.025 for 2.5%; blank revenue is unavailable, not zero. Text starting with a spreadsheet formula prefix is apostrophe-prefixed for safer opening.
The API bounds limit and offset. This script stops rather than silently saving a partial export when it cannot continue. Narrow the report if needed. It does not bypass service limits, retry failed requests, or guarantee a frozen snapshot while data changes.
For a smaller recurring summary, use Build a Weekly Marketing Report.