/*
* 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.104.0
* Contact: support@zernio.com
* Generated by: https://openapi-generator.tech
*/
use crate::models;
use serde::{Deserialize, Serialize};
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct AdMetrics {
#[serde(rename = "spend", skip_serializing_if = "Option::is_none")]
pub spend: Option<f64>,
#[serde(rename = "impressions", skip_serializing_if = "Option::is_none")]
pub impressions: Option<i32>,
/// Unique people reached in the requested date range. Meta (facebook/instagram) and TikTok: the platform's own de-duplicated reach for the exact range, fetched live and cached up to ~1 hour (may lag recent delivery; on a transient platform error the value temporarily falls back to a sum of per-day reach, which overcounts people reached on multiple days or by multiple child ads). Because it is de-duplicated, reach is NOT additive on these platforms: neither daily values nor child nodes sum to the range total. LinkedIn: the campaign's `approximateMemberReach`, de-duplicated by LinkedIn per day and then summed over the range here, so it overcounts members reached on more than one day; LinkedIn withholds it below its audience privacy threshold, where it reads 0. Google, X, Pinterest and OpenAI report 0 (reach not synced). Frequency (impressions / reach) is only meaningful for Meta and TikTok.
#[serde(rename = "reach", skip_serializing_if = "Option::is_none")]
pub reach: Option<i32>,
#[serde(rename = "clicks", skip_serializing_if = "Option::is_none")]
pub clicks: Option<i32>,
/// Click-through rate (%)
#[serde(rename = "ctr", skip_serializing_if = "Option::is_none")]
pub ctr: Option<f64>,
/// Cost per click
#[serde(rename = "cpc", skip_serializing_if = "Option::is_none")]
pub cpc: Option<f64>,
/// Cost per 1000 impressions
#[serde(rename = "cpm", skip_serializing_if = "Option::is_none")]
pub cpm: Option<f64>,
#[serde(rename = "engagement", skip_serializing_if = "Option::is_none")]
pub engagement: Option<i32>,
/// Count of conversion events over the requested date range. FRACTIONAL: attribution splits one conversion across touchpoints and Google additionally reports modeled conversions, so values like 0.347 are normal. Meta: events matching the campaign's promoted_object.custom_event_type (PURCHASE, LEAD, etc.). Google: the account's tracked conversions. X and LinkedIn: their reported website/lead conversions (added 2026-07). 0 for non-conversion campaigns or when no events have fired.
#[serde(rename = "conversions", skip_serializing_if = "Option::is_none")]
pub conversions: Option<f64>,
/// All conversions, including actions excluded from the Conversions column (Google metrics.all_conversions). 0 on platforms without the concept.
#[serde(rename = "allConversions", skip_serializing_if = "Option::is_none")]
pub all_conversions: Option<f64>,
/// Derived spend / conversions in the same currency as spend. 0 when conversions is 0.
#[serde(rename = "costPerConversion", skip_serializing_if = "Option::is_none")]
pub cost_per_conversion: Option<f64>,
/// Per-action-type counts summed over the date range, keyed by the platform's action-type names. Meta: raw Insights action_type keys (link_click, offsite_conversion.fb_pixel_purchase, onsite_conversion.lead_grouped, ...), covering both engagement and conversion events. TikTok: pixel conversions (purchase, add_to_cart, initiate_checkout, view_content, complete_payment, lead) plus the paid-engagement family (follow, post_reaction for paid likes, comment, share). Follow is how FOLLOWERS-goal campaigns report their result. X: conversion types (purchase, sign_up, site_visit, download, custom). LinkedIn: conversion types (post_click, post_view, lead_gen) plus `video_view`, which carries LinkedIn's `videoViews` under the same key every other platform uses, so it feeds `engagementBreakdown.videoViews` too; it is an engagement, NOT a conversion, and is excluded from `conversions`. Google returns {} (its per-action names aren't synced per ad). Empty object when no actions are reported. NOTE: keys differ by platform, so branch on the ad's platform when interpreting them.
#[serde(rename = "actions", skip_serializing_if = "Option::is_none")]
pub actions: Option<std::collections::HashMap<String, i32>>,
/// Monetary mirror of `actions`, from Meta's Insights `action_values[]` array. Same keying: values are the revenue attributed to each action_type, in ad-account native currency (same unit as `spend`; see the campaign node's `currency` field). Use this to compute revenue-per-event (e.g. avg purchase value). Meta-only; other platforms return {}.
#[serde(rename = "actionValues", skip_serializing_if = "Option::is_none")]
pub action_values: Option<std::collections::HashMap<String, f64>>,
/// Convenience sum of purchase-type action values, picked from `actionValues` via the same priority list as `conversions` so both fields describe the same events. In ad-account native currency. 0 when the campaign has no purchase event configured. Meta-only.
#[serde(rename = "purchaseValue", skip_serializing_if = "Option::is_none")]
pub purchase_value: Option<f64>,
/// Return on ad spend, derived as `purchaseValue / spend`. 0 when `spend` is 0. Equivalent to Meta's `purchase_roas` under default attribution. At ad-set and campaign levels this is recomputed from summed purchaseValue + spend (NOT averaged across children) so it's mathematically correct at every rollup level.
#[serde(rename = "roas", skip_serializing_if = "Option::is_none")]
pub roas: Option<f64>,
/// Derived `spend / actions[type]` for every action type with a non-zero count, in ad-account native currency. Same keys as `actions`. Rounded to 4 decimals because cheap actions cost well under a cent. Recomputed from summed spend + counts at every rollup level. Empty object when spend is 0 or no actions are reported.
#[serde(rename = "costPerAction", skip_serializing_if = "Option::is_none")]
pub cost_per_action: Option<std::collections::HashMap<String, f64>>,
/// Clicks leading off Meta's surfaces to the advertiser's destination. Meta-only; other platforms report 0.
#[serde(rename = "outboundClicks", skip_serializing_if = "Option::is_none")]
pub outbound_clicks: Option<i32>,
/// Derived `outboundClicks / impressions * 100`, recomputed from sums at every rollup level.
#[serde(rename = "outboundClicksCtr", skip_serializing_if = "Option::is_none")]
pub outbound_clicks_ctr: Option<f64>,
/// In-session link clicks. Differs from the attributed `link_click` count in `actions`/`engagementBreakdown.linkClicks`, which uses the attribution window. Meta-only.
#[serde(rename = "inlineLinkClicks", skip_serializing_if = "Option::is_none")]
pub inline_link_clicks: Option<i32>,
/// Derived `inlineLinkClicks / impressions * 100`, recomputed from sums at every rollup level.
#[serde(rename = "inlineLinkClickCtr", skip_serializing_if = "Option::is_none")]
pub inline_link_click_ctr: Option<f64>,
/// People who clicked at least once. NOT additive: summed across days/children it overcounts people who clicked on multiple days or ads, so treat rollups as an upper bound (same caveat as `reach`). Meta-only.
#[serde(rename = "uniqueClicks", skip_serializing_if = "Option::is_none")]
pub unique_clicks: Option<i32>,
/// Derived `uniqueClicks / impressions * 100` (NOT Meta's reach-based unique_ctr). Inherits the non-additivity caveat of `uniqueClicks`.
#[serde(rename = "uniqueCtr", skip_serializing_if = "Option::is_none")]
pub unique_ctr: Option<f64>,
/// Number of times the video started playing, summed over the date range and across children at ad-set/campaign level. 0 for non-video ads. Sources: Meta `video_play_actions`, TikTok `video_play_actions`.
#[serde(rename = "videoPlayActions", skip_serializing_if = "Option::is_none")]
pub video_play_actions: Option<i32>,
/// Views of at least 30 seconds (or to the end, for shorter videos). Sources: Meta `video_30_sec_watched_actions` (Meta only).
#[serde(
rename = "video30SecWatchedActions",
skip_serializing_if = "Option::is_none"
)]
pub video30_sec_watched_actions: Option<i32>,
/// ThruPlays (watched to completion, or at least 15 seconds). Sources: Meta `video_thruplay_watched_actions` (Meta only).
#[serde(
rename = "videoThruplayWatchedActions",
skip_serializing_if = "Option::is_none"
)]
pub video_thruplay_watched_actions: Option<i32>,
/// Views reaching 25% of the video's length. With the other percentile fields, powers hook/hold/drop-off analysis (e.g. hook rate = videoP25WatchedActions / videoPlayActions). Sources: Meta `video_p25_watched_actions`, TikTok `video_views_p25`.
#[serde(
rename = "videoP25WatchedActions",
skip_serializing_if = "Option::is_none"
)]
pub video_p25_watched_actions: Option<i32>,
/// Views reaching 50% of the video's length. Sources: Meta `video_p50_watched_actions`, TikTok `video_views_p50`.
#[serde(
rename = "videoP50WatchedActions",
skip_serializing_if = "Option::is_none"
)]
pub video_p50_watched_actions: Option<i32>,
/// Views reaching 75% of the video's length. Sources: Meta `video_p75_watched_actions`, TikTok `video_views_p75`.
#[serde(
rename = "videoP75WatchedActions",
skip_serializing_if = "Option::is_none"
)]
pub video_p75_watched_actions: Option<i32>,
/// Views reaching 95% of the video's length. Sources: Meta `video_p95_watched_actions` (Meta only).
#[serde(
rename = "videoP95WatchedActions",
skip_serializing_if = "Option::is_none"
)]
pub video_p95_watched_actions: Option<i32>,
/// Views reaching 100% of the video's length. Sources: Meta `video_p100_watched_actions`, TikTok `video_views_p100`.
#[serde(
rename = "videoP100WatchedActions",
skip_serializing_if = "Option::is_none"
)]
pub video_p100_watched_actions: Option<i32>,
/// Average seconds watched per play. Aggregated over date ranges and across children as a play-weighted average (total watch time / total plays), never a plain average of averages. Sources: Meta `video_avg_time_watched_actions`, TikTok `average_video_play`.
#[serde(
rename = "videoAvgTimeWatchedActions",
skip_serializing_if = "Option::is_none"
)]
pub video_avg_time_watched_actions: Option<f64>,
/// Derived `spend / videoThruplayWatchedActions`, in ad-account native currency. Rounded to 4 decimals rather than the usual 2 because a ThruPlay routinely costs well under a cent. 0 when the ad has no ThruPlays (ThruPlay is Meta-only).
#[serde(rename = "costPerThruplay", skip_serializing_if = "Option::is_none")]
pub cost_per_thruplay: Option<f64>,
#[serde(rename = "funnel", skip_serializing_if = "Option::is_none")]
pub funnel: Option<Box<models::AdFunnelCounts>>,
#[serde(
rename = "engagementBreakdown",
skip_serializing_if = "Option::is_none"
)]
pub engagement_breakdown: Option<Box<models::AdEngagementCounts>>,
/// Present on individual ads only, not on campaign aggregations
#[serde(rename = "lastSyncedAt", skip_serializing_if = "Option::is_none")]
pub last_synced_at: Option<String>,
}
impl AdMetrics {
pub fn new() -> AdMetrics {
AdMetrics {
spend: None,
impressions: None,
reach: None,
clicks: None,
ctr: None,
cpc: None,
cpm: None,
engagement: None,
conversions: None,
all_conversions: None,
cost_per_conversion: None,
actions: None,
action_values: None,
purchase_value: None,
roas: None,
cost_per_action: None,
outbound_clicks: None,
outbound_clicks_ctr: None,
inline_link_clicks: None,
inline_link_click_ctr: None,
unique_clicks: None,
unique_ctr: None,
video_play_actions: None,
video30_sec_watched_actions: None,
video_thruplay_watched_actions: None,
video_p25_watched_actions: None,
video_p50_watched_actions: None,
video_p75_watched_actions: None,
video_p95_watched_actions: None,
video_p100_watched_actions: None,
video_avg_time_watched_actions: None,
cost_per_thruplay: None,
funnel: None,
engagement_breakdown: None,
last_synced_at: None,
}
}
}