/*
* Zernio API
*
* API reference for Zernio. Authenticate with a Bearer API key. Base URL: https://zernio.com/api Versioning and deprecation: all endpoints are versioned in the URL path (current version: /v1). Breaking changes only ship in a new path version; existing versions keep working. Deprecated operations are marked 'deprecated: true' in this spec and announced in the changelog (https://zernio.com/changelog) before removal. Errors: every 4xx/5xx response is application/json with a machine-readable 'code' and a human-readable 'error' message (see the ErrorResponse schema). Request ids: responses carry an X-Request-Id header with the id we log the request under. Quote it when reporting a problem. A valid x-request-id you send is reused as that id.
*
* The version of the OpenAPI document: 1.404.0
* Contact: support@zernio.com
* Generated by: https://openapi-generator.tech
*/
use super::{configuration, ContentType, Error};
use crate::{apis::ResponseContent, models};
use reqwest;
use serde::{de::Error as _, Deserialize, Serialize};
/// struct for typed errors of method [`browse_ad_targeting`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum BrowseAdTargetingError {
Status400(models::ErrorResponse),
Status401(models::ErrorResponse),
Status403(),
Status404(models::ErrorResponse),
Status409(models::ErrorResponse),
Status501(),
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`estimate_ad_reach`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum EstimateAdReachError {
Status400(),
Status401(models::ErrorResponse),
Status403(),
Status404(models::ErrorResponse),
Status409(models::ErrorResponse),
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`get_linked_in_bid_pricing`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum GetLinkedInBidPricingError {
Status400(),
Status401(models::ErrorResponse),
Status403(),
Status404(models::ErrorResponse),
Status409(models::ErrorResponse),
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`get_linked_in_supply_forecast`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum GetLinkedInSupplyForecastError {
Status400(),
Status401(models::ErrorResponse),
Status403(),
Status404(models::ErrorResponse),
Status409(models::ErrorResponse),
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`search_ad_interests`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SearchAdInterestsError {
Status400(models::ErrorResponse),
Status401(models::ErrorResponse),
Status403(),
Status404(models::ErrorResponse),
Status409(models::ErrorResponse),
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`search_ad_targeting`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SearchAdTargetingError {
Status400(),
Status401(models::ErrorResponse),
Status403(),
Status404(models::ErrorResponse),
Status409(models::ErrorResponse),
UnknownValue(serde_json::Value),
}
/// Shows Meta's full tree of detailed targeting categories for an ad account, so you can browse options without a keyword. The whole Meta detailed-targeting category tree of one ad account (Meta's `GET /act_{ad_account_id}/targetingbrowse`), as one flat list you can render as a tree. Use it to show what can be targeted without a keyword; use `GET /v1/ads/targeting/search` to find an entry by name. Every node is one of two kinds: - **Selectable entity** (`selectable: true`): an interest, behavior or demographic Meta gives an id. `id` plus `type` is what a targeting spec takes. `interests`, `behaviors` and `industries` ids go in `TargetingSpec.interests`, `behaviors` and `workIndustries` on `POST /v1/ads/create`; every other type goes in `rawTargeting.flexible_spec` under its `type` as the key. `life_events`, `family_statuses` and `income` take objects (`{ \"flexible_spec\": [{ \"life_events\": [{ \"id\": \"6017476616183\" }] }] }`), while `education_statuses` and `relationship_statuses` take the bare number (`{ \"flexible_spec\": [{ \"education_statuses\": [3] }] }`): Meta answers an object there with a 500. - **Organizational node** (`selectable: false`, `id: null`): a category such as `Demographics > Financial > Income` that only groups other nodes and cannot be targeted. A few carry a `type` and have no children (`Schools`, `Employers`, `Job titles`, `Fields of study`, `Undergrad years`): those are open-ended categories Meta only exposes through search (`dimension=workEmployer` / `workPosition` on the search endpoint). `nodeId` identifies a node within this response and `parentNodeId` points at its parent (`null` for the three roots `Demographics`, `Interests`, `Behaviors`). A selectable node's `nodeId` is `{type}:{id}`, because Meta reuses small ids across types (education status 3 and relationship status 3 are different entities). An organizational node's `nodeId` is its full path joined with ` > `. Labels are kept exactly as Meta sends them, including stray leading or trailing spaces, because Meta has sibling nodes that differ only by whitespace. The interests branch is Meta's curated browse list (a few hundred entries), not every interest Meta can target: search finds the long tail. **No pagination.** Meta returns the whole catalog in one response (about 770 nodes) and ignores `limit`, so there is no cursor. Narrow it with `type`, `parentNodeId` and `selectable` instead; they are applied by Zernio. The catalog is cached for an hour per ad account and connection.
pub async fn browse_ad_targeting(
configuration: &configuration::Configuration,
account_id: &str,
ad_account_id: &str,
r#type: Option<&str>,
parent_node_id: Option<&str>,
selectable: Option<bool>,
) -> Result<models::BrowseAdTargeting200Response, Error<BrowseAdTargetingError>> {
// add a prefix to parameters to efficiently prevent name collisions
let p_query_account_id = account_id;
let p_query_ad_account_id = ad_account_id;
let p_query_type = r#type;
let p_query_parent_node_id = parent_node_id;
let p_query_selectable = selectable;
let uri_str = format!("{}/v1/ads/targeting/browse", configuration.base_path);
let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
req_builder = req_builder.query(&[("accountId", &p_query_account_id.to_string())]);
req_builder = req_builder.query(&[("adAccountId", &p_query_ad_account_id.to_string())]);
if let Some(ref param_value) = p_query_type {
req_builder = req_builder.query(&[("type", ¶m_value.to_string())]);
}
if let Some(ref param_value) = p_query_parent_node_id {
req_builder = req_builder.query(&[("parentNodeId", ¶m_value.to_string())]);
}
if let Some(ref param_value) = p_query_selectable {
req_builder = req_builder.query(&[("selectable", ¶m_value.to_string())]);
}
if let Some(ref user_agent) = configuration.user_agent {
req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
}
if let Some(ref token) = configuration.bearer_access_token {
req_builder = req_builder.bearer_auth(token.to_owned());
};
let req = req_builder.build()?;
let resp = configuration.client.execute(req).await?;
let status = resp.status();
let content_type = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("application/octet-stream");
let content_type = super::ContentType::from(content_type);
if !status.is_client_error() && !status.is_server_error() {
let content = resp.text().await?;
match content_type {
ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::BrowseAdTargeting200Response`"))),
ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::BrowseAdTargeting200Response`")))),
}
} else {
let content = resp.text().await?;
let entity: Option<BrowseAdTargetingError> = serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}
/// Estimates how many people a targeting setup would reach before you create a campaign. Returns a normalized pre-flight audience-size estimate for a targeting spec, before any campaign is created. Backed by each platform's native reach API (Meta `delivery_estimate`, LinkedIn `audienceCounts`, X `audience_summary`, Pinterest `audience_sizing`). Platforms without a usable pre-flight reach API (Google Search/Display, TikTok) return `available: false` with no bounds, so clients can hide or grey out the estimate rather than treat the absence as an error.
pub async fn estimate_ad_reach(
configuration: &configuration::Configuration,
estimate_ad_reach_request: models::EstimateAdReachRequest,
) -> Result<models::EstimateAdReach200Response, Error<EstimateAdReachError>> {
// add a prefix to parameters to efficiently prevent name collisions
let p_body_estimate_ad_reach_request = estimate_ad_reach_request;
let uri_str = format!(
"{}/v1/ads/targeting/reach-estimate",
configuration.base_path
);
let mut req_builder = configuration
.client
.request(reqwest::Method::POST, &uri_str);
if let Some(ref user_agent) = configuration.user_agent {
req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
}
if let Some(ref token) = configuration.bearer_access_token {
req_builder = req_builder.bearer_auth(token.to_owned());
};
req_builder = req_builder.json(&p_body_estimate_ad_reach_request);
let req = req_builder.build()?;
let resp = configuration.client.execute(req).await?;
let status = resp.status();
let content_type = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("application/octet-stream");
let content_type = super::ContentType::from(content_type);
if !status.is_client_error() && !status.is_server_error() {
let content = resp.text().await?;
match content_type {
ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::EstimateAdReach200Response`"))),
ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::EstimateAdReach200Response`")))),
}
} else {
let content = resp.text().await?;
let entity: Option<EstimateAdReachError> = serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}
/// LinkedIn-only. Returns the suggested bid and bid limits for a targeting spec, plus the daily-budget bounds LinkedIn will accept. Use it before creating a campaign to pick a bid inside the allowed range and warn the user if their daily budget is below the minimum. Wraps LinkedIn's `adBudgetPricing` finder. Non-LinkedIn accounts return `available: false` so clients can hide the pricing UI without treating it as a failure.
pub async fn get_linked_in_bid_pricing(
configuration: &configuration::Configuration,
get_linked_in_bid_pricing_request: models::GetLinkedInBidPricingRequest,
) -> Result<models::GetLinkedInBidPricing200Response, Error<GetLinkedInBidPricingError>> {
// add a prefix to parameters to efficiently prevent name collisions
let p_body_get_linked_in_bid_pricing_request = get_linked_in_bid_pricing_request;
let uri_str = format!("{}/v1/ads/targeting/bid-pricing", configuration.base_path);
let mut req_builder = configuration
.client
.request(reqwest::Method::POST, &uri_str);
if let Some(ref user_agent) = configuration.user_agent {
req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
}
if let Some(ref token) = configuration.bearer_access_token {
req_builder = req_builder.bearer_auth(token.to_owned());
};
req_builder = req_builder.json(&p_body_get_linked_in_bid_pricing_request);
let req = req_builder.build()?;
let resp = configuration.client.execute(req).await?;
let status = resp.status();
let content_type = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("application/octet-stream");
let content_type = super::ContentType::from(content_type);
if !status.is_client_error() && !status.is_server_error() {
let content = resp.text().await?;
match content_type {
ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::GetLinkedInBidPricing200Response`"))),
ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::GetLinkedInBidPricing200Response`")))),
}
} else {
let content = resp.text().await?;
let entity: Option<GetLinkedInBidPricingError> = serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}
/// LinkedIn-only. Forecasted impressions, clicks, spend and ~20 other metrics for a targeting spec over a time range. Wraps LinkedIn's `adSupplyForecasts` finder. Each returned series carries a `metricType` (IMPRESSION, CLICK, SPENDING, MAX_POTENTIAL_BUDGET, COST_PER_MILLION_IMPRESSIONS, ...) and a `granularity` (DAILY, SEVEN_DAY, THIRTY_DAY, CUSTOM). LinkedIn caps the daily spending forecast at 1.2x the daily budget and returns 0 once the total budget is exhausted. Non-LinkedIn accounts return `available: false`.
pub async fn get_linked_in_supply_forecast(
configuration: &configuration::Configuration,
get_linked_in_supply_forecast_request: models::GetLinkedInSupplyForecastRequest,
) -> Result<models::GetLinkedInSupplyForecast200Response, Error<GetLinkedInSupplyForecastError>> {
// add a prefix to parameters to efficiently prevent name collisions
let p_body_get_linked_in_supply_forecast_request = get_linked_in_supply_forecast_request;
let uri_str = format!(
"{}/v1/ads/targeting/supply-forecast",
configuration.base_path
);
let mut req_builder = configuration
.client
.request(reqwest::Method::POST, &uri_str);
if let Some(ref user_agent) = configuration.user_agent {
req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
}
if let Some(ref token) = configuration.bearer_access_token {
req_builder = req_builder.bearer_auth(token.to_owned());
};
req_builder = req_builder.json(&p_body_get_linked_in_supply_forecast_request);
let req = req_builder.build()?;
let resp = configuration.client.execute(req).await?;
let status = resp.status();
let content_type = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("application/octet-stream");
let content_type = super::ContentType::from(content_type);
if !status.is_client_error() && !status.is_server_error() {
let content = resp.text().await?;
match content_type {
ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::GetLinkedInSupplyForecast200Response`"))),
ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::GetLinkedInSupplyForecast200Response`")))),
}
} else {
let content = resp.text().await?;
let entity: Option<GetLinkedInSupplyForecastError> = serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}
/// Deprecated alias for `GET /v1/ads/targeting/search?dimension=interest`. Kept for backward compatibility, it returns the legacy `{ interests: [...] }` shape rather than the normalized `{ results: [...] }`. New integrations should use `GET /v1/ads/targeting/search` with `dimension=interest`.
#[deprecated]
pub async fn search_ad_interests(
configuration: &configuration::Configuration,
q: &str,
account_id: &str,
) -> Result<models::SearchAdInterests200Response, Error<SearchAdInterestsError>> {
// add a prefix to parameters to efficiently prevent name collisions
let p_query_q = q;
let p_query_account_id = account_id;
let uri_str = format!("{}/v1/ads/interests", configuration.base_path);
let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
req_builder = req_builder.query(&[("q", &p_query_q.to_string())]);
req_builder = req_builder.query(&[("accountId", &p_query_account_id.to_string())]);
if let Some(ref user_agent) = configuration.user_agent {
req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
}
if let Some(ref token) = configuration.bearer_access_token {
req_builder = req_builder.bearer_auth(token.to_owned());
};
let req = req_builder.build()?;
let resp = configuration.client.execute(req).await?;
let status = resp.status();
let content_type = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("application/octet-stream");
let content_type = super::ContentType::from(content_type);
if !status.is_client_error() && !status.is_server_error() {
let content = resp.text().await?;
match content_type {
ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::SearchAdInterests200Response`"))),
ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::SearchAdInterests200Response`")))),
}
} else {
let content = resp.text().await?;
let entity: Option<SearchAdInterestsError> = serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}
/// Finds targeting options by name, such as locations, interests or behaviors, and returns the ids to use in ad targeting. Resolve a human-readable query into the platform's opaque targeting ids used in the `TargetingSpec` (`countries`/`regions`/`cities`/`zips`/`metros` geo keys, and `interests`/`behaviors` entity ids) on `POST /v1/ads/create`, `POST /v1/ads/targeting/reach-estimate`, and `saved_targeting` audiences. The `dimension` param selects what is searched: - `geo`: locations, further scoped by `geoType` - `interest` - `interestSuggestion`: Meta only. Interests related to the interest names in `q`, from Meta's interest suggestions. `q` lists interest names, case sensitive, names and not ids: either comma-separated (`NBA,Michael Jordan`) or, for names that contain a comma, a JSON array of strings (`[\"Earth, Wind & Fire\",\"Yoga\"]`, URL-encoded). A `q` starting with `[` is always read as JSON. A `q` with no name or malformed JSON returns 400 naming `q`. Meta also suggests behaviors and demographics; Zernio keeps only interests, so every id goes in `TargetingSpec.interests`, like `interest` results. Zernio classifies them with a Meta call pinned to `en_US` (ids whose `path` starts with `Interests`). Without `locale` results are in English; with another `locale`, a second call in that locale returns the same interests with localized names and `path`. Zernio asks Meta for three times `limit` (at most 100) before filtering, so fewer than `limit` results can come back. Meta weights the first name heavily: a multi-name `q` mostly returns suggestions for the first one - `behavior`: Meta, TikTok and LinkedIn, matched by name; ids feed `TargetingSpec.behaviors`. Meta: its fixed behaviors catalog (e.g. `Small business owners`, `Frequent Travelers`). TikTok: video and creator interaction categories (e.g. `Software & Apps`), with ids like `video:1913101` or `creator:24001` and `path` starting with `Video interactions` or `Creator interactions` (hashtags are their own `hashtag` dimension). LinkedIn: member behaviors (e.g. `Frequent Travelers`, `Job Seekers`, `Recently Promoted`), ids like `urn:li:memberBehavior:9`. Google has no separate behavior catalog: its in-market and affinity segments come back from `interest`, and X removed behavior targeting from its Ads API - `interestKeyword`: TikTok only. The \"Additional interests\" of TikTok Ads Manager (e.g. `folk` returns `Folk Music`), from TikTok's keyword recommendations for the seed `q`. Ids look like `keyword:123456` and go in `TargetingSpec.interests`, next to interest categories. Only keywords TikTok lets an ad group target come back (`status` `EFFECTIVE`). TikTok returns no audience size - `hashtag`: TikTok only. Hashtags recommended for the seed `q`, to target people who viewed videos with them. Ids look like `hashtag:123456` and go in `TargetingSpec.behaviors`. Only hashtags TikTok reports `ONLINE` come back. Spaces in `q` are removed first, because TikTok recommends no hashtags for a multi-word seed (`acoustic guitar` searches `acousticguitar`) - `income`: the household-income tiers the platform can target (Meta, TikTok, Google). The id is the normalized tier (`top_5`, `top_10`, `top_10_25`, `top_25_50`) to pass as `TargetingSpec.incomeTier`, never a platform segment id. Meta's tiers are US-only ZIP-code percentiles (label and `audienceSize` come live from Meta), TikTok expresses all four and Google only `top_10`. `q` is matched against the label, so `top` or `income` lists every tier - `language`: Google-only - `workPosition`, `workEmployer`, `workIndustry`: the Meta-only work demographics, whose ids feed `TargetingSpec.workPositions`/`workEmployers`/`workIndustries` - `industry`, `jobFunction`, `seniority`, `companySize`, `skill`: the LinkedIn-only B2B facets, whose URNs feed `TargetingSpec.industries`/`jobFunctions`/`seniorities`/`companySizes`/`skills` - `community`: Reddit only. Subreddits matching `q`; the id is the subreddit name for `TargetingSpec.communities` / `excludedCommunities`, `platformId` Reddit's own id and `audienceSize` its subscriber count when Reddit reports it - `conversationKeyword`: Reddit only. Reddit's keyword suggestions for the seed `q`; the id is the keyword for `TargetingSpec.conversationKeywords` / `excludedConversationKeywords` Availability of each dimension varies by platform. A dimension a platform cannot search returns a 400 naming `dimension`; it never falls back to another dimension, and every result's `type` is the requested dimension (or the geo level for `geo`). TikTok `interest` searches TikTok's interest category catalog (about 700 categories over four levels, `path` holds the parent categories), matched by name in Zernio. The ids are what `TargetingSpec.interests` sends to TikTok as `interest_category_ids`. For `interestKeyword` and `hashtag`, a `q` made only of prefixed ids (`keyword:101,keyword:102` or `hashtag:201`) looks those ids up instead of searching, unavailable ones included with their `status` (`INEFFECTIVE` or `OFFLINE`), so ids read back from an ad group (`interest_keyword_ids`, `actions` in `nativeSettings`) resolve to names. An `interestKeyword` lookup takes at most 50 ids (more returns 400 naming `q`). Work industries are a fixed ~30-entry Meta catalog with no server-side query, so `workIndustry` matching, ranking and `limit` happen in Zernio. `language` is likewise a fixed, checked-in table of Google's targetable `language_constant` rows (id, ISO code, name) matched by name or code, capped at 20, with no network call; its ids feed `TargetingSpec.languages`. Results are normalized across platforms into a single shape, so the same client code consumes Meta, TikTok, LinkedIn, X, Pinterest, Google and Reddit results. TikTok geo searches return every matching level in one list (`type` is `country`, `region`, `city`, `district`, or `metro` for DMA areas), and `geoType` is not applied. Results are scoped to the advertiser's targetable markets (pass `adAccountId` when the connection holds several advertisers). A two-letter `q` also matches a country by ISO code (`q=GB` returns the United Kingdom first). A `country` result's id is its ISO 3166-1 alpha-2 code, for `targeting.countries`; every other id is TikTok's numeric location id, usable in `regions`/`cities`/`metros` keys on `POST /v1/ads/create`. LinkedIn geo searches also return every matching level in one list, and neither `geoType` nor `countryCode` is applied: LinkedIn's typeahead only returns a name and a URN per result, with no level or country field to filter on. A result whose URN is a country Zernio holds a code for has `type: country` and its ISO 3166-1 alpha-2 code as the id, for `targeting.countries`. Every other result has `type: region` and keeps its `urn:li:geo:*` URN as the id, usable as a `regions[].key` on `POST /v1/ads/create`, `POST /v1/ads/boost` and `POST /v1/ads/targeting/reach-estimate` (LinkedIn puts countries and regions in the same `locations` facet, so both target the same way). LinkedIn B2B searches (`industry`, `jobFunction`, `seniority`, `companySize`, `skill`) return the full URN to pass straight back, so no URN id fragment has to be assembled by hand: `urn:li:industry:4`, `urn:li:function:8`, `urn:li:seniority:6`, `urn:li:staffCountRange:(51,200)`, `urn:li:skill:1346`. Only `industry` and `skill` are server-side name searches (LinkedIn's typeahead finder). LinkedIn exposes no typeahead for job functions, seniorities and company sizes, so Zernio fetches each whole table (26, 10 and 9 entries), caches it, and does the matching, ranking and `limit` cutoff itself. Those three never carry `audienceSize`, and `countryCode` and `geoType` are not applied to any of the four. Google geo searches resolve against Google's geoTargetConstants and return every matching level in one list; `geoType` is not applied (Google's `target_type` is an open taxonomy that does not map one-to-one onto the `geoType` enum), so filter client-side on the returned `type` (`country`, `region`, `city`, `zip`, `metro`, or the lowercased Google target type for rarer levels). `countryCode` scopes the search to one country. A `country` result's id is its ISO 3166-1 alpha-2 code, for `targeting.countries`; every other id is Google's numeric criterion id, usable as a `regions`/`cities`/`zips`/`metros` `key` on `POST /v1/ads/create`. Google city radius is not supported (pass a `customLocations` lat/lng pin for a radius); country targeting also accepts plain ISO codes via `countries` with no search call. Pinterest resolves against three whole-catalog endpoints (interests, locations, regions) with no server-side query or pagination, so matching, ranking and the `limit` cutoff all happen in Zernio; the catalog is independent of any ad account and results never carry `audienceSize`. Names come back localized to the connected Pinterest account's language (there is no way to force a locale), so match against whatever language that account returns. `geoType` routes to a different Pinterest catalog: - `country` and `metro_area` read the locations catalog (`type` is `country` or `metro`) - `region` reads the regions catalog (`type` is `region`, its id a `regions[].key` on `POST /v1/ads/create`) - `all` and the default `city` merge both catalogs with honest per-entry `type`s, since Pinterest has no city-level catalog and `city` is an alias for `all`, not a literal city search - `zip`, `subcity`, `neighborhood`, `place` and `geo_market` return a 400: Pinterest exposes no postal-code catalog, pass postal codes directly as `targeting.zips: [{ key }]` on `POST /v1/ads/create` Reddit (`redditads` connection) searches `geo`, `interest`, `community` and `conversationKeyword`; any other dimension returns 400. Its geo search honours `geoType` `city` (the default, a server-side city search), `zip` (`q` is the postal code), `country`, `region` and `metro_area` (matched in Zernio against Reddit's geolocation catalog) and `all`; other values return 400. `countryCode` scopes every geo search. Each result's id is Reddit's geolocation id, usable as a `regions`/`metros`/`cities`/`zips` `key` (country ids go in `countries`). `interest` matches Reddit's interest catalog in Zernio, with the category as `path`. For geo queries, `q` should contain only the locality name (e.g. `\"Amsterdam\"`, not `\"Amsterdam, NL\"`). Use `countryCode` to disambiguate. Meta language: Meta matches `q` against names in one language and returns names and `path` in that language. Pass `locale` (e.g. `pl_PL`) to choose it. Without `locale`, the `behavior` and `workIndustry` catalogs and `interestSuggestion` are in `en_US`, and `interest`, `geo`, `workPosition` and `workEmployer` follow the language of the Facebook user who connected the account (so a Russian-language Facebook user gets Russian paths, and Polish names need `locale=pl_PL`). A non-English `behavior` or `workIndustry` query without `locale` never matches, so pass the locale of the names you search. Meta's `interest` search matches its own interest names and has no fuzzy or synonym mode, so a phrase Meta does not use as an interest name (e.g. `dental implants`) can return nothing while a shorter word (`dentistry`) does. If Meta returns an empty `behavior`, `workIndustry` or `income` catalog the request fails with a 502 `platform_api_error` instead of an empty `results` list.
pub async fn search_ad_targeting(
configuration: &configuration::Configuration,
account_id: &str,
q: &str,
dimension: Option<&str>,
geo_type: Option<&str>,
country_code: Option<&str>,
ad_account_id: Option<&str>,
locale: Option<&str>,
limit: Option<i32>,
) -> Result<models::SearchAdTargeting200Response, Error<SearchAdTargetingError>> {
// add a prefix to parameters to efficiently prevent name collisions
let p_query_account_id = account_id;
let p_query_q = q;
let p_query_dimension = dimension;
let p_query_geo_type = geo_type;
let p_query_country_code = country_code;
let p_query_ad_account_id = ad_account_id;
let p_query_locale = locale;
let p_query_limit = limit;
let uri_str = format!("{}/v1/ads/targeting/search", configuration.base_path);
let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
req_builder = req_builder.query(&[("accountId", &p_query_account_id.to_string())]);
req_builder = req_builder.query(&[("q", &p_query_q.to_string())]);
if let Some(ref param_value) = p_query_dimension {
req_builder = req_builder.query(&[("dimension", ¶m_value.to_string())]);
}
if let Some(ref param_value) = p_query_geo_type {
req_builder = req_builder.query(&[("geoType", ¶m_value.to_string())]);
}
if let Some(ref param_value) = p_query_country_code {
req_builder = req_builder.query(&[("countryCode", ¶m_value.to_string())]);
}
if let Some(ref param_value) = p_query_ad_account_id {
req_builder = req_builder.query(&[("adAccountId", ¶m_value.to_string())]);
}
if let Some(ref param_value) = p_query_locale {
req_builder = req_builder.query(&[("locale", ¶m_value.to_string())]);
}
if let Some(ref param_value) = p_query_limit {
req_builder = req_builder.query(&[("limit", ¶m_value.to_string())]);
}
if let Some(ref user_agent) = configuration.user_agent {
req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
}
if let Some(ref token) = configuration.bearer_access_token {
req_builder = req_builder.bearer_auth(token.to_owned());
};
let req = req_builder.build()?;
let resp = configuration.client.execute(req).await?;
let status = resp.status();
let content_type = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("application/octet-stream");
let content_type = super::ContentType::from(content_type);
if !status.is_client_error() && !status.is_server_error() {
let content = resp.text().await?;
match content_type {
ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::SearchAdTargeting200Response`"))),
ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::SearchAdTargeting200Response`")))),
}
} else {
let content = resp.text().await?;
let entity: Option<SearchAdTargetingError> = serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}