📢 Bing Ads - Ad Management
📖 Overview
Ad management for creating, retrieving, and reporting on various ad types including Expanded Text, Responsive Search, Image, Product, and Dynamic Search ads. No direct campaign-to-ads operation exists in Bing Ads API - ads must be retrieved through ad groups.
Source Files:
- Controller:
external/Integrations/BingAds/Controllers/Ads/AdsController.js - Model:
external/Integrations/BingAds/Models/Ads/AdsModel.js - Routes:
external/Integrations/BingAds/Routes/ads.js
External APIs:
- Campaign Management Service:
https://campaign.api.bingads.microsoft.com/Api/Advertiser/CampaignManagement/v13/CampaignManagementService.svc - Reporting Service:
https://reporting.api.bingads.microsoft.com/Api/Advertiser/Reporting/v13/ReportingService.svc
🏗️ Ad Retrieval Architecture
graph TD
A[Campaign ID] --> B[Get Ad Groups]
B --> C1[Ad Group 1]
B --> C2[Ad Group 2]
C1 --> D1[Get Ads by Ad Group]
C2 --> D2[Get Ads by Ad Group]
D1 --> E1[Ads Array]
D2 --> E2[Ads Array]
style A fill:#4CAF50
style B fill:#2196F3
style C1 fill:#FF9800
style C2 fill:#FF9800
style D1 fill:#9C27B0
style D2 fill:#9C27B0
style E1 fill:#E91E63
style E2 fill:#E91E63
Important: Bing Ads API has no GetAdsByCampaignId operation. Available operations are:
GetAdsByAdGroupId- Get ads by ad groupGetAdsByIds- Get specific ads by IDGetAdsByEditorialStatus- Get ads by editorial status
Workaround: To get ads by campaign, the code:
- Gets ad groups for campaign
- Loops through ad groups
- Gets ads for each ad group
- Aggregates results
🔄 Data Flow
sequenceDiagram
participant Client as API Client
participant DC as DashClicks API
participant MW as Token Middleware
participant Model as AdsModel
participant SOAP as Bing SOAP API
Client->>DC: GET /ads?campaignID={id}
DC->>MW: Check token expiration
MW->>DC: Valid access token
DC->>Model: getAdsByCampaignID()
Model->>SOAP: POST GetAdGroupsByCampaignId
SOAP-->>Model: Ad groups list
loop For each ad group
Model->>SOAP: POST GetAdsByAdGroupId
SOAP-->>Model: Ads for ad group
end
Model->>Model: Aggregate campaign + adgroup + ads
Model-->>DC: Nested structure
DC-->>Client: JSON response
🔧 Ad Functions
List Ads by Campaign
GET /ads?campaignID={id}
Purpose: Retrieve all ads for a specific campaign (via ad groups)
Controller: AdsController.index() → getAdsByCampaignID()
Model Method: AdsModel.getAdsByCampaignID(customerAccountID, customerID, campaignID)
SOAP Operations: GetAdGroupsByCampaignId + multiple GetAdsByAdGroupId calls
Request:
GET /v1/integrations/bing/ads/ads?campaignID=987654321&customerAccountID=111222333&customerID=123456
Authorization: Bearer {jwt_token}
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
campaignID | Integer | ✅ | Campaign ID to filter ads |
customerAccountID | Integer | ✅ | Account ID |
customerID | Integer | ⚪ | Customer ID (optional) |
Validation:
- If
campaignIDmissing, returns error: "You can get ads by campaign ID and adgroup ID"
Business Logic Flow:
- Get Ad Groups: Call
getAdgroupFilteredByCampaignID()to get campaign's ad groups - Check Fault: If SOAP fault returned, pass through error
- Build Promises: Create promise array for parallel ad fetching
- Loop Ad Groups: For each ad group, call
getAdsByAdGroupId() - Aggregate: Combine campaign + ad group + ads into nested structure
- Return: Single object if one ad group, array if multiple
SOAP Request XML (GetAdsByAdGroupId):
<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/CampaignManagement/v13">
<Action mustUnderstand="1">GetAdsByAdGroupId</Action>
<AuthenticationToken i:nil="false">{ACCESS_TOKEN}</AuthenticationToken>
<CustomerAccountId i:nil="false">111222333</CustomerAccountId>
<CustomerId i:nil="false">123456</CustomerId>
<DeveloperToken i:nil="false">{DEVELOPER_TOKEN}</DeveloperToken>
</s:Header>
<s:Body>
<GetAdsByAdGroupIdRequest xmlns="https://bingads.microsoft.com/CampaignManagement/v13">
<AdGroupId>111222333</AdGroupId>
<AdTypes i:nil="false">
<AdType>ExpandedText</AdType>
<AdType>Text</AdType>
<AdType>Image</AdType>
<AdType>Product</AdType>
<AdType>AppInstall</AdType>
<AdType>DynamicSearch</AdType>
<AdType>ResponsiveAd</AdType>
<AdType>ResponsiveSearch</AdType>
</AdTypes>
</GetAdsByAdGroupIdRequest>
</s:Body>
</s:Envelope>
Default Ad Types (if no filter provided):
ExpandedText- Standard text ads with extended headlinesText- Legacy text adsImage- Image/banner adsProduct- Shopping product adsAppInstall- Mobile app install adsDynamicSearch- Dynamic search ads (auto-generated)ResponsiveAd- Responsive display adsResponsiveSearch- Responsive search ads
JSON Response (Single Ad Group):
{
"success": true,
"message": "SUCCESS",
"data": {
"campaign": {
"CampaignId": 987654321,
"Name": "Search Campaign",
"Status": "Active"
},
"adgroup": {
"Id": 111222333,
"Name": "Ad Group 1",
"Status": "Active"
},
"ad": [
{
"@i:type": "ResponsiveSearchAd",
"Id": 444555666,
"AdGroupId": 111222333,
"Status": "Active",
"FinalUrls": {
"string": ["https://example.com/landing"]
},
"Headlines": {
"AssetLink": [
{
"Asset": {
"@i:type": "TextAsset",
"Text": "Buy Quality Products"
},
"PinnedField": "Headline1"
},
{
"Asset": {
"@i:type": "TextAsset",
"Text": "Free Shipping Today"
}
}
]
},
"Descriptions": {
"AssetLink": [
{
"Asset": {
"@i:type": "TextAsset",
"Text": "Shop our wide selection of quality products with free shipping."
}
}
]
},
"Path1": "products",
"Path2": "deals"
}
]
}
}
JSON Response (Multiple Ad Groups):
{
"success": true,
"message": "SUCCESS",
"data": [
{
"campaign": { ... },
"adgroup": { ... },
"ad": [ ... ]
},
{
"campaign": { ... },
"adgroup": { ... },
"ad": [ ... ]
}
]
}
Side Effects:
- ⚠️ Multiple API Calls: One call per ad group (performance consideration)
- ⚠️ Async Promises: Uses
Promise.all()for parallel execution - None (read-only operation)
List Ads by Ad Group
GET /ads?adGroupID={id}
Purpose: Retrieve ads for a specific ad group (direct operation)
Controller: AdsController.index() → getAdsByAdGroupId()
Model Method: AdsModel.getAdsByAdGroupId(customerAccountID, customerID, adGroupID, adType)
SOAP Operation: GetAdsByAdGroupId
Request:
GET /v1/integrations/bing/ads/ads?adGroupID=111222333&customerAccountID=111222333&customerID=123456
Authorization: Bearer {jwt_token}
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
adGroupID | Integer | ✅ | Ad group ID to filter ads |
customerAccountID | Integer | ✅ | Account ID |
customerID | Integer | ⚪ | Customer ID (optional) |
adType | String | ⚪ | Filter by specific ad type |
JSON Response:
{
"success": true,
"message": "SUCCESS",
"data": [
{
"@i:type": "ExpandedTextAd",
"Id": 777888999,
"AdGroupId": 111222333,
"Status": "Active",
"EditorialStatus": "Active",
"FinalUrls": {
"string": ["https://example.com"]
},
"TitlePart1": "Quality Products",
"TitlePart2": "Shop Now",
"TitlePart3": "Save Big",
"Text": "Browse our wide selection of quality products. Free shipping on orders over $50.",
"TextPart2": "Limited time offer!",
"Path1": "products",
"Path2": "sale",
"DisplayUrl": "example.com/products/sale"
},
{
"@i:type": "ResponsiveSearchAd",
"Id": 222333444,
"AdGroupId": 111222333,
"Status": "Active",
"Headlines": {
"AssetLink": [
{ "Asset": { "Text": "Best Deals" } },
{ "Asset": { "Text": "Shop Today" } },
{ "Asset": { "Text": "Free Shipping" } }
]
},
"Descriptions": {
"AssetLink": [
{ "Asset": { "Text": "Get amazing deals on quality products." } },
{ "Asset": { "Text": "Limited time offer. Shop now!" } }
]
}
}
]
}
Ad Type-Specific Fields:
Expanded Text Ad (ExpandedTextAd):
TitlePart1,TitlePart2,TitlePart3- Three headline parts (30 chars each)Text- Description line 1 (90 chars)TextPart2- Description line 2 (90 chars)Path1,Path2- Display URL paths (15 chars each)
Responsive Search Ad (ResponsiveSearchAd):
Headlines.AssetLink- Array of headline assets (up to 15, min 3)Descriptions.AssetLink- Array of description assets (up to 4, min 2)PinnedField- Optional pinning (Headline1, Headline2, Description1, Description2)
Dynamic Search Ad (DynamicSearchAd):
Text- Description textPath1,Path2- Display URL paths- No headlines (auto-generated from landing page)
Get Ad by ID
GET /ads/{adID}
Purpose: Retrieve specific ad by ID
Controller: AdsController.show()
Model Method: AdsModel.getAdbyAdId(customerAccountID, customerID, adGroupID, adID, adType)
SOAP Operation: GetAdsByIds
Request:
GET /v1/integrations/bing/ads/ads/444555666?adGroupID=111222333&customerAccountID=111222333&customerID=123456&adType=ResponsiveSearch
Authorization: Bearer {jwt_token}
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
adID | Integer | ✅ | Specific ad ID |
adGroupID | Integer | ✅ | Parent ad group ID |
customerAccountID | Integer | ✅ | Account ID |
customerID | Integer | ⚪ | Customer ID (optional) |
adType | String | ⚪ | Filter by ad type (default: ExpandedText) |
Validation:
- If required params missing, returns error: "Please provide customerAccountID, adGroupID and adID. optional you can provide customerID and adType as well as"
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/CampaignManagement/v13">
<Action mustUnderstand="1">GetAdsByIds</Action>
<AuthenticationToken i:nil="false">{ACCESS_TOKEN}</AuthenticationToken>
<CustomerAccountId i:nil="false">111222333</CustomerAccountId>
<CustomerId i:nil="false">123456</CustomerId>
<DeveloperToken i:nil="false">{DEVELOPER_TOKEN}</DeveloperToken>
</s:Header>
<s:Body>
<GetAdsByIdsRequest xmlns="https://bingads.microsoft.com/CampaignManagement/v13">
<AdGroupId>111222333</AdGroupId>
<AdIds i:nil="false" xmlns:a1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<a1:long>444555666</a1:long>
</AdIds>
<AdTypes i:nil="false">
<AdType>ResponsiveSearch</AdType>
</AdTypes>
</GetAdsByIdsRequest>
</s:Body>
</s:Envelope>
Helper Method: generateAdIdsXml(adID)
- Splits comma-separated IDs
- Generates XML:
<a1:long>{id}</a1:long>for each ID
JSON Response:
{
"success": true,
"message": "SUCCESS",
"data": {
"@i:type": "ResponsiveSearchAd",
"Id": 444555666,
"AdGroupId": 111222333,
"Status": "Active",
"FinalUrls": {
"string": ["https://example.com/products"]
},
"Headlines": { ... },
"Descriptions": { ... }
}
}
Get Ad Performance Metrics
GET /ads/metrics
Purpose: Generate asynchronous performance report for ads
Controller: AdsController.metrics()
Model Method: AdsModel.getAdReport(...)
SOAP Operation: SubmitGenerateReport
Service: Reporting Service v13
Request:
GET /v1/integrations/bing/ads/ads/metrics?customerAccountID=111222333&customerID=123456&campaignID=987654321&adGroupID=111222333&adType=ResponsiveSearch&aggregation=Daily&predefinedTime=Last7Days
Authorization: Bearer {jwt_token}
Query Parameters:
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
customerAccountID | Integer | ✅ | Account ID | - |
customerID | Integer | ⚪ | Customer ID | - |
campaignID | Integer | ⚪ | Specific campaign ID | All campaigns |
campaignType | String | ⚪ | Campaign type filter | Search |
adGroupID | Integer | ⚪ | Specific ad group ID | All ad groups |
adType | String | ⚪ | Ad type filter | ExpandedText |
aggregation | String | ⚪ | Aggregation level | Monthly |
predefinedTime | String | ⚪ | Predefined time period | Last30Days |
timeZone | String | ⚪ | Report timezone | EasternTimeUSCanada |
fromDate | String | ⚪ | Custom start date (YYYY-MM-DD) | - |
endDate | String | ⚪ | Custom end date (YYYY-MM-DD) | - |
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">SubmitGenerateReport</Action>
<AuthenticationToken i:nil="false">{ACCESS_TOKEN}</AuthenticationToken>
<CustomerAccountId i:nil="true">111222333</CustomerAccountId>
<CustomerId i:nil="false">123456</CustomerId>
<DeveloperToken i:nil="false">{DEVELOPER_TOKEN}</DeveloperToken>
</s:Header>
<s:Body>
<SubmitGenerateReportRequest xmlns="https://bingads.microsoft.com/Reporting/v13">
<ReportRequest i:nil="false" i:type="AdPerformanceReportRequest">
<ExcludeColumnHeaders>false</ExcludeColumnHeaders>
<ExcludeReportFooter>false</ExcludeReportFooter>
<ExcludeReportHeader>false</ExcludeReportHeader>
<Format>Csv</Format>
<Language>English</Language>
<ReportName>AdPerformanceReportRequest</ReportName>
<ReturnOnlyCompleteData>false</ReturnOnlyCompleteData>
<Aggregation>Daily</Aggregation>
<Columns>
<AdPerformanceReportColumn>AccountName</AdPerformanceReportColumn>
<AdPerformanceReportColumn>AdId</AdPerformanceReportColumn>
<AdPerformanceReportColumn>AdTitle</AdPerformanceReportColumn>
<AdPerformanceReportColumn>AdDescription</AdPerformanceReportColumn>
<AdPerformanceReportColumn>Impressions</AdPerformanceReportColumn>
<AdPerformanceReportColumn>Clicks</AdPerformanceReportColumn>
<!-- 65+ more columns available -->
</Columns>
<Filter i:nil="false">
<AdType i:nil="false">ResponsiveSearch</AdType>
<CampaignType i:nil="false">Search</CampaignType>
</Filter>
<Scope i:nil="false">
<AccountIds i:nil="false" xmlns:a1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<a1:long>111222333</a1:long>
</AccountIds>
<!-- Optional: Specific campaign and ad group -->
<AdGroups i:nil="false">
<AdGroupReportScope>
<AccountId>111222333</AccountId>
<CampaignId>987654321</CampaignId>
<AdGroupId>111222333</AdGroupId>
</AdGroupReportScope>
</AdGroups>
<Campaigns i:nil="false">
<CampaignReportScope>
<AccountId>111222333</AccountId>
<CampaignId>987654321</CampaignId>
</CampaignReportScope>
</Campaigns>
</Scope>
<Time>
<PredefinedTime i:nil="false">Last7Days</PredefinedTime>
<ReportTimeZone>EasternTimeUSCanada</ReportTimeZone>
</Time>
</ReportRequest>
</SubmitGenerateReportRequest>
</s:Body>
</s:Envelope>
Available Report Columns (71 total):
| Column | Description |
|---|---|
AccountName, AccountNumber, AccountId | Account identifiers |
CampaignName, CampaignId, CampaignStatus | Campaign info |
AdGroupName, AdGroupId, AdGroupStatus | Ad group info |
AdId, AdTitle, AdDescription, AdType | Ad creative info |
DestinationUrl, DisplayUrl | Landing page and display URL |
Impressions, Clicks, Ctr | Core metrics |
Spend, AverageCpc, AveragePosition | Cost metrics |
Conversions, ConversionRate, CostPerConversion | Conversion metrics |
Revenue, ReturnOnAdSpend, RevenuePerConversion | Revenue metrics |
QualityScore, ExpectedCtr, AdRelevance, LandingPageExperience | Quality metrics |
DeviceType, Language, Network | Targeting dimensions |
AllConversions, AllRevenue, ViewThroughConversions | Advanced metrics |
Full columns list: See getAdsColumn() method in source code (71 columns total)
JSON Response:
{
"success": true,
"message": "SUCCESS",
"data": {
"ReportRequestId": "987654321012345"
}
}
Important: This is an asynchronous operation:
- Returns
ReportRequestIdimmediately - Report generation happens in background
- Must poll
PollGenerateReportto check status - When
Success, download CSV viaReportRequestId
📊 Ad Types
| Type | Description | Use Case |
|---|---|---|
ExpandedText | Standard text ads with 3 headlines + 2 descriptions | Most common search ads |
ResponsiveSearch | Dynamic ads with multiple headlines/descriptions | Google-style responsive ads |
Text | Legacy text ads (deprecated) | Old campaigns |
Image | Banner/display image ads | Visual campaigns |
Product | Shopping product listing ads | E-commerce |
AppInstall | Mobile app installation ads | App promotion |
DynamicSearch | Auto-generated ads from website | Large inventories |
ResponsiveAd | Responsive display ads | Display network |
📏 Character Limits
Expanded Text Ads
| Field | Character Limit |
|---|---|
TitlePart1 | 30 characters |
TitlePart2 | 30 characters |
TitlePart3 | 30 characters |
Text (Description 1) | 90 characters |
TextPart2 (Description 2) | 90 characters |
Path1 | 15 characters |
Path2 | 15 characters |
Responsive Search Ads
| Field | Count Limit | Character Limit |
|---|---|---|
| Headlines | 3-15 assets | 30 chars each |
| Descriptions | 2-4 assets | 90 chars each |
| Path1 | 1 | 15 characters |
| Path2 | 1 | 15 characters |
🎯 Editorial Status Values
| Status | Description |
|---|---|
Active | Ad approved and serving |
ActiveLimited | Ad serving with limitations |
Disapproved | Ad rejected by editorial review |
Inactive | Ad not serving (paused or deleted) |
🔄 Helper Methods
generateAdIdsXml(adID)
Converts comma-separated ad IDs to SOAP XML format.
Input: "444555666,777888999"
Output:
<a1:long>444555666</a1:long>
<a2:long>777888999</a2:long>
getAdsColumn()
Returns array of 71 available report column names for ad performance reports.
⚠️ Important Notes
- 🚫 No Direct Campaign Query: Cannot get ads directly by campaign ID
- 🔄 Multiple API Calls: Campaign query requires looping through ad groups
- 📊 Async Reporting: Metrics endpoint returns request ID, not actual data
- 🕒 Report Polling: Must implement polling to check report completion
- 📁 CSV Format: Reports returned as CSV files (Format: Csv)
- 🎯 Ad Group Required: Ads always belong to ad groups
- 🔢 Default Ad Type: If not specified, defaults to
ExpandedTextfor reporting - 📊 71 Metrics Available: Full list in
getAdsColumn() - 🌍 Timezone: Affects date range interpretation
- 🔐 Ad Type Filter: Can filter both queries and reports by ad type
- 🔄 Token Auto-Refresh: Middleware ensures valid tokens
- 🏗️ SOAP Protocol: XML requests/responses (parsed to JSON)
- 📋 Campaign Type Filter: Can filter reports by
Search,Shopping, orDynamicSearchAds - 🎨 Responsive Ads: Support dynamic asset combinations
- ⚡ Promise.all(): Uses parallel execution for multiple ad group queries
🔗 Related Documentation
- Authentication: OAuth 2.0 with Auto-Refresh
- Accounts: Account Hierarchy and Selection
- Campaigns: Campaign Management
- Ad Groups: Ad Group Management
- Keywords: Keyword Management
- Reports: Async Report Generation
- Microsoft Docs: Campaign Management Service
- Ads API: GetAdsByAdGroupId
- Responsive Search Ads: Best Practices