/*
* 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.217.1
* 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 [`add_users_to_ad_audience`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AddUsersToAdAudienceError {
Status400(),
Status401(models::ErrorResponse),
Status403(),
Status404(models::InlineObject1),
Status422(),
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`create_ad_audience`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum CreateAdAudienceError {
Status400(),
Status401(models::ErrorResponse),
Status403(),
Status404(models::ErrorResponse),
Status409(models::ErrorResponse),
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`delete_ad_audience`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum DeleteAdAudienceError {
Status401(models::ErrorResponse),
Status403(),
Status404(models::InlineObject1),
Status422(),
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`get_ad_audience`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum GetAdAudienceError {
Status401(models::ErrorResponse),
Status403(),
Status404(models::InlineObject1),
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`list_ad_audiences`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ListAdAudiencesError {
Status400(models::ErrorResponse),
Status401(models::ErrorResponse),
Status403(),
Status404(models::ErrorResponse),
Status409(models::ErrorResponse),
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`replace_ad_audience_companies`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ReplaceAdAudienceCompaniesError {
Status400(),
Status401(models::ErrorResponse),
Status403(),
Status404(models::InlineObject1),
Status422(),
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`update_ad_audience`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum UpdateAdAudienceError {
Status400(),
Status401(models::ErrorResponse),
Status403(),
Status404(models::InlineObject1),
Status422(),
Status501(),
UnknownValue(serde_json::Value),
}
/// Upload user data to a customer_list audience. Data is SHA256-hashed server-side before sending to the platform. Email is used on every platform; phone is used on Meta only (other platforms ignore it). On TikTok and Pinterest, the first upload also provisions the audience (deferred create). LinkedIn uploads are full-replace. Max 10,000 users per request. customer_list only. A LinkedIn `company_list` audience takes company rows, not people: send those to `POST /v1/ads/audiences/{audienceId}/companies`. This endpoint 422s for every other audience type.
pub async fn add_users_to_ad_audience(
configuration: &configuration::Configuration,
audience_id: &str,
add_users_to_ad_audience_request: models::AddUsersToAdAudienceRequest,
) -> Result<models::AddUsersToAdAudience200Response, Error<AddUsersToAdAudienceError>> {
// add a prefix to parameters to efficiently prevent name collisions
let p_path_audience_id = audience_id;
let p_body_add_users_to_ad_audience_request = add_users_to_ad_audience_request;
let uri_str = format!(
"{}/v1/ads/audiences/{audienceId}/users",
configuration.base_path,
audienceId = crate::apis::urlencode(p_path_audience_id)
);
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_add_users_to_ad_audience_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::AddUsersToAdAudience200Response`"))),
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::AddUsersToAdAudience200Response`")))),
}
} else {
let content = resp.text().await?;
let entity: Option<AddUsersToAdAudienceError> = serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}
/// Create a custom audience. `customer_list` is supported on Meta, Google, X, LinkedIn, TikTok, and Pinterest. `website` (pixel/tag visitors) and `lookalike` are supported on Meta, TikTok, Pinterest and Google. `meta_engagement` is Meta-only, `tiktok_engagement` TikTok-only, `pinterest_engagement` Pinterest-only; `company_list`, `engagement` and `website_retargeting` are LinkedIn-only. A type sent to a platform that does not support it is a 422 `FEATURE_NOT_AVAILABLE`. Per-platform rules for `website`: - Meta: `pixelId` required, `retentionDays` 1-180, optional `urlContains` or raw `rule`. `event` is not accepted. - TikTok: `pixelId` required, `retentionDays` one of 7, 14, 30, 60, 90, 180. `event` is a TikTok pixel event (default `PAGE BROWSE`; also `CLICK BUTTON`, `PIXEL SUBMIT FORM`, `CONTACT`, `DOWNLOAD`, `PIXEL ADD PAYMENT INFO`, `COMPLETE PAYMENT`, `INITIATE CHECKOUT`, `COMPLETE REGISTRATION`, `PRODUCT DETAIL PAGE BROWSE`, `PIXEL SEARCH`, `PIXEL ADD TO CART`, `PLACE AN ORDER`, `PIXEL ADD TO WISHLIST`, `PIXEL SUBSCRIBE`), optional `urlContains`. - Pinterest: `pixelId` is the Pinterest tag id, `retentionDays` 1-540, optional `event` (e.g. `checkout`, `addtocart`). Pinterest matches URLs exactly, so `urlContains` is a 400 there. - Google: no `pixelId` (the account's Google tag is used), `retentionDays` 1-540, `urlContains` defaults to `/` (all visitors). Built as a rule-based user list with prepopulation requested. Per-platform rules for `lookalike`: Meta takes `ratio` (0.01-0.20); Pinterest takes `ratio` in whole percents (0.01-0.10) and `country` US, CA or GB; TikTok and Google take `size` (narrow, balanced, broad) instead of `ratio`. TikTok needs a seed with at least 1,000 matched people. `saved_targeting` stores a reusable TargetingSpec (no member upload, no adAccountId) that you reference later via `savedTargetingId` on `POST /v1/ads/create`. How the audience gets filled depends on the type: - `customer_list` is created empty. Add members with `POST /v1/ads/audiences/{audienceId}/users`. On TikTok and Pinterest the audience is provisioned lazily on that first upload (until then its status is `pending`). - `company_list` is filled AT CREATION from the `companies` array below, which is required. To change the list afterwards send the new full list to `POST /v1/ads/audiences/{audienceId}/companies` (a replace, not a merge). The `/users` endpoint rejects these audiences with a 422. - `website`, `website_retargeting`, `engagement`, `meta_engagement`, `tiktok_engagement`, `pinterest_engagement` and `lookalike` fill themselves from the pixel, engagement source or seed audience you point them at. They take no member upload at all. Create is not idempotent, never auto-retry.
pub async fn create_ad_audience(
configuration: &configuration::Configuration,
create_ad_audience_request: models::CreateAdAudienceRequest,
) -> Result<models::CreateAdAudience201Response, Error<CreateAdAudienceError>> {
// add a prefix to parameters to efficiently prevent name collisions
let p_body_create_ad_audience_request = create_ad_audience_request;
let uri_str = format!("{}/v1/ads/audiences", 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_create_ad_audience_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::CreateAdAudience201Response`"))),
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::CreateAdAudience201Response`")))),
}
} else {
let content = resp.text().await?;
let entity: Option<CreateAdAudienceError> = serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}
/// Removes the audience on its ad platform, then deletes the Zernio record. Meta, Google, TikTok, LinkedIn list and engagement segments, and X are deleted; Pinterest audiences and LinkedIn `website_retargeting` segments are archived, which is how those platforms remove them. `saved_targeting` audiences exist only on Zernio, so only the local record is removed. If the platform refuses, the error is returned and the Zernio record is kept, so a retry is safe. An audience the platform no longer has counts as removed. Google Ads does not allow removing lookalike lists through its API, so those return 422 `FEATURE_NOT_AVAILABLE`.
pub async fn delete_ad_audience(
configuration: &configuration::Configuration,
audience_id: &str,
) -> Result<models::DeleteAccountGroup200Response, Error<DeleteAdAudienceError>> {
// add a prefix to parameters to efficiently prevent name collisions
let p_path_audience_id = audience_id;
let uri_str = format!(
"{}/v1/ads/audiences/{audienceId}",
configuration.base_path,
audienceId = crate::apis::urlencode(p_path_audience_id)
);
let mut req_builder = configuration
.client
.request(reqwest::Method::DELETE, &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());
};
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::DeleteAccountGroup200Response`"))),
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::DeleteAccountGroup200Response`")))),
}
} else {
let content = resp.text().await?;
let entity: Option<DeleteAdAudienceError> = serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}
/// Returns the local audience record and fresh data from Meta (if available).
pub async fn get_ad_audience(
configuration: &configuration::Configuration,
audience_id: &str,
) -> Result<models::GetAdAudience200Response, Error<GetAdAudienceError>> {
// add a prefix to parameters to efficiently prevent name collisions
let p_path_audience_id = audience_id;
let uri_str = format!(
"{}/v1/ads/audiences/{audienceId}",
configuration.base_path,
audienceId = crate::apis::urlencode(p_path_audience_id)
);
let mut req_builder = configuration.client.request(reqwest::Method::GET, &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());
};
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::GetAdAudience200Response`"))),
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::GetAdAudience200Response`")))),
}
} else {
let content = resp.text().await?;
let entity: Option<GetAdAudienceError> = serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}
/// Returns custom audiences for the given ad account. Supports Meta, Google, TikTok, Pinterest, LinkedIn, and X.
pub async fn list_ad_audiences(
configuration: &configuration::Configuration,
account_id: &str,
ad_account_id: &str,
platform: Option<&str>,
r#type: Option<&str>,
) -> Result<models::ListAdAudiences200Response, Error<ListAdAudiencesError>> {
// 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_platform = platform;
let p_query_type = r#type;
let uri_str = format!("{}/v1/ads/audiences", 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_platform {
req_builder = req_builder.query(&[("platform", ¶m_value.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 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::ListAdAudiences200Response`"))),
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::ListAdAudiences200Response`")))),
}
} else {
let content = resp.text().await?;
let entity: Option<ListAdAudiencesError> = serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}
/// Upload the company rows of a LinkedIn `company_list` audience (account-based marketing). LinkedIn-only, every other platform returns 422. A LinkedIn audience segment holds exactly one uploaded list, so the list you send here REPLACES the segment's list instead of being appended to it: always send the full set of companies. LinkedIn returns only the identifier of the uploaded file, never its rows, so the merge cannot be done for you, keep the source list on your side. How the matching behaves: - Rows are plain text (not hashed), matched against LinkedIn's own company graph. - Matching is asynchronous: LinkedIn takes up to 48h for a new audience and up to 24h for a later update, and the audience stays `processing` meanwhile. - LinkedIn does not document how quickly companies dropped from the list stop being targeted, so treat removals as eventual rather than immediate. - LinkedIn recommends at least 1,000 companies for a usable match rate, and caps a list at 300,000. The initial list is sent with `companies` on `POST /v1/ads/audiences`; this endpoint is for every change after that.
pub async fn replace_ad_audience_companies(
configuration: &configuration::Configuration,
audience_id: &str,
replace_ad_audience_companies_request: models::ReplaceAdAudienceCompaniesRequest,
) -> Result<models::ReplaceAdAudienceCompanies200Response, Error<ReplaceAdAudienceCompaniesError>> {
// add a prefix to parameters to efficiently prevent name collisions
let p_path_audience_id = audience_id;
let p_body_replace_ad_audience_companies_request = replace_ad_audience_companies_request;
let uri_str = format!(
"{}/v1/ads/audiences/{audienceId}/companies",
configuration.base_path,
audienceId = crate::apis::urlencode(p_path_audience_id)
);
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_replace_ad_audience_companies_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::ReplaceAdAudienceCompanies200Response`"))),
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::ReplaceAdAudienceCompanies200Response`")))),
}
} else {
let content = resp.text().await?;
let entity: Option<ReplaceAdAudienceCompaniesError> = serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}
/// Update an audience. `saved_targeting` audiences accept `name`, `description`, and `spec` (full replacement, no merge, Zernio-only, no platform call). Platform audiences (uploaded/website/lookalike) accept `name` and `description` only, updated on the platform first and then mirrored locally; their rules are immutable, so `spec` returns 400 for them. Platform audience updates are Meta-only for now (other platforms return 501). Ads already created from a saved_targeting audience are unaffected, they snapshot the targeting at creation.
pub async fn update_ad_audience(
configuration: &configuration::Configuration,
audience_id: &str,
update_ad_audience_request: models::UpdateAdAudienceRequest,
) -> Result<models::CreateAdAudience201Response, Error<UpdateAdAudienceError>> {
// add a prefix to parameters to efficiently prevent name collisions
let p_path_audience_id = audience_id;
let p_body_update_ad_audience_request = update_ad_audience_request;
let uri_str = format!(
"{}/v1/ads/audiences/{audienceId}",
configuration.base_path,
audienceId = crate::apis::urlencode(p_path_audience_id)
);
let mut req_builder = configuration.client.request(reqwest::Method::PUT, &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_update_ad_audience_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::CreateAdAudience201Response`"))),
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::CreateAdAudience201Response`")))),
}
} else {
let content = resp.text().await?;
let entity: Option<UpdateAdAudienceError> = serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}