Menu
Dessert Data
A love for ingesting data

Bing Ads

Campaign Performance Report

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 FREE

Overview

Bing Ads Reporting API is intended to be run on a .NET system, but it is possible to connect to the API using other languages such as Javascript or Python. This guide will walk you through the steps to connect to the Bing Ads Reporting API using Typescript.
The steps are:
  1. Create and setup a Bing Ads developer account
  2. Identify your customer ID and customer account ID
  3. Generate a report
  4. Poll until report is created
  5. Download the csv
  6. Parse

Creating the Bing Ads Developer Account

I won't go into detail here as this guide is more about the technical implementation. You will need to generate a client ID and secret. The documentation starts here

Identify your Customer ID and Customer Account ID

You will need to identify your customer ID and customer account ID. You can do this by logging into your Bing Ads account, and checking the URL. The URL will look something like:
https://ui.bingads.microsoft.com/campaigns?cid=YOUR_CUSTOMER_ID&aid=YOUR_ACCOUNT_ID
Bing Ads Customer Account ID

Generate a Report

Now that you've got your client ID and secret, you will need to generate a SOAP request to Bing to generate a report. Microsoft supports some libraries if you're using C#, Python, Java, or PHP. If you're using Javascript you'll need to generate the SOAP request manually.

SOAP Request XML

The request will look something like below.
You will need:

<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>
        

Javascript Fetch Request

Now plug that SOAP request in to a fetch request:

// 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')
}
        

Polling for Report Status

Now that you've requested the report to be build, you have to wait for it to be created. You can check the status of the report by calling the PollGenerateReport endpoint.

SOAP Request XML

You will need the report request ID from the previous step.

<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>

Javascript Fetch Request

Five seconds is a good interval to check the status of the report.

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(/&amp;/g, '&')

        return url
    }
         if (status === 'Error') {
      throw new Error('Error polling for report')
    }
}

Downloading the Report

Now that the report has been generated, you can download it. The URL will be returned in the previous step.

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))
}