📊 Bing Ads - Async Report Generation
📖 Overview
Bing Ads reporting uses an asynchronous workflow where you submit a report request, poll for completion, and download the results. This pattern is consistent across all entity types (campaigns, ad groups, ads, keywords).
Report Types Covered:
- Campaign Performance Report (
campaigns/metrics) - Ad Group Performance Report (
adgroups/metrics) - Ad Performance Report (
ads/metrics) - Keyword Performance Report (
keywords/metrics)
Service: Reporting Service v13
Endpoint: https://reporting.api.bingads.microsoft.com/Api/Advertiser/Reporting/v13/ReportingService.svc
🔄 Report Lifecycle
graph TD
A[Submit Report Request] --> B[Receive ReportRequestId]
B --> C[Poll Report Status]
C --> D{Status?}
D -->|Pending| E[Wait 30-60 seconds]
E --> C
D -->|Success| F[Download Report URL]
D -->|Error| G[Handle Error]
F --> H[Fetch CSV Data]
H --> I[Parse and Process]
style A fill:#4CAF50
style B fill:#2196F3
style C fill:#FF9800
style D fill:#9C27B0
style F fill:#00BCD4
style H fill:#8BC34A
style I fill:#4CAF50
📋 Three-Step Process
Step 1: Submit Report Request
Submit report generation request with desired columns, filters, and time range.
Endpoints:
GET /campaigns/metrics→ CampaignPerformanceReportRequestGET /adgroups/metrics→ AdGroupPerformanceReportRequestGET /ads/metrics→ AdPerformanceReportRequestGET /keywords/metrics→ KeywordPerformanceReportRequest
Common Parameters:
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
customerAccountID | Integer | ✅ | Account ID | - |
customerID | Integer | ⚪ | Customer ID | - |
campaignID | Integer | ⚪ | Filter by campaign | All campaigns |
campaignType | String | ⚪ | Search, Shopping, DynamicSearchAds | Search |
adGroupID | Integer | ⚪ | Filter by ad group | All ad groups |
aggregation | String | ⚪ | Time aggregation level | Monthly |
predefinedTime | String | ⚪ | Predefined time range | Last30Days |
timeZone | String | ⚪ | Report timezone | EasternTimeUSCanada |
fromDate | String | ⚪ | Custom start (YYYY-MM-DD) | - |
endDate | String | ⚪ | Custom end (YYYY-MM-DD) | - |
Aggregation Options:
Hourly- Hour-by-hour breakdownDaily- Daily breakdownWeekly- Week-by-week breakdownMonthly- Monthly breakdown (default)Yearly- Annual breakdownSummary- Single row with totals
Predefined Time Ranges:
TodayYesterdayLast7DaysLast14DaysLast30Days(default)LastWeekLastMonthLastThreeMonthsLastSixMonthsLastYearThisWeekStartingMondayThisMonthThisYear
Response:
{
"success": true,
"message": "SUCCESS",
"data": {
"ReportRequestId": "987654321012345"
}
}
Save: Store ReportRequestId for polling and download
Step 2: Poll Report Status
Poll the report status using the ReportRequestId until status is Success or Error.
SOAP Operation: PollGenerateReport
Model Method: ReportModel.pollGenerateReport(reportRequestId)
Request:
# Internal API call (not exposed as REST endpoint)
# Called within your application logic
SOAP Request XML:
<s:Envelope xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header xmlns="https://bingads.microsoft.com/Reporting/v13">
<Action mustUnderstand="1">PollGenerateReport</Action>
<AuthenticationToken i:nil="false">{ACCESS_TOKEN}</AuthenticationToken>
<DeveloperToken i:nil="false">{DEVELOPER_TOKEN}</DeveloperToken>
</s:Header>
<s:Body>
<PollGenerateReportRequest xmlns="https://bingads.microsoft.com/Reporting/v13">
<ReportRequestId>987654321012345</ReportRequestId>
</PollGenerateReportRequest>
</s:Body>
</s:Envelope>
SOAP Response XML (Pending):
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<PollGenerateReportResponse xmlns="https://bingads.microsoft.com/Reporting/v13">
<ReportRequestStatus>
<ReportDownloadUrl i:nil="true"/>
<Status>Pending</Status>
</ReportRequestStatus>
</PollGenerateReportResponse>
</s:Body>
</s:Envelope>
SOAP Response XML (Success):
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<PollGenerateReportResponse xmlns="https://bingads.microsoft.com/Reporting/v13">
<ReportRequestStatus>
<ReportDownloadUrl>https://reporting.api.bingads.microsoft.com/ReportDownload/Download.svc?token=abc123...</ReportDownloadUrl>
<Status>Success</Status>
</ReportRequestStatus>
</PollGenerateReportResponse>
</s:Body>
</s:Envelope>
Status Values:
| Status | Description | Action |
|---|---|---|
Pending | Report is being generated | Wait and poll again (30-60 sec) |
Success | Report is ready for download | Proceed to download |
Error | Report generation failed | Check error details |
Polling Strategy:
async function pollReportStatus(reportRequestId, maxAttempts = 20) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const response = await pollGenerateReport(reportRequestId);
if (response.Status === 'Success') {
return response.ReportDownloadUrl;
}
if (response.Status === 'Error') {
throw new Error('Report generation failed');
}
// Wait before next poll (exponential backoff)
const waitTime = Math.min(60000, 5000 * attempt); // Start 5s, max 60s
await new Promise(resolve => setTimeout(resolve, waitTime));
}
throw new Error('Report polling timeout');
}
Best Practices:
- ⏱️ Wait 30-60 seconds between polls
- 🔄 Implement exponential backoff (5s → 10s → 20s → 30s → 60s)
- ⏰ Set maximum polling time (10-15 minutes)
- 📊 Large reports take longer to generate
- 🚫 Don't poll more frequently than every 5 seconds
Step 3: Download Report
Download the CSV report file from the provided URL.
SOAP Operation: HTTP GET to ReportDownloadUrl
Model Method: ReportModel.downloadReport(reportDownloadUrl)
Request:
// Simple HTTP GET request
const axios = require('axios');
async function downloadReport(reportDownloadUrl) {
const response = await axios.get(reportDownloadUrl, {
responseType: 'text', // CSV text data
});
return response.data; // CSV string
}
CSV Response Example (Campaign Performance):
"GregorianDate","AccountName","AccountNumber","CampaignName","CampaignId","CampaignStatus","CampaignType","Impressions","Clicks","Ctr","AverageCpc","Spend","Conversions","ConversionRate","CostPerConversion","Revenue","ReturnOnAdSpend"
"2024-01-15","Example Account","123456789","Search Campaign","987654321","Active","Search","10000","500","5.0","2.50","1250.00","25","5.0","50.00","2500.00","2.0"
"2024-01-16","Example Account","123456789","Search Campaign","987654321","Active","Search","12000","600","5.0","2.45","1470.00","30","5.0","49.00","3000.00","2.04"
Parsing CSV:
const csv = require('csv-parser');
const { Readable } = require('stream');
function parseCSV(csvData) {
return new Promise((resolve, reject) => {
const results = [];
Readable.from(csvData)
.pipe(csv())
.on('data', row => results.push(row))
.on('end', () => resolve(results))
.on('error', reject);
});
}
// Usage
const csvData = await downloadReport(reportDownloadUrl);
const parsedData = await parseCSV(csvData);
console.log(parsedData);
// [
// {
// GregorianDate: '2024-01-15',
// AccountName: 'Example Account',
// Impressions: '10000',
// Clicks: '500',
// ...
// },
// ...
// ]
🔧 Complete Report Workflow Example
const BingAdsAPI = require('./external/Integrations/BingAds');
async function generateAndDownloadReport() {
try {
// Step 1: Submit Report Request
console.log('Step 1: Submitting report request...');
const submitResponse = await fetch('/v1/integrations/bing/ads/campaigns/metrics', {
method: 'GET',
headers: {
Authorization: 'Bearer YOUR_JWT_TOKEN',
},
params: {
customerAccountID: '111222333',
customerID: '123456',
campaignID: '987654321',
aggregation: 'Daily',
predefinedTime: 'Last7Days',
},
});
const { data } = await submitResponse.json();
const reportRequestId = data.ReportRequestId;
console.log(`Report Request ID: ${reportRequestId}`);
// Step 2: Poll for Completion
console.log('Step 2: Polling report status...');
let reportDownloadUrl = null;
let attempts = 0;
const maxAttempts = 20;
while (!reportDownloadUrl && attempts < maxAttempts) {
attempts++;
// Wait before polling (exponential backoff)
const waitTime = Math.min(60000, 5000 * attempts);
console.log(`Waiting ${waitTime / 1000}s before poll attempt ${attempts}...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
// Poll status (internal SOAP call)
const pollResult = await BingAdsAPI.pollGenerateReport(reportRequestId);
if (pollResult.Status === 'Success') {
reportDownloadUrl = pollResult.ReportDownloadUrl;
console.log('Report ready!');
} else if (pollResult.Status === 'Error') {
throw new Error('Report generation failed');
} else {
console.log(`Status: ${pollResult.Status} - continuing to poll...`);
}
}
if (!reportDownloadUrl) {
throw new Error('Report polling timeout');
}
// Step 3: Download CSV
console.log('Step 3: Downloading report...');
const csvData = await BingAdsAPI.downloadReport(reportDownloadUrl);
// Step 4: Parse and Process
console.log('Step 4: Parsing CSV data...');
const parsedData = await parseCSV(csvData);
console.log(`Report downloaded: ${parsedData.length} rows`);
// Process data
return parsedData;
} catch (error) {
console.error('Report generation error:', error);
throw error;
}
}
// Execute
generateAndDownloadReport()
.then(data => {
console.log('Report data:', data);
})
.catch(error => {
console.error('Failed:', error);
});
📊 Report Column Counts
Different report types return different numbers of columns:
| Report Type | Columns | Unique Metrics |
|---|---|---|
| Campaign Performance | 67 | Campaign-level aggregation |
| Ad Group Performance | 56 | Ad group-level aggregation |
| Ad Performance | 71 | Ad creative metrics |
| Keyword Performance | 73 | Quality scores, bid estimates |
Note: All columns are hardcoded in source code (cannot be customized dynamically)
🔍 Report Filtering
Campaign Filter
// Submit request with campaignID
GET /campaigns/metrics?campaignID=987654321&...
SOAP XML:
<Scope>
<Campaigns>
<CampaignReportScope>
<AccountId>111222333</AccountId>
<CampaignId>987654321</CampaignId>
<CampaignType>Search</CampaignType>
</CampaignReportScope>
</Campaigns>
</Scope>
Ad Group Filter
// Submit request with campaignID and adGroupID
GET /adgroups/metrics?campaignID=987654321&adGroupID=111222333&...
SOAP XML:
<Scope>
<AdGroups>
<AdGroupReportScope>
<AccountId>111222333</AccountId>
<CampaignId>987654321</CampaignId>
<AdGroupId>111222333</AdGroupId>
</AdGroupReportScope>
</AdGroups>
</Scope>
Keyword Filter
// Submit request with keywordID
GET /keywords/metrics?keywordID=444555666&...
SOAP XML:
<Filter>
<Keywords xmlns:a1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<a1:string>444555666</a1:string>
</Keywords>
</Filter>
🕒 Time Range Options
Predefined Time Ranges
Use predefinedTime parameter for common date ranges:
GET /campaigns/metrics?predefinedTime=Last7Days&...
Available values:
Today,YesterdayLast7Days,Last14Days,Last30DaysLastWeek,LastMonth,LastThreeMonths,LastSixMonths,LastYearThisWeekStartingMonday,ThisMonth,ThisYear
Custom Date Ranges
Use fromDate and endDate for custom ranges:
GET /campaigns/metrics?fromDate=2024-01-01&endDate=2024-01-31&...
SOAP XML:
<Time>
<CustomDateRangeStart>
<Day>1</Day>
<Month>1</Month>
<Year>2024</Year>
</CustomDateRangeStart>
<CustomDateRangeEnd>
<Day>31</Day>
<Month>1</Month>
<Year>2024</Year>
</CustomDateRangeEnd>
<ReportTimeZone>EasternTimeUSCanada</ReportTimeZone>
</Time>
🌍 Timezone Options
Default: EasternTimeUSCanada
Available timezones:
EasternTimeUSCanadaPacificTimeUSCanadaTijuanaCentralTimeUSCanadaMountainTimeUSCanadaGMTGreenwichUTC(Coordinated Universal Time)AbuDhabiMuscatAdelaideAlaskaCanadaBangkokBeijingBrisbaneCanberraDarwinFijiGuamHobartHongKongJerusalemMelbourneNewZealandPerthSamoaSeoulSingaporeSydneyTaipeiTokyo
Important: Timezone affects interpretation of date ranges and report data timestamps.
⚠️ Important Considerations
Report Generation Time
| Data Volume | Expected Time |
|---|---|
| Small (< 1000 rows) | 30-60 seconds |
| Medium (1000-10000 rows) | 1-3 minutes |
| Large (> 10000 rows) | 3-10 minutes |
| Very Large (> 100000 rows) | 10-15 minutes |
Polling Best Practices
✅ Do:
- Wait at least 30 seconds before first poll
- Implement exponential backoff (5s → 10s → 20s → 30s → 60s)
- Set maximum polling attempts (20 attempts = ~10 minutes)
- Log polling progress for debugging
- Handle network errors gracefully
❌ Don't:
- Poll more frequently than every 5 seconds (API throttling)
- Poll indefinitely without timeout
- Block UI thread while polling
- Ignore
Errorstatus
CSV Parsing
- First row contains column headers
- Values are quoted with double quotes
- Numeric values may contain commas (e.g., "10,000")
- Date format: YYYY-MM-DD
- Timezone affects date values
Report Limitations
- Maximum report size: 2GB (compressed)
- Maximum rows: 4 million
- Data retention: 13 months (varies by metric)
- Column selection: Fixed (cannot customize)
- Report format: CSV only (no JSON option)
🐛 Error Handling
Common Errors
| Error | Cause | Solution |
|---|---|---|
InvalidReportId | Wrong/expired report ID | Verify ReportRequestId |
ReportingServiceTimeout | Report took too long | Reduce date range or filters |
InvalidCustomDateRange | Invalid date format | Use YYYY-MM-DD format |
InsufficientPrivileges | No access to account | Check account permissions |
DataUnavailable | No data for time range | Try different date range |
Error Response Example
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<s:Fault>
<faultcode>s:Server</faultcode>
<faultstring>InvalidReportId</faultstring>
<detail>
<ApiFaultDetail xmlns="https://bingads.microsoft.com/Reporting/v13">
<TrackingId>abc-123-def-456</TrackingId>
<Errors>
<AdApiError>
<Code>2053</Code>
<ErrorCode>InvalidReportId</ErrorCode>
<Message>The report request id is invalid or expired.</Message>
</AdApiError>
</Errors>
</ApiFaultDetail>
</detail>
</s:Fault>
</s:Body>
</s:Envelope>
📝 Report Data Processing
Converting CSV to JSON
const csv = require('csv-parser');
const { Readable } = require('stream');
async function csvToJson(csvData) {
return new Promise((resolve, reject) => {
const results = [];
Readable.from(csvData)
.pipe(csv())
.on('data', row => {
// Type conversion
const processed = {
date: row.GregorianDate,
impressions: parseInt(row.Impressions) || 0,
clicks: parseInt(row.Clicks) || 0,
ctr: parseFloat(row.Ctr) || 0,
spend: parseFloat(row.Spend) || 0,
conversions: parseInt(row.Conversions) || 0,
revenue: parseFloat(row.Revenue) || 0,
};
results.push(processed);
})
.on('end', () => resolve(results))
.on('error', reject);
});
}
Aggregating Report Data
function aggregateMetrics(reportData) {
return reportData.reduce(
(totals, row) => {
return {
totalImpressions: totals.totalImpressions + row.impressions,
totalClicks: totals.totalClicks + row.clicks,
totalSpend: totals.totalSpend + row.spend,
totalConversions: totals.totalConversions + row.conversions,
totalRevenue: totals.totalRevenue + row.revenue,
};
},
{
totalImpressions: 0,
totalClicks: 0,
totalSpend: 0,
totalConversions: 0,
totalRevenue: 0,
},
);
}
🔗 Related Documentation
- Authentication: OAuth 2.0 with Auto-Refresh
- Accounts: Account Hierarchy
- Campaigns: Campaign Reports
- Ad Groups: Ad Group Reports
- Ads: Ad Reports
- Keywords: Keyword Reports with Quality Scores
- Microsoft Docs: Reporting Service Reference
- API Guides: Request and Download a Report