Bing Ads Reporting API returns download_url : None
02:47 09 Mar 2026

I have a python script using python requests library, where I try to fetch data from bings reporting api but for some reason I got the following response whenever I used PollGenerateReport Service api.

https://learn.microsoft.com/en-us/advertising/reporting-service/pollgeneratereport?view=bingads-13&tabs=prod&pivots=rest

{'ReportRequestStatus': {'Status': 'Success', 'ReportDownloadUrl': None}}

I didn't get any download links for Campaign Performance API.

Whenever I try to fetch regular campaigns data object from campaign management system api, it shows all the campaign.
https://learn.microsoft.com/en-us/advertising/campaign-management-service/getcampaignsbyids?view=bingads-13&tabs=prod&pivots=rest

Here is the script:

import requests
import csv
import io
import time
import logging

ACCESS_TOKEN = "XXXXXXXX"
DEVELOPER_TOKEN = "XXXXXXXXXXXX"
CUSTOMER_ID     = "XXXXXXXX"       
ACCOUNT_ID      = "XXXXXXX"       

START_DATE = {"Year": 2024, "Month": 1,  "Day": 1}
END_DATE   = {"Year": 2026, "Month": 3,  "Day": 5}

REPORT_TYPE = "CampaignPerformanceReport"

COLUMNS = [
    "TimePeriod",
    "AccountId",
    "AccountName",
    "CampaignId",
    "CampaignName",
    "Impressions",
    "Clicks",
    "Spend",
    "Ctr",
    "AverageCpc",
    "Conversions",
]

payload = {
    "ReportRequest": {
        "Type": "CampaignPerformanceReportRequest",
        "ReportName": "CampaignReport",
        "Format": "Csv",
        "FormatVersion": "2.0",           
        "Aggregation": "Daily",
        "ReturnOnlyCompleteData": False,
        "ExcludeColumnHeaders": False,    
        "ExcludeReportHeader": True,      
        "ExcludeReportFooter": True,      
        "Scope": {
            "AccountIds": [int(ACCOUNT_ID)]
        },
        "Time": {
            "CustomDateRangeStart": START_DATE,  
            "CustomDateRangeEnd": END_DATE,
            "ReportTimeZone": "PacificTimeUSCanadaTijuana"  
        },
        "Columns": COLUMNS,
    }
}




def get_headers():
    return {
        "Authorization": f"Bearer {ACCESS_TOKEN}",
        "Content-Type": "application/json",
        "DeveloperToken": DEVELOPER_TOKEN,
        "CustomerAccountId": str(ACCOUNT_ID),
        "CustomerId": str(CUSTOMER_ID),
    }



def submit_report(report_type=REPORT_TYPE, columns=COLUMNS):
    url = "https://reporting.api.bingads.microsoft.com/Reporting/v13/GenerateReport/Submit"

    api_type = report_type + "Request" if not report_type.endswith("Request") else report_type

    payload = {
        "ReportRequest": {
            "Type": api_type,
            "ReportName": f"{report_type}_{int(time.time())}",
            "Format": "Csv",
            "FormatVersion": "2.0",
            "Aggregation": "Daily",
            "ReturnOnlyCompleteData": False,
            "ExcludeColumnHeaders": False,
            "ExcludeReportHeader": True,
            "ExcludeReportFooter": True,
            "Scope": {
                "AccountIds": [int(ACCOUNT_ID)]
            },
            "Time": {
                "CustomDateRangeStart": START_DATE,
                "CustomDateRangeEnd": END_DATE,
                "ReportTimeZone": "PacificTimeUSCanadaTijuana"
            },
            "Columns": columns,
        }
    }

    logging.info(f"Submitting report: {report_type}")
    response = requests.post(url, json=payload, headers=get_headers())
    print("Submit Response: ", response.json())
    if not response.ok:
        logging.error(f"Submit failed [{response.status_code}]: {response.text}")
        response.raise_for_status()

    report_request_id = response.json().get("ReportRequestId")
    logging.info(f"Report submitted. ID: {report_request_id}")
    return report_request_id



def poll_report(report_request_id, max_attempts=60, sleep_seconds=5):
    url = "https://reporting.api.bingads.microsoft.com/Reporting/v13/GenerateReport/Poll"

    payload = {"ReportRequestId": report_request_id}

    for attempt in range(1, max_attempts + 1):
        response = requests.post(url, json=payload, headers=get_headers())
        print("Poll Response: ", response.json())
        if not response.ok:
            logging.error(f"Poll failed [{response.status_code}]: {response.text}")
            response.raise_for_status()

        result = response.json()
        status_obj = result.get("ReportRequestStatus", result)
        status = status_obj.get("Status")
        download_url = status_obj.get("ReportDownloadUrl")

        logging.info(f"Poll {attempt}/{max_attempts}: status = {status}")

        if status == "Success":
            if not download_url:
                logging.warning("Report succeeded but no download URL — no data for this date range / account.")
                return None
            return download_url

        elif status == "Error":
            raise Exception(f"Report failed on the server side. Full response: {result}")

        elif status in ("Pending", "InProgress"):
            if attempt < max_attempts:
                time.sleep(sleep_seconds)
            else:
                raise Exception("Timed out waiting for report.")

        else:
            raise Exception(f"Unexpected status '{status}'. Response: {result}")

    raise Exception("Exceeded max poll attempts.")



def download_report(download_url):
    logging.info("Downloading report...")
    response = requests.get(download_url, timeout=120)

    if not response.ok:
        logging.error(f"Download failed [{response.status_code}]")
        response.raise_for_status()

    lines = response.text.splitlines()
    start_idx = 0
    for i, line in enumerate(lines):
        if any(col in line for col in COLUMNS):
            start_idx = i
            break

    clean_csv = "\n".join(lines[start_idx:])
    reader = csv.DictReader(io.StringIO(clean_csv))
    rows = [dict(row) for row in reader if any(v.strip() for v in row.values())]

    logging.info(f"Parsed {len(rows)} rows.")
    return rows



def run():
    report_id = submit_report()

    download_url = poll_report(report_id)
    if download_url is None:
        print("No data returned. Check your date range, account ID, and customer ID.")
        return []

    rows = download_report(download_url)

    if rows:
        print(f"Report: {REPORT_TYPE}  |  Rows: {len(rows)}")
        for row in rows[:5]:
            for k, v in row.items():
                print(f"  {k}: {v}")
            print()
    else:
        print("Report downloaded but no rows found after parsing.")

    return rows


if __name__ == "__main__":
    rows = run()
    
    
python python-requests bing-ads-api