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.
{'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()