# Migrate from Google Ads

Every Google Ads API service, method and GAQL resource you call today, the Zernio endpoint that replaces it and the fields to send, plus the calls with no Zernio equivalent.

Each table maps a Google Ads API call to the Zernio endpoint that does the same job, with the fields to send.

Read the first two tables before migrating any call. Every row below assumes them.

## Rules for every call

| Topic | Google Ads API | Zernio |
|---|---|---|
| Base URL | `https://googleads.googleapis.com/v{N}/customers/{customerId}/...` | `https://zernio.com/api/v1/...` |
| Auth | OAuth token, `developer-token` and `login-customer-id` headers | `Authorization: Bearer YOUR_ZERNIO_API_KEY` only. Zernio holds the OAuth token and developer token |
| API version | You move to each new version | Zernio moves the version, nothing to change on your side |
| Writes | `GoogleAdsService.Mutate` or a per-service `Mutate*` call with operations | One REST endpoint per resource, JSON body |
| Money | Micros (`amount_micros: 10500000`) | Whole currency (`10.5`) in request bodies. Raw Google rows keep micros: GAQL results, keyword ideas, recommendations, search terms |
| Status on status endpoints | `ENABLED`, `PAUSED` | `active`, `paused` (lowercase) on every `/status` endpoint |
| Status on create bodies | `ENABLED`, `PAUSED` | `ACTIVE`, `PAUSED` on `POST /v1/ads/create`, `POST /v1/ads/campaigns`, `POST /v1/ads/ad-sets`. Asset groups keep `ENABLED`, `PAUSED` |
| Remove | `remove` operation, sets `REMOVED`, cannot be undone | `DELETE` on the resource, also final at Google |
| Several objects in one request | Temporary ids (`campaignBudgets/-1`) in one mutate | `POST /v1/ads/create` builds budget, campaign, ad group, ad and criteria in one call |
| Dry run | `validate_only` on any mutate | `validateOnly` on account creation, manager links, user invitations, billing, asset groups and `POST /v1/ads/create`. Not on `POST /v1/ads/campaigns` |
| Reads | Always live | Lists are Zernio's synced copy. Single reads and Google-only endpoints are live, often cached 10 minutes, and return `cachedAt` and `stale` |
| Rate limits | Your developer token quota | 15 requests per minute per Google Ads account and 120 per minute per user on live calls. A `429` carries `details.budgetScope`, or `quotaExhausted` with `resetsAt` |

## Ids

| What | Google Ads API | Zernio |
|---|---|---|
| Connection | Your OAuth refresh token | `accountId`: connect once with `GET /v1/connect/{platform}/ads` and `platform` set to `googleads`, then read it from `GET /v1/accounts` |
| Customer | `1234567890`, resource `customers/1234567890` | `adAccountId`: the same digits, no dashes. List them with `GET /v1/ads/accounts?accountId=...` (`accounts[].id`). Google-only endpoints also accept `customerId`. Optional when the connection has one customer |
| Manager account | `login-customer-id` header | `managerCustomerId` in the body, only on account creation and manager links |
| Campaign | `customers/{cid}/campaigns/111` | `campaignId` = `111`, Google's numeric id. A campaign has no Zernio id |
| Ad group | `customers/{cid}/adGroups/222` | `adSetId` = `222`, Google's numeric id |
| Ad | `customers/{cid}/adGroupAds/222~333` | `adId` = Zernio's ad `_id`, or Google's ad id `333` once synced. `GET /v1/ads/{adId}/analytics` needs the Zernio `_id` |
| Keyword | `customers/{cid}/adGroupCriteria/222~444` | `keywordId` = Zernio's id or `222~444` |
| Labels, bid strategies, conversion actions, custom goals, keyword lists, asset groups | Numeric id in the resource name | The same Google numeric id |
| Audience | `customers/{cid}/userLists/555` | `audienceId` = Zernio's id. `platformAudienceId` holds `555` |
| Platform filter on ads endpoints | | `platform=google` |

Campaign and ad group endpoints find the customer through ads Zernio has synced. A campaign with no synced ads returns `404`, except on conversion goals and on `DELETE` with `accountId`.

To find the Zernio ad ids of a Google campaign, call `GET /v1/ads?accountId=...&campaignId=111` and read `ads[]._id`.

## Accounts

| Google Ads API | Zernio | Send | Watch out |
|---|---|---|---|
| `CustomerService.ListAccessibleCustomers` and GAQL `customer_client` | `GET /v1/ads/accounts` | `accountId`, optional `adAccountId`, `limit` up to 1000, `inScopeOnly` | Cached 1 hour |
| `CustomerService.CreateCustomerClient` | `POST /v1/ads/accounts` | `platform: "google"`, `accountId`, `managerCustomerId`, `name`, `currency` (ISO 4217), `timeZone` (IANA), `validateOnly` | Not idempotent. Returns `adAccountId` |
| `CustomerService.MutateCustomer` (`tracking_url_template`, `final_url_suffix`) | `PATCH /v1/ads/accounts` | `accountId`, `adAccountId`, `trackingUrlTemplate`, `finalUrlSuffix` | Other fields return `400` |
| GAQL `customer_client` (manager tree) | `GET /v1/ads/accounts/hierarchy` | `accountId`, optional `adAccountId` | Cached 10 minutes |
| `CustomerClientLinkService.MutateCustomerClientLink` (invite) | `POST /v1/ads/accounts/manager-links` | `accountId`, `managerCustomerId`, `clientCustomerId`, `validateOnly` | |
| `CustomerManagerLinkService.MutateCustomerManagerLink`, `MutateCustomerClientLink` (status) | `PATCH /v1/ads/accounts/manager-links` | The same, plus `managerLinkId`, `action`: `accept`, `decline`, `cancel`, `unlink` | |
| `CustomerUserAccessInvitationService.MutateCustomerUserAccessInvitation`, GAQL `customer_user_access_invitation` | `GET`, `POST`, `DELETE /v1/ads/accounts/user-invitations` | POST: `accountId`, `adAccountId`, `emailAddress`, `accessRole` (`ADMIN`, `STANDARD`, `READ_ONLY`, `EMAIL_ONLY`), `validateOnly`. DELETE: `invitationId` | Invitations only |
| GAQL `multi_party_auth_review`, `MultiPartyAuthReviewService.ResolveMultiPartyAuthReview` | `GET`, `DELETE /v1/ads/accounts/user-invitations/pending-approvals` | DELETE: `accountId`, `adAccountId`, `multiPartyAuthReviewId` | |
| GAQL `billing_setup`, `account_budget`; `PaymentsAccountService.ListPaymentsAccounts` | `GET /v1/ads/accounts/billing` | `accountId`, `adAccountId` | |
| `BillingSetupService.MutateBillingSetup`, `AccountBudgetProposalService.MutateAccountBudgetProposal` | `POST /v1/ads/accounts/billing` | `paymentsAccountId`, `spendingLimit` (whole currency), `budgetName`, `purchaseOrderNumber`, `validateOnly` | Monthly invoicing only. `409` when billing exists |
| GAQL `change_event` | `GET /v1/ads/activity` | `accountId`, `adAccountId`, `since`, `until` (YYYY-MM-DD), `objectId`, `limit` up to 200, `after` | Paged with `paging.after` |

## Campaigns and budgets

| Google Ads API | Zernio | Send | Watch out |
|---|---|---|---|
| Budget, campaign, ad group, ad and criteria in one mutate (temporary ids) | `POST /v1/ads/create` | See [creating campaigns](#creating-campaigns) | Leaving out `campaignType` creates a Display campaign |
| `CampaignBudgetService` and `CampaignService` create, campaign only | `POST /v1/ads/campaigns` | `accountId`, `adAccountId`, `name`, `goal`, `budgetAmount`, `budgetType: "daily"`, bidding fields, `networkSettings`, `locationTargetingType`, `status` | Search campaigns only. No `validateOnly`, `startDate` or `endDate`. Returns `campaignId` |
| GAQL `campaign` | `GET /v1/ads/campaigns` | `accountId`, `adAccountId`, `campaignId`, `status`, `fromDate`, `toDate`, `page`, `limit` up to 100 | Synced copy. Rows carry `platformCampaignId`. Campaigns with no ads are not listed. Budget comes back as `amountMicros` |
| GAQL `campaign` with its ad groups and ads | `GET /v1/ads/tree` | `platform=google`, `campaignId` (comma list), `limit` up to 100 | Synced copy |
| `CampaignService.MutateCampaigns` update | `PUT /v1/ads/campaigns/{campaignId}` | Any of `name`, `budget: {amount, type}`, `bidStrategy`, `bidAmount`, `roasAverageFloor`, `targetImpressionShare`, `manualCpc: {maxCpc}`, `networkSettings`, `trackingUrlTemplate`, `finalUrlSuffix` | Needs synced ads |
| `CampaignBudgetService.MutateCampaignBudgets` update | `PUT /v1/ads/campaigns/{campaignId}` | `budget: {amount, type: "daily"}`; add `allowSharedBudgetUpdate: true` for a shared budget | Daily budgets only. A shared budget without the flag returns `409` |
| `campaign.status` | `PUT /v1/ads/campaigns/{campaignId}/status` | `status`: `active` or `paused` | Returns `updated` or `skipped` |
| The same on many campaigns | `POST /v1/ads/campaigns/bulk-status` | `status`, `campaigns: [{platformCampaignId, platform: "google"}]`, up to 50 | |
| `CampaignService` remove | `DELETE /v1/ads/campaigns/{campaignId}` | `accountId` when Zernio holds no ads for it | Final at Google |
| GAQL `campaign` bidding fields | `GET /v1/ads/campaigns/{campaignId}/bidding` | `accountId`, `platform=google` (required), `adAccountId` | Whole currency. Cached 10 minutes |
| `CampaignCriterionService` with `AD_SCHEDULE` | `GET`, `PUT /v1/ads/campaigns/{campaignId}/ad-schedule` | PUT `schedule: [{dayOfWeek, startHour, startMinute, endHour, endMinute, bidModifier}]`, up to 42; `[]` clears | `bidModifier` 0.1 to 10. GET with `includePerformance=true` adds metrics |
| `CampaignBudgetService` with `explicitly_shared`, GAQL `campaign_budget` | `GET`, `POST /v1/ads/shared-budgets` | POST `accountId`, `adAccountId`, `name`, `amount` (whole currency), `type` | |

## Creating campaigns

`POST /v1/ads/create` replaces the multi-operation mutate. Every type needs `accountId`, `adAccountId`, `budgetAmount` (whole currency) and `budgetType: "daily"`. It returns `ad._id`, `platformCampaignId`, `platformAdSetId` and `platformAdId`.

| Campaign type | Send | Watch out |
|---|---|---|
| Search | `campaignType: "search"`, `goal` (`traffic`, `engagement` or `awareness`), `headline`, `body`, `linkUrl`, usually `keywords` | Fewer than 3 headlines or 2 descriptions are padded. `countries` defaults to `US` |
| Display | `campaignType: "display"`, the Search fields, `businessName` (up to 25 characters), `images.landscape`, `images.square` | |
| Performance Max | `campaignType: "pmax"`, `assetGroup: {businessName, finalUrl, longHeadline, headlines (3 to 15), descriptions (2 to 5), images: {landscape, square, logo}}` | No `headline`, `body` or `linkUrl`. Created paused |
| Demand Gen | `campaignType: "demand_gen"`, `demandGen: {finalUrl, businessName, headlines, descriptions, images}`; video with `youtubeVideoIds` and `longHeadlines`, carousel with `carouselCards` (2 to 10) | Created paused |
| Ad group or ad in an existing campaign | The Search, Display or Demand Gen fields, plus `existingCampaignId` or `adSetId` | Budget, bidding, schedule and `networkSettings` return `400` here |
| Options at create time | `negativeKeywords`, `campaignNegativeKeywords`, `audienceInclude`, `audienceExclude`, `sitelinks`, `callouts`, `structuredSnippets`, `locationTargetingType`, `validateOnly` | |

## Ad groups

| Google Ads API | Zernio | Send | Watch out |
|---|---|---|---|
| GAQL `ad_group` | `GET /v1/ads/ad-sets` | `accountId`, `adAccountId`, `campaignId`, `adSetId`, `status`, `page`, `limit` up to 500 | Synced copy |
| `AdGroupService.MutateAdGroups` create | `POST /v1/ads/ad-sets` | `accountId`, `platform: "google"`, `campaignId`, `name`, `status` (`ACTIVE` or `PAUSED`), `maxCpc` (whole currency), `adAccountId` | Returns `adSetId` |
| `AdGroupService.MutateAdGroups` update | `PUT /v1/ads/ad-sets/{adSetId}` | `name`, `status` | `maxCpc` cannot change after creation |
| `ad_group.status` | `PUT /v1/ads/ad-sets/{adSetId}/status` | `status`: `active` or `paused` | |
| `AdGroupService` remove | `DELETE /v1/ads/ad-sets/{adSetId}` | | Final at Google |

## Ads

| Google Ads API | Zernio | Send | Watch out |
|---|---|---|---|
| GAQL `ad_group_ad` | `GET /v1/ads` | `accountId`, `adAccountId`, `campaignId`, `adSetId`, `platformAdId`, `status`, `page`, `limit` up to 500 | Synced copy. Returns `_id` |
| GAQL `ad_group_ad`, one ad | `GET /v1/ads/{adId}` | | Search ad text read live, cached 10 minutes |
| `AdService.MutateAds` update | `PUT /v1/ads/{adId}` | Search: `headlines` (3 to 15), `descriptions` (2 to 4), `finalUrls`. Display: the same plus `creative: {longHeadline, businessName, imageUrl, squareImageUrl}`. Performance Max: `assetGroup`. Demand Gen: `demandGen` | Each list replaces the whole set. `name` is not accepted |
| Ad group criteria through the ad | `PUT /v1/ads/{adId}` | `targeting: {keywords, negativeKeywords, devices, locations, languages}`, `budget`, `status` | Keyword lists replace the ad group's keywords |
| `AdGroupAdService.MutateAdGroupAds` status | `PUT /v1/ads/{adId}/status` | `status`: `active` or `paused` | |
| `AdGroupAdService` remove | `DELETE /v1/ads/{adId}` | | Final at Google |
| `campaign.tracking_url_template`, `final_url_suffix` | `GET`, `PATCH /v1/ads/{adId}/tracking-tags` | `trackingUrlTemplate` (must contain `{lpurl}`), `finalUrlSuffix`; `""` clears | Edits the ad's campaign |

## Performance Max asset groups

| Google Ads API | Zernio | Send | Watch out |
|---|---|---|---|
| GAQL `asset_group`, `AssetGroupService` create | `GET`, `POST /v1/ads/campaigns/{campaignId}/asset-groups` | POST `name`, `finalUrls` (1 to 10), `finalMobileUrls`, `path1`, `path2`, `status`, `assets: [{fieldType, asset or text or imageUrl or youtubeVideoId}]`, `listingGroupFilter`, `validateOnly` | Created paused. Cached 10 minutes |
| `AssetGroupService` update or remove | `GET`, `PATCH`, `DELETE /v1/ads/campaigns/{campaignId}/asset-groups/{assetGroupId}` | PATCH `name`, `status`, URLs, paths. DELETE `?validateOnly=true` | |
| `AssetService` and `AssetGroupAssetService` | `POST /v1/ads/campaigns/{campaignId}/asset-groups/{assetGroupId}/assets` | `link: [...]`, `unlink: [{fieldType, asset}]` | Unlinking leaves the asset, as at Google |
| `AssetGroupListingGroupFilterService` | `PUT /v1/ads/campaigns/{campaignId}/asset-groups/{assetGroupId}/listing-group-filters` | `tree: {children: [{dimension, excluded}]}` | Replaces the whole tree. Needs Merchant Center |

## Keywords and negatives

| Google Ads API | Zernio | Send | Watch out |
|---|---|---|---|
| GAQL `keyword_view`, `ad_group_criterion` | `GET /v1/ads/keywords` | `accountId`, `adAccountId`, `campaignId`, `adSetId`, `status`, `matchType`, `negative`, `search`, `page`, `limit` up to 500 | Synced about daily. Ad group keywords only |
| `AdGroupCriterionService` create | `POST /v1/ads/keywords` | `accountId`, `adSetId`, `keywords` (strings or `{text, matchType}`, up to 1000), `negative` | Adds, never replaces |
| `AdGroupCriterionService` update | `PATCH /v1/ads/keywords/{keywordId}` | `status`: `active` or `paused` | Text, match type and bid cannot change. Negatives return `422` |
| `AdGroupCriterionService` remove | `DELETE /v1/ads/keywords/{keywordId}` | | |
| `KeywordPlanIdeaService.GenerateKeywordIdeas` | `POST /v1/ads/keywords/ideas` | `accountId`, `adAccountId`, `seedKeywords` (up to 20) or `seedUrl`, `countries` (up to 10), `languageConstantId`, `network`, `pageSize`, `pageToken` | Google's rows, money in micros |
| `KeywordPlanIdeaService.GenerateKeywordHistoricalMetrics` | `POST /v1/ads/keywords/historical-metrics` | `keywords` (up to 1000), `includeAverageCpc` | |
| GAQL `search_term_view` | `GET /v1/ads/search-terms` | `accountId`, `adAccountId`, `fromDate`, `toDate`, `campaignId`, `adSetId`, `pageToken` | Cached 1 hour. `costMicros` in micros |
| `CampaignCriterionService` negative keywords | `GET`, `PUT /v1/ads/campaigns/{campaignId}/negative-keywords` | PUT `keywords` (up to 1000) | Replaces the list; `[]` clears |
| `SharedSetService`, GAQL `shared_set` | `GET`, `POST /v1/ads/accounts/negative-keyword-lists` | POST `accountId`, `adAccountId`, `name`, `keywords` (up to 5000) | |
| The same, one list | `GET`, `PUT`, `DELETE /v1/ads/accounts/negative-keyword-lists/{listId}` | PUT `name` | |
| `SharedCriterionService` | `PUT /v1/ads/accounts/negative-keyword-lists/{listId}/keywords` | `keywords` (up to 5000) | Replaces the keywords |
| `CampaignSharedSetService` | `GET`, `PUT /v1/ads/campaigns/{campaignId}/negative-keyword-lists` | `listIds` (up to 20) | Attaches exactly these lists |

## Targeting

| Google Ads API | Zernio | Send | Watch out |
|---|---|---|---|
| `CampaignCriterionService` (device, location, language), `campaign.geo_target_type_setting` | `GET`, `PUT /v1/ads/campaigns/{campaignId}/targeting` | PUT `targeting: {devices, locations, excludedLocations, languages, locationTargetingType}` | No keywords here: use the keyword endpoints. Cached 10 minutes |
| `GeoTargetConstantService.SuggestGeoTargetConstants` | `GET /v1/ads/targeting/search` | `accountId`, `q`, `dimension` (`geo`, `interest`, `language`, `income`), `countryCode`, `limit` | |

## Audiences

| Google Ads API | Zernio | Send | Watch out |
|---|---|---|---|
| `UserListService.MutateUserLists` create | `POST /v1/ads/audiences` | `accountId`, `adAccountId`, `name`, `type`: `customer_list`; `website` with `retentionDays`, `urlContains`; `lookalike` with `sourceAudienceId`, `country`, `size` | Returns Zernio's `audienceId` |
| GAQL `user_list` | `GET /v1/ads/audiences`, `GET /v1/ads/audiences/{audienceId}` | `accountId`, `adAccountId` | Target with `platformAudienceId` |
| `UserListService` update or remove | `PUT`, `DELETE /v1/ads/audiences/{audienceId}` | | |
| `OfflineUserDataJobService` or `UserDataService.UploadUserData` | `POST /v1/ads/audiences/{audienceId}/users` | `users: [{email}]`, up to 10,000 | Sent through Google's Data Manager API, which Google now requires for new integrations. Emails only |

## Conversions

| Google Ads API | Zernio | Send | Watch out |
|---|---|---|---|
| GAQL `conversion_action` (upload actions), `ConversionActionService` create `UPLOAD_CLICKS` | `GET`, `POST /v1/accounts/{accountId}/conversion-destinations` | | Gives the `destinationId` for uploads |
| `ConversionUploadService.UploadClickConversions` | `POST /v1/ads/conversions` | `accountId`, `destinationId` (`customers/{cid}/conversionActions/{id}`), `events: [{eventName, eventTime (Unix seconds), eventId, value, currency, user: {email, phone, clickIds: {gclid, gbraid, wbraid}}}]`, `consent` | Sent through the Data Manager API. Up to 2,000 events per chunk. Partial failures still return `200`. Up to 3 hours to show |
| `ConversionAdjustmentUploadService.UploadConversionAdjustments` | `POST /v1/ads/conversions/adjustments` | `adjustments: [{adjustmentType, adjustmentTime, orderId or gclid with conversionTime, restatementValue}]` | |
| GAQL `conversion_action`, `ConversionActionService` create (webpage) | `GET`, `POST /v1/ads/conversions/actions` | POST `type: "WEBPAGE"` | Webpage actions only |
| `ConversionActionService` update | `PATCH /v1/ads/conversions/actions/{actionId}` | `name`, `primaryForGoal`, `status` (`ENABLED` or `REMOVED`) | |
| `CustomerConversionGoalService` | `GET`, `PATCH /v1/ads/conversions/goals` | PATCH `goals: [{category, origin, biddable}]`, up to 100 | Cached 10 minutes |
| `CustomConversionGoalService` | `GET`, `POST /v1/ads/conversions/custom-goals` | POST `name`, `conversionActionIds` (1 to 100) | |
| The same, one goal | `PATCH`, `DELETE /v1/ads/conversions/custom-goals/{goalId}` | | |
| `CampaignConversionGoalService`, `ConversionGoalCampaignConfigService` | `GET`, `PATCH /v1/ads/campaigns/{campaignId}/conversion-goals` | `goals`, `goalConfigLevel` (`CUSTOMER` or `CAMPAIGN`), `customConversionGoalId` | Works on campaigns with no synced ads |
| `ConversionTrackingSetting`, `customer.auto_tagging_enabled`, Google tag | `/v1/accounts/{accountId}/tracking-tags` and `/v1/accounts/{accountId}/tracking-tags/{tagId}` | PATCH `autoTagging` | Also `events`, `stats` and `install` under the tag |

## Reporting

| Google Ads API | Zernio | Send | Watch out |
|---|---|---|---|
| `GoogleAdsService.Search` with any GAQL `SELECT` | `GET /v1/ads/insights` | `accountId`, `query`, `adAccountId`, `pageToken` | Live. Google's raw rows, money in micros. Pages of 10,000 rows. `SELECT` only |
| `metrics.*` for one ad | `GET /v1/ads/{adId}/analytics` | `fromDate`, `toDate` | `adId` must be Zernio's `_id`. From Zernio's synced store |
| `metrics.*` for one campaign, with impression share | `GET /v1/ads/campaigns/{campaignId}/analytics` | `fromDate`, `toDate`, `platform` | Impression share is a ratio |
| `metrics.*` ranked across ads | `GET /v1/ads/performance` | `accountId`, `fromDate`, `toDate` (both required), `platform=google`, `sortBy`, `limit` up to 100 | Synced store |

## Recommendations

| Google Ads API | Zernio | Send | Watch out |
|---|---|---|---|
| GAQL `recommendation` | `GET /v1/ads/recommendations` | `accountId`, `adAccountId`, `campaignId`, `types` (comma list) | Cached 10 minutes. `details` in micros |
| `RecommendationService.ApplyRecommendation` | `POST /v1/ads/recommendations/apply` | `recommendations: [{resourceName, parameters}]`, up to 100 | Not idempotent. `parameters` in micros |
| `RecommendationService.DismissRecommendation` | `POST /v1/ads/recommendations/dismiss` | `resourceNames` (resource names or ids), up to 100 | |

## Labels

| Google Ads API | Zernio | Send | Watch out |
|---|---|---|---|
| GAQL `label`, `LabelService` create | `GET`, `POST /v1/ads/labels` | POST `name`, `backgroundColor`, `description`. GET `limit` up to 100, `after` | |
| `LabelService` update or remove | `PATCH`, `DELETE /v1/ads/labels/{labelId}` | | |
| `CampaignLabelService`, `AdGroupLabelService`, `AdGroupAdLabelService`, `AdGroupCriterionLabelService` | `POST`, `DELETE /v1/ads/labels/{labelId}/assignments` | `campaignIds`, `adSetIds`, `adIds` (`222~333`), `keywordIds` (`222~444`), up to 1000 each | Google ids |

## Assets

| Google Ads API | Zernio | Send | Watch out |
|---|---|---|---|
| Sitelink `AssetService` and `CustomerAssetService`, GAQL `customer_asset` | `GET`, `POST`, `PUT`, `DELETE /v1/ads/accounts/sitelinks` | POST the sitelinks, PUT `updates: [{assetResourceName, ...}]`, DELETE `assetId` | DELETE unlinks; Google cannot delete an asset |
| The same for callouts | `GET`, `POST`, `PUT`, `DELETE /v1/ads/accounts/callouts` | As sitelinks | |
| The same for structured snippets | `GET`, `POST`, `PUT`, `DELETE /v1/ads/accounts/structured-snippets` | As sitelinks | |
| `CampaignAssetService` | `GET`, `POST`, `PUT`, `DELETE /v1/ads/campaigns/{campaignId}/assets` | POST `sitelinks`, `callouts`, `structuredSnippets`, `images` (URLs). DELETE `assetResourceNames`, `campaignAssetResourceNames` | |
| `AdGroupAssetService` | `GET`, `POST`, `PUT`, `DELETE /v1/ads/ad-sets/{adSetId}/assets` | As campaign assets | |

## Bidding strategies

| Google Ads API | Zernio | Send | Watch out |
|---|---|---|---|
| GAQL `bidding_strategy`, `BiddingStrategyService` create | `GET`, `POST /v1/ads/bid-strategies` | POST `type` (`TARGET_CPA`, `TARGET_ROAS`, `MAXIMIZE_CONVERSIONS`, `MAXIMIZE_CONVERSION_VALUE`, `TARGET_IMPRESSION_SHARE`), `targetCpa` (whole currency), `targetRoas`, `targetImpressionShare` | Portfolio strategies. Cached 1 hour |
| `BiddingStrategyService` update | `PATCH /v1/ads/bid-strategies/{strategyId}` | `type` with `targetCpa` or `targetRoas` | |
| `campaign.bidding_strategy`, `campaign.campaign_budget` | `PUT /v1/ads/campaigns/{campaignId}` | `portfolioBidStrategyId`, `sharedBudgetId` with `allowSharedBudgetUpdate` | |

## Local Services

| Google Ads API | Zernio | Send | Watch out |
|---|---|---|---|
| GAQL `local_services_lead` | `GET /v1/ads/local-services/leads` | `fromDate`, `toDate`, `leadType`, `leadStatus`, `chargedOnly`, `pageToken` | Read only |
| GAQL `local_services_lead_conversation` | `GET /v1/ads/local-services/leads/{leadId}/conversations` | `pageToken` | Read only |

## No Zernio equivalent

Keep these on the Google Ads API, or drop them:

- **Generic calls:** `GoogleAdsService.Mutate` on arbitrary resources, GAQL writes, `GoogleAdsService.SearchStream`, `BatchJobService`, `GoogleAdsFieldService`, GAQL `change_status` as a sync feed.
- **Options:** `partial_failure`, `response_content_type: MUTABLE_RESOURCE`, and `validate_only` outside the endpoints listed under the rules.
- **Accounts:** `CustomerUserAccessService` (listing, changing or removing existing users), `CustomerManagerLinkService.MoveManagerLink`, `InvoiceService.ListInvoices`, `IdentityVerificationService`, `ProductLinkService` (Merchant Center links), `CustomerLabelService`.
- **Campaigns:** creating Shopping, Video and Smart campaigns, `CampaignDraftService`, `ExperimentService`, `CampaignGroupService`, copying a campaign, ad group or ad.
- **Bids and criteria:** `AdGroupBidModifierService` and `CampaignBidModifierService` (device and ad schedule modifiers do map), `CustomerNegativeCriterionService`, changing a keyword's text, match type or bid, `BiddingSeasonalityAdjustmentService`, `BiddingDataExclusionService`.
- **Ads and assets:** ad customizers (`CustomizerAttributeService` and its campaign and ad group services), `AdParameterService`, asset sets, `AssetGroupSignalService`, `AssetGenerationService`, `ShareablePreviewService`.
- **Audiences:** removing Customer Match members, `CustomAudienceService`, `AudienceService`, `CustomInterestService`, `RemarketingActionService`.
- **Conversions:** `ConversionUploadService.UploadCallConversions`, `ConversionCustomVariableService`, `ConversionValueRuleService`, `ConversionValueRuleSetService`.
- **Planning:** `KeywordPlanIdeaService.GenerateKeywordForecastMetrics`, `KeywordPlanService`, `ReachPlanService`, `AudienceInsightsService`, `BenchmarksService`.
- **Recommendations:** `RecommendationService.GenerateRecommendations`, `RecommendationSubscriptionService` (auto-apply).
- **Local Services writes:** `LocalServicesLeadService.AppendLeadConversation`, `ProvideLeadFeedback`.