🔑 Bing Ads - Keyword Management
📖 Overview
Keyword management for targeting search queries with various match types, bid strategies, and quality score optimization. Like ads, there is no direct campaign-to-keywords operation in Bing Ads API - keywords must be retrieved through ad groups.
Source Files:
- Controller:
external/Integrations/BingAds/Controllers/Keywords/KeywordsController.js - Model:
external/Integrations/BingAds/Models/Keywords/KeywordsModel.js - Routes:
external/Integrations/BingAds/Routes/keywords.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
🏗️ Keyword Retrieval Architecture
graph TD
A[Campaign ID] --> B[Get Ad Groups]
B --> C1[Ad Group 1]
B --> C2[Ad Group 2]
C1 --> D1[Get Keywords by Ad Group]
C2 --> D2[Get Keywords by Ad Group]
D1 --> E1[Keywords Array]
D2 --> E2[Keywords 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 GetKeywordsByCampaignId operation. Available operations are:
GetKeywordsByAdGroupId- Get keywords by ad groupGetKeywordsByIds- Get specific keywords by IDGetKeywordsByEditorialStatus- Get keywords by editorial status
Workaround: To get keywords by campaign, the code:
- Gets ad groups for campaign
- Loops through ad groups
- Gets keywords 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 KeywordsModel
participant SOAP as Bing SOAP API
Client->>DC: GET /keywords?campaignID={id}
DC->>MW: Check token expiration
MW->>DC: Valid access token
DC->>Model: getKeywordsByCampaignID()
Model->>SOAP: POST GetAdGroupsByCampaignId
SOAP-->>Model: Ad groups list
loop For each ad group
Model->>SOAP: POST GetKeywordsByAdGroupId
SOAP-->>Model: Keywords for ad group
end
Model->>Model: Aggregate campaign + adgroup + keywords
Model-->>DC: Nested structure
DC-->>Client: JSON response
🔧 Keyword Functions
List Keywords by Campaign
GET /keywords?campaignID={id}
Purpose: Retrieve all keywords for a specific campaign (via ad groups)
Controller: KeyWordsController.index() → getKeywordsByCampaignID()
Model Method: KeywordsModel.getKeywordsByCampaignID(customerAccountID, customerID, campaignID)
SOAP Operations: GetAdGroupsByCampaignId + multiple GetKeywordsByAdGroupId calls
Request:
GET /v1/integrations/bing/ads/keywords?campaignID=987654321&customerAccountID=111222333&customerID=123456
Authorization: Bearer {jwt_token}
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
campaignID | Integer | ✅ | Campaign ID to filter keywords |
customerAccountID | Integer | ✅ | Account ID |
customerID | Integer | ⚪ | Customer ID (optional) |
Validation:
- If
campaignIDmissing, returns error: "You can get keywords 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 keyword fetching
- Loop Ad Groups: For each ad group, call
getKeywordsByAdGroupId() - Aggregate: Combine campaign + ad group + keywords into nested structure
- Return: Single object if one ad group, array if multiple
SOAP Request XML (GetKeywordsByAdGroupId):
<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">GetKeywordsByAdGroupId</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>
<GetKeywordsByAdGroupIdRequest xmlns="https://bingads.microsoft.com/CampaignManagement/v13">
<AdGroupId>111222333</AdGroupId>
</GetKeywordsByAdGroupIdRequest>
</s:Body>
</s:Envelope>
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"
},
"keyword": [
{
"Id": 444555666,
"AdGroupId": 111222333,
"Text": "quality products",
"MatchType": "Phrase",
"Bid": {
"Amount": 2.50
},
"Status": "Active",
"EditorialStatus": "Active",
"DestinationUrl": "https://example.com/products",
"Param1": null,
"Param2": null,
"Param3": null
},
{
"Id": 777888999,
"AdGroupId": 111222333,
"Text": "buy products online",
"MatchType": "Broad",
"Bid": {
"Amount": 1.75
},
"Status": "Active",
"EditorialStatus": "Active"
}
]
}
}
JSON Response (Multiple Ad Groups):
{
"success": true,
"message": "SUCCESS",
"data": [
{
"campaign": { ... },
"adgroup": { ... },
"keyword": [ ... ]
},
{
"campaign": { ... },
"adgroup": { ... },
"keyword": [ ... ]
}
]
}
Key Response Fields:
| Field | Type | Description |
|---|---|---|
campaign | Object | Campaign details |
adgroup | Object | Ad group details |
keyword | Array | Keywords for the ad group |
Id | Integer | Keyword identifier |
Text | String | Keyword text/phrase |
MatchType | String | Exact, Phrase, Broad |
Bid.Amount | Decimal | Maximum CPC bid amount |
Status | String | Active, Paused, Deleted |
EditorialStatus | String | Editorial review status |
DestinationUrl | String | Landing page URL |
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 Keywords by Ad Group
GET /keywords?adGroupID={id}
Purpose: Retrieve keywords for a specific ad group (direct operation)
Controller: KeyWordsController.index() → getKeywordsByAdGroupId()
Model Method: KeywordsModel.getKeywordsByAdGroupId(customerAccountID, customerID, adGroupID)
SOAP Operation: GetKeywordsByAdGroupId
Request:
GET /v1/integrations/bing/ads/keywords?adGroupID=111222333&customerAccountID=111222333&customerID=123456
Authorization: Bearer {jwt_token}
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
adGroupID | Integer | ✅ | Ad group ID to filter keywords |
customerAccountID | Integer | ✅ | Account ID |
customerID | Integer | ⚪ | Customer ID (optional) |
JSON Response:
{
"success": true,
"message": "SUCCESS",
"data": [
{
"Id": 444555666,
"AdGroupId": 111222333,
"Text": "quality products",
"MatchType": "Phrase",
"Bid": {
"Amount": 2.50
},
"Status": "Active",
"EditorialStatus": "Active",
"DestinationUrl": "https://example.com/products",
"FinalUrls": {
"string": ["https://example.com/products"]
},
"Param1": null,
"Param2": null,
"Param3": null
},
{
"Id": 777888999,
"AdGroupId": 111222333,
"Text": "best deals",
"MatchType": "Exact",
"Bid": {
"Amount": 3.00
},
"Status": "Active",
"EditorialStatus": "Active"
}
]
}
Keyword Object Fields:
| Field | Type | Description |
|---|---|---|
Id | Integer | Keyword ID |
AdGroupId | Integer | Parent ad group ID |
Text | String | Keyword text (search query) |
MatchType | String | Exact, Phrase, Broad |
Bid.Amount | Decimal | Maximum CPC bid |
Status | String | Active, Paused, Deleted |
EditorialStatus | String | Editorial review status |
DestinationUrl | String | Legacy destination URL |
FinalUrls | Object | Array of final destination URLs |
Param1, Param2, Param3 | String/Null | Dynamic text substitution parameters |
Side Effects:
- None (read-only operation)
Get Keyword by ID
GET /keywords/{keywordID}
Purpose: Retrieve specific keyword by ID
Controller: KeyWordsController.show()
Model Method: KeywordsModel.getKeywordsById(customerAccountID, customerID, adGroupID, keywordID)
SOAP Operation: GetKeywordsByIds
Request:
GET /v1/integrations/bing/ads/keywords/444555666?adGroupID=111222333&customerAccountID=111222333&customerID=123456
Authorization: Bearer {jwt_token}
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
keywordID | Integer | ✅ | Specific keyword ID |
adGroupID | Integer | ✅ | Parent ad group ID |
customerAccountID | Integer | ✅ | Account ID |
customerID | Integer | ✅ | Customer ID |
Validation:
- If any required param missing, returns error: "Please provide customerAccountID,customerID,adGroupID and keywordID"
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">GetKeywordsByIds</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>
<GetKeywordsByIdsRequest xmlns="https://bingads.microsoft.com/CampaignManagement/v13">
<AdGroupId>111222333</AdGroupId>
<KeywordIds i:nil="false" xmlns:a1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<a1:long>444555666</a1:long>
</KeywordIds>
</GetKeywordsByIdsRequest>
</s:Body>
</s:Envelope>
Helper Method: generateKeywordsIdsXml(keywordID)
- Splits comma-separated IDs
- Generates XML:
<a1:long>{id}</a1:long>for each ID
JSON Response:
{
"success": true,
"message": "SUCCESS",
"data": {
"Keywords": {
"Id": 444555666,
"AdGroupId": 111222333,
"Text": "quality products",
"MatchType": "Phrase",
"Bid": {
"Amount": 2.50
},
"Status": "Active",
"EditorialStatus": "Active"
}
}
}
Get Keyword Performance Metrics
GET /keywords/metrics
Purpose: Generate asynchronous performance report for keywords with quality scores
Controller: KeyWordsController.metrics()
Model Method: KeywordsModel.getKeywordReport(...)
SOAP Operation: SubmitGenerateReport
Service: Reporting Service v13
Request:
GET /v1/integrations/bing/ads/keywords/metrics?customerAccountID=111222333&customerID=123456&campaignID=987654321&campaignType=Search&adGroupID=111222333&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 |
keywordID | Integer | ⚪ | Specific keyword ID | All keywords |
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="KeywordPerformanceReportRequest">
<ExcludeColumnHeaders>false</ExcludeColumnHeaders>
<ExcludeReportFooter>false</ExcludeReportFooter>
<ExcludeReportHeader>false</ExcludeReportHeader>
<Format>Csv</Format>
<Language>English</Language>
<ReportName>KeywordPerformanceReportRequest</ReportName>
<ReturnOnlyCompleteData>false</ReturnOnlyCompleteData>
<Aggregation>Daily</Aggregation>
<Columns>
<KeywordPerformanceReportColumn>AccountName</KeywordPerformanceReportColumn>
<KeywordPerformanceReportColumn>Keyword</KeywordPerformanceReportColumn>
<KeywordPerformanceReportColumn>KeywordId</KeywordPerformanceReportColumn>
<KeywordPerformanceReportColumn>MatchType</KeywordPerformanceReportColumn>
<KeywordPerformanceReportColumn>Impressions</KeywordPerformanceReportColumn>
<KeywordPerformanceReportColumn>Clicks</KeywordPerformanceReportColumn>
<KeywordPerformanceReportColumn>QualityScore</KeywordPerformanceReportColumn>
<KeywordPerformanceReportColumn>ExpectedCtr</KeywordPerformanceReportColumn>
<KeywordPerformanceReportColumn>AdRelevance</KeywordPerformanceReportColumn>
<KeywordPerformanceReportColumn>LandingPageExperience</KeywordPerformanceReportColumn>
<!-- 67+ more columns available -->
</Columns>
<!-- Optional: Filter by specific keyword -->
<Filter i:nil="false">
<Keywords i:nil="false" xmlns:a1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<a1:string>444555666</a1:string>
</Keywords>
</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>
<CampaignType>Search</CampaignType>
</CampaignReportScope>
</Campaigns>
</Scope>
<Time>
<PredefinedTime i:nil="false">Last7Days</PredefinedTime>
<ReportTimeZone>EasternTimeUSCanada</ReportTimeZone>
</Time>
</ReportRequest>
</SubmitGenerateReportRequest>
</s:Body>
</s:Envelope>
Available Report Columns (73 total - hardcoded in source):
| Column | Description |
|---|---|
AccountName, AccountNumber, AccountId | Account identifiers |
CampaignName, CampaignId, CampaignStatus | Campaign info |
AdGroupName, AdGroupId, AdGroupStatus | Ad group info |
Keyword, KeywordId, KeywordStatus | Keyword info |
AdId, AdType, DestinationUrl | Associated ad info |
BidMatchType, DeliveredMatchType | Match type information |
CurrentMaxCpc | Current maximum CPC bid |
Impressions, Clicks, Ctr | Core metrics |
Spend, AverageCpc, AveragePosition | Cost metrics |
Conversions, ConversionRate, CostPerConversion | Conversion metrics |
Revenue, ReturnOnAdSpend, RevenuePerConversion | Revenue metrics |
QualityScore | Overall keyword quality (1-10) |
ExpectedCtr | Expected CTR component (1-3) |
AdRelevance | Ad relevance component (1-3) |
LandingPageExperience | Landing page component (1-3) |
QualityImpact | Quality score impact |
DeviceType, DeviceOS, Language | Targeting dimensions |
Network, TopVsOther | Network placement |
BidStrategyType | Bidding strategy type |
Mainline1Bid, MainlineBid, FirstPageBid | Bid estimates |
AllConversions, AllRevenue, ViewThroughConversions | Advanced metrics |
Note: All 73 columns are hardcoded in the XML request (see source code lines 255-325)
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
🎯 Match Types
| Match Type | Description | Example Keyword | Triggers For |
|---|---|---|---|
Exact | Exact match only | [quality products] | "quality products" only |
Phrase | Phrase match with close variants | "quality products" | "buy quality products", "quality products online" |
Broad | Broad match with variations | quality products | "high quality items", "product quality", "best products" |
📊 Quality Score Components
Overall Quality Score: 1-10 scale (10 is best)
Three Components (each scored 1-3):
| Component | Description | Impact |
|---|---|---|
| Expected CTR | Predicted click-through rate | How likely ad will be clicked |
| Ad Relevance | How well ad matches keyword | Ad copy relevance to search |
| Landing Page Experience | Landing page quality and relevance | User experience after click |
Score Meanings:
3- Above Average (good)2- Average (acceptable)1- Below Average (needs improvement)
💰 Bid Estimates
| Bid Type | Description |
|---|---|
Mainline1Bid | Estimated bid for top position (mainline 1) |
MainlineBid | Estimated bid for mainline (top section) |
FirstPageBid | Estimated bid for first page (any position) |
These estimates help optimize bids for desired ad positions.
📝 Status Values
Keyword Status
| Status | Description |
|---|---|
Active | Keyword is active and eligible to trigger ads |
Paused | Keyword is manually paused |
Deleted | Keyword is deleted (soft delete) |
Editorial Status
| Status | Description |
|---|---|
Active | Approved and serving |
ActiveLimited | Serving with limitations |
Disapproved | Rejected by editorial review |
Inactive | Not serving (paused or deleted) |
🔄 Helper Methods
generateKeywordsIdsXml(keywordID)
Converts comma-separated keyword IDs to SOAP XML format.
Input: "444555666,777888999"
Output:
<a1:long>444555666</a1:long>
<a2:long>777888999</a2:long>
⚠️ Important Notes
- 🚫 No Direct Campaign Query: Cannot get keywords 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: Keywords always belong to ad groups
- 🔢 Quality Score: Available only in reporting, not in keyword retrieval
- 📊 73 Columns: All hardcoded in source (no dynamic column selection)
- 🌍 Timezone: Affects date range interpretation
- 🔐 Four IDs Required: customerAccountID, customerID, adGroupID, keywordID for specific keyword retrieval
- 🔄 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 - 🎨 Dynamic Parameters: Param1, Param2, Param3 for dynamic text substitution
- ⚡ Promise.all(): Uses parallel execution for multiple ad group queries
- 🎯 Keyword Filter: Can filter reports by specific keyword ID in Filter element
🔗 Related Documentation
- Authentication: OAuth 2.0 with Auto-Refresh
- Accounts: Account Hierarchy and Selection
- Campaigns: Campaign Management
- Ad Groups: Ad Group Management
- Ads: Ad Management
- Reports: Async Report Generation
- Microsoft Docs: Campaign Management Service
- Keywords API: GetKeywordsByAdGroupId
- Quality Score: Understanding Quality Score