Reporting RecipesExport Campaign Reports to CSV

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.