Skip to main content

📊 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 → CampaignPerformanceReportRequest
  • GET /adgroups/metrics → AdGroupPerformanceReportRequest
  • GET /ads/metrics → AdPerformanceReportRequest
  • GET /keywords/metrics → KeywordPerformanceReportRequest

Common Parameters:

ParameterTypeRequiredDescriptionDefault
customerAccountIDIntegerAccount ID-
customerIDIntegerCustomer ID-
campaignIDIntegerFilter by campaignAll campaigns
campaignTypeStringSearch, Shopping, DynamicSearchAdsSearch
adGroupIDIntegerFilter by ad groupAll ad groups
aggregationStringTime aggregation levelMonthly
predefinedTimeStringPredefined time rangeLast30Days
timeZoneStringReport timezoneEasternTimeUSCanada
fromDateStringCustom start (YYYY-MM-DD)-
endDateStringCustom end (YYYY-MM-DD)-

Aggregation Options:

  • Hourly - Hour-by-hour breakdown
  • Daily - Daily breakdown
  • Weekly - Week-by-week breakdown
  • Monthly - Monthly breakdown (default)
  • Yearly - Annual breakdown
  • Summary - Single row with totals

Predefined Time Ranges:

  • Today
  • Yesterday
  • Last7Days
  • Last14Days
  • Last30Days (default)
  • LastWeek
  • LastMonth
  • LastThreeMonths
  • LastSixMonths
  • LastYear
  • ThisWeekStartingMonday
  • ThisMonth
  • ThisYear

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:

StatusDescriptionAction
PendingReport is being generatedWait and poll again (30-60 sec)
SuccessReport is ready for downloadProceed to download
ErrorReport generation failedCheck 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 TypeColumnsUnique Metrics
Campaign Performance67Campaign-level aggregation
Ad Group Performance56Ad group-level aggregation
Ad Performance71Ad creative metrics
Keyword Performance73Quality 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, Yesterday
  • Last7Days, Last14Days, Last30Days
  • LastWeek, LastMonth, LastThreeMonths, LastSixMonths, LastYear
  • ThisWeekStartingMonday, 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:

  • EasternTimeUSCanada
  • PacificTimeUSCanadaTijuana
  • CentralTimeUSCanada
  • MountainTimeUSCanada
  • GMTGreenwich
  • UTC (Coordinated Universal Time)
  • AbuDhabiMuscat
  • Adelaide
  • AlaskaCanada
  • Bangkok
  • Beijing
  • Brisbane
  • Canberra
  • Darwin
  • Fiji
  • Guam
  • Hobart
  • HongKong
  • Jerusalem
  • Melbourne
  • NewZealand
  • Perth
  • Samoa
  • Seoul
  • Singapore
  • Sydney
  • Taipei
  • Tokyo

Important: Timezone affects interpretation of date ranges and report data timestamps.


⚠️ Important Considerations

Report Generation Time

Data VolumeExpected 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 Error status

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

ErrorCauseSolution
InvalidReportIdWrong/expired report IDVerify ReportRequestId
ReportingServiceTimeoutReport took too longReduce date range or filters
InvalidCustomDateRangeInvalid date formatUse YYYY-MM-DD format
InsufficientPrivilegesNo access to accountCheck account permissions
DataUnavailableNo data for time rangeTry 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,
},
);
}

💬

Documentation Assistant

Ask me anything about the docs

Hi! I'm your documentation assistant. Ask me anything about the docs!

I can help you with:
- Code examples
- Configuration details
- Troubleshooting
- Best practices

Try asking: How do I configure the API?
09:30 AM