Psst... Dessert Data connects all your marketing data into a neatly organized database. You can use the same query to pull spend for Bing, TikTok, or Google.
Don't worry about APIs changes or database uptime. We handle all that for you.
GET STARTED FREEhttps://ui.bingads.microsoft.com/campaigns?cid=YOUR_CUSTOMER_ID&aid=YOUR_ACCOUNT_ID
<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">SubmitGenerateReport</Action>
<AuthenticationToken i:nil="false">${accessToken}</AuthenticationToken>
<CustomerAccountId i:nil="false">${accountId}</CustomerAccountId>
<CustomerId i:nil="false">${customerId}</CustomerId>
<DeveloperToken i:nil="false">${developerToken}</DeveloperToken>
</s:Header>
<s:Body>
<SubmitGenerateReportRequest xmlns="https://bingads.microsoft.com/Reporting/v13">
<ReportRequest i:nil="false" i:type="CampaignPerformanceReportRequest">
<ExcludeColumnHeaders>false</ExcludeColumnHeaders>
<ExcludeReportFooter>true</ExcludeReportFooter>
<ExcludeReportHeader>true</ExcludeReportHeader>
<ReportName i:nil="false">CampaignPerformanceReport.${now}</ReportName>
<Aggregation>Daily</Aggregation>
<Columns i:nil="false">
<CampaignPerformanceReportColumn>AccountName</CampaignPerformanceReportColumn>
<CampaignPerformanceReportColumn>AccountNumber</CampaignPerformanceReportColumn>
...
</Columns>
<Scope i:nil="false">
<AccountIds i:nil="false" xmlns:a1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<a1:long>${accountId}</a1:long>
</AccountIds>
</Scope>
<Time i:nil="false">
<PredefinedTime i:nil="false">LastSevenDays</PredefinedTime>
<ReportTimeZone i:nil="false">EasternTimeUSCanada</ReportTimeZone>
</Time>
</ReportRequest>
</SubmitGenerateReportRequest>
</s:Body>
</s:Envelope>
// Call the reporting API
const resp = await fetch(
"https://reporting.api.bingads.microsoft.com/Api/Advertiser/Reporting/V13/ReportingService.svc",
{
method: "post",
headers: {
"Content-Type": "text/xml",
SOAPAction: "SubmitGenerateReport",
},
body: `${soapRequest}`,
}
);
// A ReportRequestId element will be returned if the report was generated successfully
const respText = await resp.text()
const reportRequestParsed = respText.match(/<ReportRequestId>([0-9_]+)</ReportRequestId>/)
if (!reportRequestParsed) {
throw new Error('Error generating report request')
}
<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">${accessToken}</AuthenticationToken>
<CustomerAccountId i:nil="false">${accountId}</CustomerAccountId>
<CustomerId i:nil="false">${customerId}</CustomerId>
<DeveloperToken i:nil="false">${developerToken}</DeveloperToken>
</s:Header>
<s:Body>
<PollGenerateReportRequest xmlns="https://bingads.microsoft.com/Reporting/v13">
<ReportRequestId i:nil="false">${reportRequestId}</ReportRequestId>
</PollGenerateReportRequest>
</s:Body>
</s:Envelope>
let status = 'Pending'
while (status === 'Pending') {
await sleep(5) // Wait five seconds
// Call PollGenerateReport service
const resp = await fetch('https://reporting.api.bingads.microsoft.com/Api/Advertiser/Reporting/V13/ReportingService.svc', {
method: 'post',
headers: {
'Content-Type': 'text/xml',
'SOAPAction': 'PollGenerateReport',
},
body: `${soapRequest}`,
})
// Parse response text for the status
const respText = await resp.text()
status = respText.match(/<Status>(.+)</Status>/)?.[1]!
// Status will be Success, Error, or Pending
if (status === 'Success') {
// Extract the download URL for the next step
const encodedUrl = respText.match(/<ReportDownloadUrl>(.+)</ReportDownloadUrl>/)?.[1]
const url = encodedUrl?.replace(/&/g, '&')
return url
}
if (status === 'Error') {
throw new Error('Error polling for report')
}
}
Use a standard fetch to download the report. It will need to be unzipped before parsing as a CSV.
A final note for parsing - the CSV header row always contains a non-printable character as the first character. It an be removed with a regex replace.
return new Promise((resolve, reject) => {
fs.createReadStream(path)
.pipe(csv()) // "csv" is the "csv-parser" package
.on("data", (row) => {
// The first key always has a non printable character in it
const firstKey = Object.keys(row)[0]!
const firstVal = row[firstKey]
const firstKeyFixed = firstKey.replace(/[^A-Za-z]/g, '')
delete row[firstKey]
row[firstKeyFixed] = firstVal
data.push(row)
})
.on("end", () => {
resolve(data)
})
.on("error", e => reject(e))
}