diff --git a/scripts/generate_mcp_tools.py b/scripts/generate_mcp_tools.py index 9eb5981a..e8381aa3 100644 --- a/scripts/generate_mcp_tools.py +++ b/scripts/generate_mcp_tools.py @@ -145,6 +145,17 @@ def _resolve_schema(schema: dict[str, Any], spec: dict[str, Any] | None) -> dict return schema +def _string_enum_literal(schema: dict[str, Any]) -> str | None: + """`Literal[...]` for a string enum, so the tool schema lists the allowed + values and FastMCP rejects anything else before the API is called (e.g. + `platform="linkedin"` on a Google-only operation). None when the schema + has no enum or it holds non-string values.""" + values = [v for v in schema.get("enum") or [] if v is not None] + if not values or not all(isinstance(v, str) for v in values): + return None + return f"Literal[{', '.join(repr(v) for v in values)}]" + + def get_python_type( schema: dict[str, Any], required: bool = True, @@ -199,14 +210,15 @@ def get_python_type( # expected to always provide a value. The `| None` widening only # applies when we're emitting a `= None` default. if schema_type == "string": + base = _string_enum_literal(schema) or "str" if default is not None: - type_str = "str" + type_str = base default_str = f'"{default}"' elif required: - type_str = "str" + type_str = base default_str = "" # placeholder; not rendered for required else: - type_str = "str | None" + type_str = f"{base} | None" default_str = "None" elif schema_type == "integer": if default is not None: @@ -247,7 +259,7 @@ def get_python_type( items_schema = _resolve_schema(schema.get("items", {}) or {}, spec) items_type = items_schema.get("type") if items_type == "string": - inner = "str" + inner = _string_enum_literal(items_schema) or "str" elif items_type == "integer": inner = "int" elif items_type == "number": @@ -399,6 +411,47 @@ def add_param(entry: dict[str, Any]) -> None: return params +MAX_TOOL_DESCRIPTION_CHARS = 1000 + + +def _truncate_description(text: str) -> str: + """Cap the operation description so tool listings and search results stay + small; cuts at the last whitespace before the limit.""" + text = text.strip() + if len(text) <= MAX_TOOL_DESCRIPTION_CHARS: + return text + cut = text[:MAX_TOOL_DESCRIPTION_CHARS].rsplit(None, 1)[0] + return f"{cut} ..." + + +def _escape_docstring_text(text: str) -> str: + """Spec text lands inside a generated triple-quoted docstring: escape + backslashes and double quotes so it can neither close the string nor be + read as escape sequences.""" + return text.replace("\\", "\\\\").replace('"', '\\"') + + +def build_tool_description_lines( + summary: str, + description: str, + platforms: list[str], +) -> list[str]: + """Summary, then the operation description and its `x-platforms`. + + The MCP tool description is all an agent sees when picking a tool, so the + spec's operation description (e.g. "Google only; every other platform + returns 501") and supported platforms must reach it, not just the summary. + """ + lines = [summary.rstrip()] + if description.strip(): + lines.append("") + lines.extend(_escape_docstring_text(_truncate_description(description)).split("\n")) + if platforms: + lines.append("") + lines.append(f"Platforms: {', '.join(platforms)}") + return lines + + def generate_tool_handler( tool_name: str, resource: str, @@ -407,6 +460,8 @@ def generate_tool_handler( params: list[dict[str, Any]], read_only: bool, title: str, + description: str = "", + platforms: list[str] | None = None, ) -> str: """Generate a complete tool handler function.""" lines = [] @@ -425,7 +480,7 @@ def generate_tool_handler( sig = ", ".join(sig_params) # Docstring - strip trailing whitespace from all lines - doc_lines = [summary.rstrip()] + doc_lines = build_tool_description_lines(summary, description, platforms or []) if params: doc_lines.append("") doc_lines.append("Args:") @@ -532,6 +587,8 @@ def main() -> int: "resource": resource, "sdk_method": sdk_method, "summary": summary, + "description": operation.get("description", ""), + "platforms": operation.get("x-platforms", []), # HTTP method drives the tool annotation: GET is read-only, # everything else (POST/PUT/PATCH/DELETE) is treated as a # destructive write. Required by Anthropic's Connectors @@ -554,7 +611,7 @@ def main() -> int: "", "from __future__ import annotations", "", - "from typing import Any", + "from typing import Any, Literal", "", "from mcp.types import ToolAnnotations", "", @@ -645,6 +702,8 @@ def main() -> int: op["params"], op["read_only"], op["title"], + op["description"], + op["platforms"], ) # Indent for being inside register function handler_lines = handler.split("\n") diff --git a/src/late/mcp/generated_tools.py b/src/late/mcp/generated_tools.py index efc112b3..811fc3c9 100644 --- a/src/late/mcp/generated_tools.py +++ b/src/late/mcp/generated_tools.py @@ -6,7 +6,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, Literal from mcp.types import ToolAnnotations @@ -87,7 +87,12 @@ def register_generated_tools(mcp, _get_client): ) ) def account_groups_list_account_groups() -> str: - """List groups""" + """List groups + + Returns all account groups visible to the authenticated user. Groups can + contain accounts from multiple profiles. For API keys scoped to specific + profiles, only groups whose accounts all live in allowed profiles are + returned.""" client = _get_client() try: response = client.account_groups.list_account_groups() @@ -108,6 +113,10 @@ def account_groups_create_account_group( ) -> str: """Create group + Creates a new account group with a name and a list of account IDs. + Accounts can belong to different profiles; the caller must have access to + every account's profile. Group names must be unique per user. + Args: name: (required) account_ids: (required) @@ -135,6 +144,8 @@ def account_groups_update_account_group( ) -> str: """Update group + Updates the name or account list of an existing group. You can rename the group, change its accounts, or both. + Args: group_id: (required) name @@ -159,6 +170,8 @@ def account_groups_update_account_group( def account_groups_delete_account_group(group_id: str) -> str: """Delete group + Permanently deletes an account group. The accounts themselves are not affected. + Args: group_id: (required)""" client = _get_client() @@ -181,6 +194,8 @@ def account_groups_delete_account_group(group_id: str) -> str: def account_settings_get_messenger_menu(account_id: str) -> str: """Get FB persistent menu + Get the persistent menu configuration for a Facebook Messenger account. + Args: account_id: (required)""" client = _get_client() @@ -203,6 +218,8 @@ def account_settings_set_messenger_menu( ) -> str: """Set FB persistent menu + Set the persistent menu for a Facebook Messenger account. Max 3 top-level items, max 5 nested items. Meta only shows a persistent menu on a page that has a Get Started button, so set one first with PUT /v1/accounts/{accountId}/messenger-get-started. A postback button whose payload is `zernio:workflow:` starts that workflow when tapped; the workflow must be active on this account and profile. + Args: account_id: (required) persistent_menu: Persistent menu configuration array (Meta format) (required)""" @@ -226,6 +243,8 @@ def account_settings_set_messenger_menu( def account_settings_delete_messenger_menu(account_id: str) -> str: """Delete FB persistent menu + Removes the persistent menu from Facebook Messenger conversations for this account. + Args: account_id: (required)""" client = _get_client() @@ -248,6 +267,8 @@ def account_settings_delete_messenger_menu(account_id: str) -> str: def account_settings_get_messenger_get_started(account_id: str) -> str: """Get FB Get Started button + Get the Get Started button payload for a Facebook Messenger account. `data` is null when the page has none. + Args: account_id: (required)""" client = _get_client() @@ -272,6 +293,8 @@ def account_settings_set_messenger_get_started( ) -> str: """Set FB Get Started button + Set the Get Started button shown on a Facebook page's Messenger welcome screen. Meta requires it before a persistent menu can be set. Tapping it sends a postback with `payload`, which arrives as a `message.received` webhook carrying it in `metadata.postbackPayload`. Use `zernio:workflow:` to start a workflow on the tap; the workflow must be active on this account and profile. + Args: account_id: (required) payload: Postback payload sent when a person taps Get Started, e.g. `GET_STARTED` or `zernio:workflow:`. (required)""" @@ -295,6 +318,8 @@ def account_settings_set_messenger_get_started( def account_settings_delete_messenger_get_started(account_id: str) -> str: """Delete FB Get Started button + Remove the Get Started button. Meta refuses while a persistent menu is set, so delete the menu first. + Args: account_id: (required)""" client = _get_client() @@ -317,6 +342,8 @@ def account_settings_delete_messenger_get_started(account_id: str) -> str: def account_settings_get_instagram_ice_breakers(account_id: str) -> str: """Get IG ice breakers + Get the ice breaker configuration for an Instagram account. + Args: account_id: (required)""" client = _get_client() @@ -341,6 +368,8 @@ def account_settings_set_instagram_ice_breakers( ) -> str: """Set IG ice breakers + Set ice breakers for an Instagram account. Max 4 ice breakers, question max 80 chars. + Args: account_id: (required) ice_breakers: (required)""" @@ -364,6 +393,8 @@ def account_settings_set_instagram_ice_breakers( def account_settings_delete_instagram_ice_breakers(account_id: str) -> str: """Delete IG ice breakers + Removes the ice breaker questions from an Instagram account's Messenger experience. + Args: account_id: (required)""" client = _get_client() @@ -386,6 +417,8 @@ def account_settings_delete_instagram_ice_breakers(account_id: str) -> str: def account_settings_get_telegram_commands(account_id: str) -> str: """Get TG bot commands + Get the bot commands configuration for a Telegram account. + Args: account_id: (required)""" client = _get_client() @@ -410,6 +443,8 @@ def account_settings_set_telegram_commands( ) -> str: """Set TG bot commands + Set bot commands for a Telegram account. + Args: account_id: (required) commands: (required)""" @@ -433,6 +468,8 @@ def account_settings_set_telegram_commands( def account_settings_delete_telegram_commands(account_id: str) -> str: """Delete TG bot commands + Clears all bot commands configured for a Telegram bot account. + Args: account_id: (required)""" client = _get_client() @@ -457,11 +494,12 @@ def account_settings_delete_telegram_commands(account_id: str) -> str: def accounts_list_accounts( profile_id: str | None = None, platform: str | None = None, - status: str | None = None, + status: Literal["connected", "disconnected"] | None = None, search: str | None = None, - category: str | None = None, - sort: str | None = None, - order: str = "asc", + category: Literal["social", "ads", "communication", "blogs"] | None = None, + sort: Literal["account", "platform", "profile", "status", "connected"] + | None = None, + order: Literal["asc", "desc"] = "asc", include_over_limit: bool = False, page: int | None = None, limit: int | None = None, @@ -470,6 +508,10 @@ def accounts_list_accounts( ) -> str: """List accounts + Returns connected accounts. Only includes accounts within the plan limit by default. Follower data requires analytics add-on. + Supports optional server-side pagination via page/limit params. When omitted, returns all accounts (backward-compatible). + page and limit must be supplied together; out-of-range page/limit values are rejected with 400 rather than silently clamped. + Args: profile_id: Filter accounts by profile ID. Must be a valid ObjectId. platform: Filter accounts by platform (e.g. "instagram", "twitter"). @@ -516,10 +558,13 @@ def accounts_get_follower_stats( profile_id: str | None = None, from_date: str | None = None, to_date: str | None = None, - granularity: str = "daily", + granularity: Literal["daily", "weekly", "monthly"] = "daily", ) -> str: """Get follower stats + Returns follower count history and growth metrics for connected accounts. + Requires analytics add-on subscription. Follower counts are refreshed once per day. + Args: account_ids: Comma-separated list of account IDs (optional, defaults to all user's accounts) profile_id: Filter by profile ID @@ -555,6 +600,15 @@ def accounts_update_account( ) -> str: """Update account + Updates a connected account's display name or username override. + + For X accounts on usage-based billing, also accepts an `xCapabilities` + object to toggle background API operations that incur X API pass-through costs. + Both fields are opt-in (default `false`). When off, no analytics syncs or DM + polling are performed for that account, and no API call is metered for those + operations. Publishing and deleting posts are always available regardless of + these toggles. Setting `xCapabilities` on a non-X account returns 400. + Args: account_id: (required) username @@ -586,6 +640,13 @@ def accounts_update_account( def accounts_move_account_to_profile(account_id: str, profile_id: str) -> str: """Move account to another profile + Moves a connected account to a different profile owned by the same + user. The target profile must belong to the same user as the account. + + For API keys restricted to specific profiles, BOTH the source account's + current profile AND the target profile must be in the key's allowed set. + Calls with a target profile outside the key's scope return 403. + Args: account_id: (required) profile_id: Target profile ID (must be a valid ObjectId and owned by the same user as the account). (required)""" @@ -609,6 +670,8 @@ def accounts_move_account_to_profile(account_id: str, profile_id: str) -> str: def accounts_delete_account(account_id: str) -> str: """Disconnect account + Disconnects and removes a connected account. Repeating the call for an account already disconnected returns 404, the account stays in its 1h grace window and the disconnect is not re-run. + Args: account_id: (required)""" client = _get_client() @@ -628,11 +691,40 @@ def accounts_delete_account(account_id: str) -> str: ) def accounts_get_all_accounts_health( profile_id: str | None = None, - platform: str | None = None, - status: str | None = None, + platform: Literal[ + "facebook", + "instagram", + "linkedin", + "twitter", + "tiktok", + "youtube", + "threads", + "pinterest", + "reddit", + "bluesky", + "googlebusiness", + "telegram", + "snapchat", + "discord", + "slack", + "whatsapp", + "shopify", + "wordpress", + "linkedinads", + "metaads", + "pinterestads", + "tiktokads", + "xads", + "googleads", + "openaiads", + ] + | None = None, + status: Literal["healthy", "warning", "error"] | None = None, ) -> str: """Check accounts health + Returns health status of all connected accounts including token validity, permissions, and issues needing attention. + Args: profile_id: Filter by profile ID platform: Filter by platform @@ -657,6 +749,17 @@ def accounts_get_all_accounts_health( def accounts_get_account_health(account_id: str) -> str: """Check account health + Returns detailed health info for a specific account including token status, permissions, and recommendations. + + For WhatsApp accounts the response also includes `platformConnection`, a live probe of the + Meta link behind the channel (the same read as `GET /v1/whatsapp/number-info`). The OAuth + token can be perfectly valid while Meta refuses to serve the phone-number object (for + example after a phone-side coexistence disconnect), so `tokenStatus` alone is not a + liveness signal for WhatsApp. When the Meta link is dead, `platformConnection.status` is + `disconnected` and the overall `status` is `error`. When Meta reports that the number's + inbound message webhook does not reach Zernio, `platformConnection.inboundWebhookSubscribed` + is `false`, an entry is added to `issues`, and the overall `status` is at least `warning`. + Args: account_id: The account ID to check (required)""" client = _get_client() @@ -677,6 +780,25 @@ def accounts_get_account_health(account_id: str) -> str: def accounts_get_account_posts(account_id: str) -> str: """List posts published on the platform + Returns the 25 most recent posts that exist on the platform for a connected account, read + live from the platform API. This covers everything on the account, including posts that + were never created through Zernio. + + Use it to obtain the platform's own post id, which the analytics endpoints take as input. + On YouTube the returned `id` is the video ID that `GET /v1/analytics/youtube/daily-views`, + `/video-retention` and `/demographics` expect as `videoId`, so this endpoint is what backs + a video picker in your own UI. + + Not every field applies to every platform: `reactionCount` is Facebook and LinkedIn, + `shareCount` is platform dependent, `cid` is the Bluesky content id needed to reply, and + `subreddit` is Reddit only. Absent fields are omitted from the response. + + The account's token is refreshed before the call when it has expired. When the refresh + cannot recover it, the response is a 401 with code `TOKEN_EXPIRED` and the account has to + be reconnected. + + Platforms: facebook, instagram, twitter, bluesky, threads, youtube, linkedin, reddit, tiktok, pinterest + Args: account_id: (required)""" client = _get_client() @@ -699,6 +821,28 @@ def accounts_get_instagram_follow_status( ) -> str: """Check whether an Instagram user follows the account + Resolves the follow relationship between an Instagram user and the connected + account, plus their public profile counters. + + `userId` is the Instagram-scoped id (IGSID) Meta gives you on a webhook: + `sender.id` on `message.received`, `comment.author.id` on `comment.received`. + + **Meta only answers for people who have MESSAGED the account.** Commenting grants + no consent, so a commenter who has never DMed you is unresolvable - that is a + platform rule, not a limitation of this endpoint. When it cannot be resolved the + response is still `200` with `isFollower: null` and an `unavailableReason`, because + \"unknown\" is a normal state to branch on: + + * `consent_required` - the user has never messaged this account. + * `dm_access_disabled` - the account owner turned off Instagram Direct API access. + * `not_messageable` - the id is not a messaging-scoped id. + * `error` - a transient Graph API failure. + + To gate a comment automation on this, use the automation's `audience` rules instead + of calling ... + + Platforms: instagram + Args: account_id: Instagram account ID (required) user_id: Instagram-scoped user id (IGSID) from a webhook payload (required) @@ -725,6 +869,8 @@ def accounts_list_tik_tok_commercial_music( ) -> str: """List trending commercial music + Returns the 100 currently trending tracks of TikTok's Commercial Music Library for a TikTok account connected through the TikTok for Business app. Use a track id as tiktokSettings.musicSoundInfo.musicSoundId when creating a post. The list is not paged; countryCode selects the country chart. + Args: account_id: The TikTok account ID (required) country_code: Two-letter ISO 3166-1 country code of the chart to read (for example ES). Defaults to TikTok's global chart.""" @@ -748,6 +894,8 @@ def accounts_list_tik_tok_commercial_music( def accounts_search_tik_tok_locations(account_id: str, query: str) -> str: """Search TikTok location tags + Searches the location tags a TikTok account connected through the TikTok for Business app can attach to a video post. Send a result's id and name as tiktokSettings.locationId and locationName when creating a post. TikTok answers the 20 closest matches and fills the list with fuzzy matches when nothing matches, so an unrelated result does not mean the place is missing. + Args: account_id: The TikTok account ID (required) query: Place name to search, for example a city, a venue or an address (required)""" @@ -769,10 +917,12 @@ def accounts_search_tik_tok_locations(account_id: str, query: str) -> str: ) ) def accounts_get_tik_tok_creator_info( - account_id: str, media_type: str = "video" + account_id: str, media_type: Literal["video", "photo"] = "video" ) -> str: """Get TikTok creator info + Returns TikTok creator details, available privacy levels, posting limits, and commercial content options for a specific TikTok account. Only works with TikTok accounts. + Args: account_id: The TikTok account ID (required) media_type: The media type to get creator info for (affects available interaction settings)""" @@ -801,6 +951,8 @@ def accounts_get_google_business_reviews( ) -> str: """Get reviews + Returns reviews for a Google Business Profile account including ratings, comments, and owner replies. Use nextPageToken for pagination. + Args: account_id: The Zernio account ID (from /v1/accounts) (required) location_id: Override which location to query. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs. @@ -831,6 +983,8 @@ def accounts_get_google_business_food_menus( ) -> str: """Get food menus + Returns food menus for a Google Business Profile location including sections, items, pricing, and dietary info. Only for locations with food menu support. + Args: account_id: The Zernio account ID (from /v1/accounts) (required) location_id: Override which location to query. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs.""" @@ -859,6 +1013,8 @@ def accounts_update_google_business_food_menus( ) -> str: """Update food menus + Updates food menus for a Google Business Profile location. Send the full menus array. Use updateMask for partial updates. + Args: account_id: The Zernio account ID (from /v1/accounts) (required) location_id: Override which location to target. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs. @@ -889,6 +1045,8 @@ def accounts_get_google_business_location_details( ) -> str: """Get location details + Returns detailed Google Business Profile location info (hours, description, phone, website, categories, services). Use readMask to request specific fields. + Args: account_id: The Zernio account ID (from /v1/accounts) (required) location_id: Override which location to query. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs. @@ -935,6 +1093,10 @@ def accounts_update_google_business_location_details( ) -> str: """Update location details + Updates Google Business Profile location details. The updateMask field is required and specifies which fields to update. + This endpoint proxies Google's Business Information API locations.patch, so any valid updateMask field is supported. + Common fields: regularHours, specialHours, profile.description, websiteUri, phoneNumbers, categories, serviceItems. + Args: account_id: The Zernio account ID (from /v1/accounts) (required) location_id: Override which location to target. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs. @@ -998,6 +1160,9 @@ def accounts_list_google_business_media( ) -> str: """List media + Lists media items (photos) for a Google Business Profile location. + Returns photo URLs, descriptions, categories, and metadata. + Args: account_id: (required) location_id: Override which location to query. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs. @@ -1027,12 +1192,32 @@ def accounts_create_google_business_media( account_id: str, source_url: str, location_id: str | None = None, - media_format: str = "PHOTO", + media_format: Literal["PHOTO", "VIDEO"] = "PHOTO", description: str | None = None, - category: str | None = None, + category: Literal[ + "CATEGORY_UNSPECIFIED", + "COVER", + "PROFILE", + "LOGO", + "EXTERIOR", + "INTERIOR", + "PRODUCT", + "FOOD_AND_DRINK", + "MENU", + "COMMON_AREA", + "ROOMS", + "TEAMS", + "AT_WORK", + "ADDITIONAL", + ] + | None = None, ) -> str: """Upload photo + Creates a media item (photo) for a location from a publicly accessible URL. + + Categories determine where the photo appears: CATEGORY_UNSPECIFIED, COVER, PROFILE, LOGO, EXTERIOR, INTERIOR, PRODUCT, FOOD_AND_DRINK, MENU, COMMON_AREA, ROOMS, TEAMS, AT_WORK, ADDITIONAL. + Args: account_id: (required) location_id: Override which location to target. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs. @@ -1067,6 +1252,8 @@ def accounts_delete_google_business_media( ) -> str: """Delete photo + Deletes a photo or media item from a Google Business Profile location. + Args: account_id: (required) location_id: Override which location to target. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs. @@ -1099,6 +1286,21 @@ def accounts_get_gmb_attribute_metadata( ) -> str: """Get attribute metadata + Returns metadata about which Google Business Profile attributes are available for + a location or business category. Use this endpoint to discover valid attribute names, + value types, and allowed enum values before reading or writing via gmb-attributes. + + Two mutually exclusive query modes: + + **Location mode**: pass `locationId` (or rely on the account's stored `selectedLocationId`). + Google returns attributes valid for that specific location. + + **Category mode**: pass `categoryName` (must start with `categories/`) and `regionCode`. + Google returns attributes valid for that category across the given region. + `languageCode` is optional in category mode. + + Both modes support `pageSize` and `pageToken` for pagination. + Args: account_id: (required) location_id: Google Business Profile location ID (e.g. "6257659026299438786"). If omitted, uses the account's stored selectedLocationId. Mutually exclusive with categoryName. @@ -1135,6 +1337,8 @@ def accounts_get_google_business_attributes( ) -> str: """Get attributes + Returns Google Business Profile location attributes (amenities, services, accessibility, payment types). Available attributes vary by business category. + Args: account_id: (required) location_id: Override which location to query. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs.""" @@ -1163,6 +1367,10 @@ def accounts_update_google_business_attributes( ) -> str: """Update attributes + Updates location attributes (amenities, services, etc.). + + The attributeMask specifies which attributes to update (comma-separated). + Args: account_id: (required) location_id: Override which location to target. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs. @@ -1196,6 +1404,10 @@ def accounts_list_google_business_place_actions( ) -> str: """List action links + Lists place action links for a Google Business Profile location. + + Place actions are the booking, ordering, and reservation buttons that appear on your listing. + Args: account_id: (required) location_id: Override which location to query. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs. @@ -1224,11 +1436,23 @@ def accounts_list_google_business_place_actions( def accounts_create_google_business_place_action( account_id: str, uri: str, - place_action_type: str, + place_action_type: Literal[ + "APPOINTMENT", + "ONLINE_APPOINTMENT", + "DINING_RESERVATION", + "FOOD_ORDERING", + "FOOD_DELIVERY", + "FOOD_TAKEOUT", + "SHOP_ONLINE", + ], location_id: str | None = None, ) -> str: """Create action link + Creates a place action link for a location. + + Available action types: APPOINTMENT, ONLINE_APPOINTMENT, DINING_RESERVATION, FOOD_ORDERING, FOOD_DELIVERY, FOOD_TAKEOUT, SHOP_ONLINE. + Args: account_id: (required) location_id: Override which location to target. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs. @@ -1259,6 +1483,8 @@ def accounts_delete_google_business_place_action( ) -> str: """Delete action link + Deletes a place action link (e.g. booking or ordering URL) from a Google Business Profile location. + Args: account_id: (required) location_id: Override which location to target. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs. @@ -1285,10 +1511,22 @@ def accounts_update_google_business_place_action( name: str, location_id: str | None = None, uri: str | None = None, - place_action_type: str | None = None, + place_action_type: Literal[ + "APPOINTMENT", + "ONLINE_APPOINTMENT", + "DINING_RESERVATION", + "FOOD_ORDERING", + "FOOD_DELIVERY", + "FOOD_TAKEOUT", + "SHOP_ONLINE", + ] + | None = None, ) -> str: """Update action link + Updates a place action link (change URL or action type). + Only the fields included in the request body will be updated. + Args: account_id: (required) location_id: Override which location to target. If omitted, uses the account's selected location. @@ -1321,10 +1559,23 @@ def accounts_batch_get_google_business_reviews( location_names: list[str] | None, page_size: int = 50, page_token: str | None = None, - order_by: str = "updateTime desc", + order_by: Literal[ + "updateTime desc", "rating", "rating desc" + ] = "updateTime desc", ) -> str: """Batch get reviews + Fetches reviews across multiple locations in a single request. + More efficient than calling GET /gmb-reviews per location for multi-location businesses. + Returns a flat locationReviews array (not grouped by location): each item carries + the location resource name it belongs to (`name`) plus the review object (`review`), + whose identity is `review.reviewId`. + Reviews are requested from Google ordered by `orderBy` (default `updateTime desc`, + newest first), so callers polling for recent reviews can stop paginating once they + cross their date window. + Note: this endpoint does not return aggregate metrics (averageRating / totalReviewCount). + For those, use the single-location GET /gmb-reviews endpoint. + Args: account_id: (required) location_names: Array of full location resource names (e.g. ['accounts/123/locations/456']). Max 50 per request (Google's batchGetReviews cap); chunk larger sets into multiple requests. (required) @@ -1357,6 +1608,10 @@ def accounts_get_google_business_review( ) -> str: """Get a review + Returns one Google Business Profile review, in the same shape as the entries of GET /v1/accounts/{accountId}/gmb-reviews. + The review is read from the account's selected location unless locationId overrides it, and Google returns 404 for a review id that belongs to another location. + Read the review before replying if a human may have answered it already: replies are overwritten in place and Google keeps no history. + Args: account_id: The Zernio account ID (from /v1/accounts) (required) review_id: The review ID portion (e.g. "AIe9_BGx1234567890"), not the full resource name (required) @@ -1383,6 +1638,12 @@ def accounts_reply_to_google_business_review( ) -> str: """Reply to a review + Posts (or updates) the business owner reply to a Google Business Profile review. + The reply is associated with the account's currently selected location (set via /v1/accounts/{accountId}/gmb-locations). + Calling this endpoint a second time on the same review overwrites the previous reply (PUT semantics on Google's side). + Google keeps no history, so an automated retry silently replaces a reply someone edited by hand in the Google Business Profile UI. + Read the review before retrying if a human may have answered it. + Args: account_id: The Zernio account ID (from /v1/accounts) (required) review_id: The review ID portion (e.g. "AIe9_BGx1234567890"), not the full resource name (required) @@ -1409,6 +1670,8 @@ def accounts_delete_google_business_review_reply( ) -> str: """Delete a review reply + Removes the business owner reply from a Google Business Profile review. The review itself remains. + Args: account_id: The Zernio account ID (from /v1/accounts) (required) review_id: The review ID portion (e.g. "AIe9_BGx1234567890"), not the full resource name (required)""" @@ -1432,6 +1695,13 @@ def accounts_delete_google_business_review_reply( def accounts_list_business_partners(account_id: str) -> str: """List partner businesses of the Page + The business portfolios (Meta Business Managers) that may act on the Facebook Page + behind this account, plus the Page's owning portfolio and linked Instagram + professional account. Works on Facebook accounts and on Instagram accounts connected + through Facebook Login. + + Platforms: facebook, instagram + Args: account_id: Zernio SocialAccount id of the Facebook or Instagram account. (required)""" client = _get_client() @@ -1450,10 +1720,64 @@ def accounts_list_business_partners(account_id: str) -> str: ) ) def accounts_grant_business_partner( - account_id: str, business_id: str, permitted_tasks: list[str] | None = None + account_id: str, + business_id: str, + permitted_tasks: list[ + Literal[ + "MANAGE", + "CREATE_CONTENT", + "MODERATE", + "MESSAGING", + "ADVERTISE", + "ANALYZE", + "MODERATE_COMMUNITY", + "MANAGE_JOBS", + "PAGES_MESSAGING", + "PAGES_MESSAGING_SUBSCRIPTIONS", + "READ_PAGE_MAILBOXES", + "VIEW_MONETIZATION_INSIGHTS", + "MANAGE_LEADS", + "CASHIER_ROLE", + "GLOBAL_STRUCTURE_MANAGEMENT", + "PROFILE_PLUS_FULL_CONTROL", + "PROFILE_PLUS_MANAGE", + "PROFILE_PLUS_FACEBOOK_ACCESS", + "PROFILE_PLUS_CREATE_CONTENT", + "PROFILE_PLUS_MODERATE", + "PROFILE_PLUS_MODERATE_DELEGATE_COMMUNITY", + "PROFILE_PLUS_MESSAGING", + "PROFILE_PLUS_ADVERTISE", + "PROFILE_PLUS_ANALYZE", + "PROFILE_PLUS_REVENUE", + "PROFILE_PLUS_MANAGE_LEADS", + "PROFILE_PLUS_CREATIVE_MANAGEMENT", + "PROFILE_PLUS_CREATOR_MANAGEMENT", + "PROFILE_PLUS_GLOBAL_STRUCTURE_MANAGEMENT", + ] + ] + | None = None, ) -> str: """Share the Page with a partner business + Grants a partner business portfolio tasks on the Facebook Page behind this account. + With `ADVERTISE`, the partner can run ads for the Page from ad accounts in its own + portfolio, which is how an integrator advertises for an end user without touching + the end user's ad accounts. + + Meta only lets a user token share a Page that a business portfolio owns. A Page + outside any portfolio must first be claimed into one at business.facebook.com; this + endpoint answers `422` until that is done. Meta refuses a second grant to a portfolio + that already has access instead of replacing its tasks, so that case answers `200` + with `alreadyShared: true` and the tasks the partner currently holds. To change a + partner's tasks, revoke and grant again. + + After the grant, the partner assigns its own people to the Page with + `POST /v1/ads/page-users`; Meta does not assign partner admins automatically. + The Instagram professional account linked to the Page is returned in `page` so the + partner can reference it; Meta ... + + Platforms: facebook, instagram + Args: account_id: Zernio SocialAccount id of the Facebook or Instagram account. (required) business_id: Meta business portfolio id of the partner (numeric string). (required) @@ -1480,6 +1804,10 @@ def accounts_grant_business_partner( def accounts_revoke_business_partner(account_id: str, business_id: str) -> str: """Revoke a partner business from the Page + Removes every task the partner business portfolio held on the Page. + + Platforms: facebook, instagram + Args: account_id: Zernio SocialAccount id of the Facebook or Instagram account. (required) business_id: Meta business portfolio id of the partner (numeric string). (required)""" @@ -1505,6 +1833,23 @@ def accounts_get_linked_in_mentions( ) -> str: """Resolve LinkedIn mention + Converts a LinkedIn profile or company URL to a URN for @mentions in posts. + + How to use LinkedIn @mentions (2-step workflow): + + 1. Call this endpoint with the LinkedIn profile/company URL to get the mention URN and format. + 2. Embed the returned mentionFormat (e.g. @[Vincent Jong](urn:li:person:xxx)) directly in your post's content field. + + Example: + - Resolve: GET /v1/accounts/{id}/linkedin-mentions?url=linkedin.com/in/vincentjong&displayName=Vincent Jong + - Returns: mentionFormat: \"@[Vincent Jong](urn:li:person:xxx)\" + - Use in post content: \"Great talk with @[Vincent Jong](urn:li:person:xxx) today!\" + + Important: The mentions array field in POST /v1/posts is stored for reference only and does NOT trigger @mentions on LinkedIn. You must embed the mention format directly in the content text. + + Requirements: + - Person mentions require the LinkedIn account to be admin of at least one organization: both endpoints that resolve a profile URL to a member URN take an organization you administer. ... + Args: account_id: The LinkedIn account ID (required) url: LinkedIn profile URL, company URL, or vanity name. (required) @@ -1529,6 +1874,8 @@ def accounts_get_linked_in_mentions( def accounts_get_slack_settings(account_id: str) -> str: """Get Slack account settings + Returns the connected Slack channel details and the default message identity (name and avatar shown as the author on every post, with Slack's APP badge). The identity applies to messages only; the app's own Slack profile is global and cannot be changed per workspace. + Args: account_id: (required)""" client = _get_client() @@ -1553,6 +1900,8 @@ def accounts_update_slack_settings( ) -> str: """Update Slack account settings + Set or clear the default message identity for this channel. Empty string clears a field; per-post platformSpecificData.username/iconUrl still override these defaults. + Args: account_id: (required) default_username: Author name shown on posts. Empty string clears it. @@ -1579,6 +1928,8 @@ def accounts_update_slack_settings( def accounts_get_bluesky_settings(account_id: str) -> str: """Get Bluesky account settings + Returns the account's default post languages (defaultLangs), applied at publish time whenever a post's platformSpecificData.langs is absent. Null when no default is set. + Args: account_id: (required)""" client = _get_client() @@ -1599,6 +1950,8 @@ def accounts_get_bluesky_settings(account_id: str) -> str: def accounts_update_bluesky_settings(account_id: str, default_langs: str) -> str: """Update Bluesky account settings + Set or clear the account's default post languages. 1-3 BCP-47 codes (e.g. \"pt\", \"en-US\"), the same validation as per-post langs; explicit null clears the default. Per-post platformSpecificData.langs always overrides this default. Applies to posts published after the change; already-published posts cannot be retagged (Bluesky has no post edit). + Args: account_id: (required) default_langs: (required)""" @@ -1623,7 +1976,7 @@ def accounts_update_bluesky_settings(account_id: str, default_langs: str) -> str ) def ad_accounts_get_ad_comments( ad_id: str, - placement: str | None = None, + placement: Literal["facebook", "instagram"] | None = None, limit: int = 25, since: str | None = None, until: str | None = None, @@ -1631,6 +1984,24 @@ def ad_accounts_get_ad_comments( ) -> str: """List comments on an ad + Returns comments on an ad's underlying creative post. Useful for moderating or analyzing + engagement on dark posts (ad creatives that never went live organically), which the + regular GET /v1/inbox/comments/{postId} endpoint cannot serve because dark posts are + not in Zernio's post database. + + An ad that runs on both Facebook feed and Instagram feed has two separate underlying + posts with separate comment threads (the creative's effective_object_story_id and + effective_instagram_media_id). Use the `placement` query param to pick one; with no + param the Instagram side is returned when it exists, otherwise Facebook. The + identifiers are read from the ad record (persisted during sync) with a Marketing-API + fallback for ads that predate the field. + + For Instagram-placed comments, the Instagram account that runs the ad must be connected + to Zernio, because those comments are read through that account's token. If no connected + Instagram account on the profile can read the ad's media, the call returns ... + + Platforms: meta, tiktok + Args: ad_id: Internal Zernio ad ID or indexed platform ad/post ID. (required) placement: Which side of the ad to return comments for. Omit to default to the Instagram side when present, else Facebook. Returns ad_not_commentable if the ad has no such placement. @@ -1669,6 +2040,22 @@ def ad_accounts_reply_to_ad_comment( ) -> str: """Reply to an ad comment + Reply to a first-level TikTok ad comment. Requires a TT_USER or CUSTOMIZED_USER identity with comment-management permission. Replies to replies are rejected. The response commentId identifies the new reply. This operation is not idempotent; do not blindly retry an uncertain response. + + Unknown identity and video item fields are resolved only when needed for this + action, then persisted for reuse. Comment-specific fields take precedence. + If TikTok no longer returns the ad needed to resolve identity, 404 ad_not_found + directs you to check deletion or archival in TikTok Ads Manager. Listing can + still succeed. Unsupported or unavailable identity returns 403 feature_not_available. + Denied access to ad details returns 403 insufficient_permissions with reconnect + guidance and the upstream platformError. + + Requires Ads access. The ad is resolved within the caller's accessible profiles. + Before moderation, Zernio verifies that the comment belongs to this ad using + TikTok's ad-group comment listing. ... + + Platforms: tiktok + Args: ad_id: Internal Zernio ad ID or indexed platform ad ID. (required) comment_id: TikTok comment ID from the ad comment listing. (required) @@ -1701,6 +2088,18 @@ def ad_accounts_hide_ad_comment( ) -> str: """Hide or unhide an ad comment + Hide or restore a TikTok ad comment. Send hidden=true to hide it or hidden=false to make it public again. Identity and video item ID are not required; no identity lookup is performed. + + Requires Ads access. The ad is resolved within the caller's accessible profiles. + Before moderation, Zernio verifies that the comment belongs to this ad using + TikTok's ad-group comment listing. The default search window is the last 30 days. + Use since/until for older comments, with at most 30 days between the dates. + Lookups scan at most 2,000 ad-group comments; narrow the date window if exceeded. + Meta returns 501 feature_not_available with guidance to use the existing inbox + comment endpoints and the account/post IDs from GET /v1/ads/{adId}/comments. + + Platforms: tiktok + Args: ad_id: Internal Zernio ad ID or indexed platform ad ID. (required) comment_id: TikTok comment ID from the ad comment listing. (required) @@ -1733,6 +2132,24 @@ def ad_accounts_delete_ad_comment( ) -> str: """Delete an ad comment + Delete your own TikTok ad comment or reply. TikTok must return can_delete=true for the comment. Other users' comments can be hidden instead. + + Unknown identity and video item fields are resolved only when needed for this + action, then persisted for reuse. Comment-specific fields take precedence. + If TikTok no longer returns the ad needed to resolve identity, 404 ad_not_found + directs you to check deletion or archival in TikTok Ads Manager. Listing can + still succeed. Unsupported or unavailable identity returns 403 feature_not_available. + Denied access to ad details returns 403 insufficient_permissions with reconnect + guidance and the upstream platformError. + + Requires Ads access. The ad is resolved within the caller's accessible profiles. + Before moderation, Zernio verifies that the comment belongs to this ad using + TikTok's ad-group comment listing. The default search window is the last 30 days. + Use since/until for older comments, with at most 30 days between the dates. + Lookups scan at most ... + + Platforms: tiktok + Args: ad_id: Internal Zernio ad ID or indexed platform ad ID. (required) comment_id: TikTok comment ID from the ad comment listing. (required) @@ -1758,6 +2175,14 @@ def ad_accounts_delete_ad_comment( def ad_accounts_list_ads_business_centers(account_id: str) -> str: """List TikTok Business Centers + Returns the TikTok Business Centers (BCs) the connected `tiktokads` account can read. + Each BC reports its advertiser count so callers can build agency-style pickers + without re-walking `/v1/ads/accounts` per BC. + + TikTok-only. Solo advertisers (non-agency tokens) return an empty array. + + Platforms: tiktok + Args: account_id: ID of the `tiktokads` (or parent `tiktok` posting) SocialAccount (required)""" client = _get_client() @@ -1788,6 +2213,14 @@ def ad_accounts_get_ads_activity_log( ) -> str: """Ad account change / audit log + Account-level audit log from Meta's `/act_X/activities`: who changed what and when + (creates, edits, status flips, budget changes...) with Meta's translated event names and + the structured before/after in `extra_data`. Rows are returned verbatim. Meta has no + server-side per-object filter on this edge, so `objectId` filters the returned page + client-side (combine with paging to walk history for one campaign/ad set/ad). + + Platforms: meta + Args: account_id: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token. (required) ad_account_id: Meta ad account id (act_). (required) @@ -1828,6 +2261,12 @@ def ad_accounts_list_ad_studies( ) -> str: """A/B tests and lift studies + Lists the ad account's A/B tests and lift studies (Meta's `/act_X/ad_studies`), rows + returned verbatim. The default projection covers id, name, type, timing and cells with + split percentages; `fields` is a raw-passthrough override. + + Platforms: meta + Args: account_id: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token. (required) ad_account_id: Meta ad account id (act_). (required) @@ -1860,6 +2299,10 @@ def ad_accounts_list_ads_instagram_accounts( ) -> str: """List Instagram ad identities + Discovers identities through connected_instagram_accounts, Page linkage and Page-backed identities, with a best-effort business fallback. Business permission errors do not fail discovery. The resolved object uses the same profile-scoped resolver as ad creation; null means no identity was resolved. Format-specific observed-actor fallbacks at creative creation are not predicted. + + Platforms: meta + Args: account_id: Zernio Meta Ads or Facebook SocialAccount ID. (required) ad_account_id: Meta ad account ID including the act_ prefix. (required)""" @@ -1889,6 +2332,10 @@ def ad_accounts_list_ads_instagram_posts( ) -> str: """List Instagram posts to boost + Lists the media of the Instagram account this Meta connection can reach, so an existing Instagram post can be boosted without connecting the Instagram account separately. Each `posts[].id` is the existing-post id to send as `platformPostId` when creating the ad; Meta turns it into `source_instagram_media_id` on the creative. Identity resolution reuses the same resolver as `/v1/ads/instagram-accounts`. `igUserId` is always checked against the identities the connection can reach and is never trusted as sent. When no identity is reachable the endpoint fails instead of returning an empty list, and the two causes stay apart: `403 reconnect_required` means the connection predates Instagram access (Meta then omits `instagram_business_account` from the Page read rather than erroring, so it looks identical to having no data) and the account must be reconnected granting Instagram access, while `422 instagram_business_account_unresolved` means the Page genuinely has no Instagram professional ... + + Platforms: meta + Args: account_id: Zernio Meta Ads, Facebook or Instagram SocialAccount ID. (required) ad_account_id: Meta ad account ID including the act_ prefix. Narrows identity resolution to the Instagram accounts reachable from this ad account. @@ -1921,6 +2368,10 @@ def ad_accounts_list_advertisable_applications( ) -> str: """List advertisable apps + Lists applications available to a Meta ad account, their supported platforms and unmodified object store URLs. A listed app still needs a configured mobile platform and store URL to run install promotion. + + Platforms: meta + Args: account_id: Zernio Meta Ads or Facebook SocialAccount ID. (required) ad_account_id: Meta ad account ID including the act_ prefix. (required)""" @@ -1946,6 +2397,10 @@ def ad_accounts_get_ios_fourteen_campaign_limits( ) -> str: """Get iOS 14 campaign limits + Reads Meta iOS 14 campaign limits for an application on an ad account. applicationId is sent as Meta app_id. This read does not establish that the application is configured for iOS promotion. + + Platforms: meta + Args: account_id: Zernio Meta Ads or Facebook SocialAccount ID. (required) ad_account_id: Meta ad account ID including the act_ prefix. (required) @@ -1974,6 +2429,13 @@ def ad_accounts_list_meta_businesses( ) -> str: """Businesses list + Business Manager portfolios the connected Meta user belongs to (Meta's `/me/businesses`), + rows returned verbatim (id, name, verification_status, created_time). Token-scoped, so no + `adAccountId` is needed. For TikTok Business Centers use + `GET /v1/ads/business-centers`. + + Platforms: meta + Args: account_id: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token. (required) limit: Rows per page @@ -1998,6 +2460,12 @@ def ad_accounts_list_meta_businesses( def ad_accounts_list_meta_business_users(account_id: str, business_id: str) -> str: """Business users + People and system users of a Meta business portfolio, with the business-scoped ids that + `POST /v1/ads/accounts/users` and `POST /v1/ads/page-users` take. The connected Meta + user must be an admin of the portfolio. + + Platforms: meta + Args: account_id: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token. (required) business_id: Meta business portfolio id. (required)""" @@ -2023,6 +2491,11 @@ def ad_accounts_list_page_users( ) -> str: """Page users of a business + People of a business portfolio assigned to a Facebook Page the portfolio owns or was + granted as a partner (`POST /v1/accounts/{accountId}/business-partners` on the owner side). + + Platforms: meta + Args: account_id: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token. (required) page_id: Facebook Page id. (required) @@ -2049,10 +2522,28 @@ def ad_accounts_assign_page_user( page_id: str, business_id: str, user_id: str, - tasks: list[str] | None, + tasks: list[ + Literal[ + "MANAGE", + "CREATE_CONTENT", + "MODERATE", + "MESSAGING", + "ADVERTISE", + "ANALYZE", + ] + ] + | None, ) -> str: """Assign a user to a Page + Gives a person of the portfolio tasks on a Page the portfolio owns or was granted as a + partner. Meta does not assign partner admins automatically, so after an owner shares a + Page the partner calls this for the people whose tokens will advertise for it. + `ADVERTISE` is what ad creation needs. Assigning an already assigned user replaces + their task set. + + Platforms: meta + Args: account_id: Zernio SocialAccount id used to resolve the Meta token. (required) page_id: Facebook Page id. (required) @@ -2085,6 +2576,8 @@ def ad_accounts_remove_page_user( ) -> str: """Remove a user from a Page + Platforms: meta + Args: account_id: Zernio SocialAccount id used to resolve the Meta token. (required) page_id: Facebook Page id. (required) @@ -2115,6 +2608,17 @@ def ad_accounts_list_ad_labels( ) -> str: """List ad labels + Lists the organizational labels on an ad account. + + - **Meta**: pass `adAccountId=act_`. Rows are Meta's `/act_X/adlabels` returned verbatim + (id, name, created/updated time), paginated with `limit` / `after`. + - **Google Ads**: pass the numeric customer id as `adAccountId` (optional when the + connection has a single customer). Returns every non-removed label as a `GoogleAdLabel` + in one page (`paging.after` is always null). Reads are cached for 10 minutes; when the + shared Google quota is exhausted the last successful result is served with `stale: true`. + + Platforms: meta, google + Args: account_id: Zernio SocialAccount id. For Meta, the posting or ads variant used to resolve the token. (required) ad_account_id: Meta ad account id (act_), or the Google Ads customer id (digits only). @@ -2152,6 +2656,12 @@ def ad_accounts_create_ad_label( ) -> str: """Create a Google Ads label + Creates a label on a Google Ads customer. Attach it to campaigns, ad groups, ads and + keywords with `POST /v1/ads/labels/{labelId}/assignments`. Label names are unique per + customer; a duplicate is a 400. + + Platforms: google + Args: account_id: Zernio SocialAccount id (Google Ads) (required) ad_account_id: Google customer id. Required when the connection has multiple customers. @@ -2192,6 +2702,10 @@ def ad_accounts_update_ad_label( ) -> str: """Update a Google Ads label + Changes the name, color or description of a label. Only the fields sent are written. + + Platforms: google + Args: label_id: Google label id (required) account_id: Zernio SocialAccount id (Google Ads) (required) @@ -2231,6 +2745,10 @@ def ad_accounts_remove_ad_label( ) -> str: """Remove a Google Ads label + Removes the label. Google drops it from every campaign, ad group, ad and keyword it was attached to. + + Platforms: google + Args: label_id: Google label id (required) account_id: Zernio SocialAccount id (Google Ads) (required) @@ -2268,6 +2786,15 @@ def ad_accounts_attach_ad_label( ) -> str: """Attach a Google Ads label + Attaches the label to campaigns, ad groups, ads and keywords (Google CampaignLabel, + AdGroupLabel, AdGroupAdLabel and AdGroupCriterionLabel) in one mutate. Idempotent: a + target that already carries the label is counted in `unchanged` instead of failing the + call. All ids are Google's own: ads and keywords use the composite id Google puts in + their resource names, `{adGroupId}~{adId}` and `{adGroupId}~{criterionId}` (the keyword + form is the tail of `resourceName` on `GET /v1/ads/keywords`). + + Platforms: google + Args: label_id: Google label id (required) account_id: Zernio SocialAccount id (Google Ads) (required) @@ -2313,6 +2840,10 @@ def ad_accounts_detach_ad_label( ) -> str: """Detach a Google Ads label + Removes the label from the given targets. Idempotent; a target without the label is counted in `unchanged`. + + Platforms: google + Args: label_id: Google label id (required) account_id: Zernio SocialAccount id (Google Ads) (required) @@ -2355,6 +2886,13 @@ def ad_accounts_list_high_demand_periods( ) -> str: """List high-demand periods + Scheduled budget increases (Meta's budget-scheduling API). The Graph edge lives on the + campaign and ad-set nodes only, so exactly one of `campaignId` / `adSetId` (platform + ids) is required. Rows returned verbatim (budget_value, budget_value_type, time window, + recurrence). + + Platforms: meta + Args: account_id: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token. (required) campaign_id: Platform campaign id. Exactly one of campaignId / adSetId. @@ -2385,16 +2923,28 @@ def ad_accounts_list_high_demand_periods( def ad_accounts_create_high_demand_period( account_id: str, budget_value: float, - budget_value_type: str, + budget_value_type: Literal["ABSOLUTE", "MULTIPLIER"], time_start: int, time_end: int, campaign_id: str | None = None, ad_set_id: str | None = None, - recurrence_type: str | None = None, + recurrence_type: Literal["ONE_TIME", "WEEKLY", "MONTHLY"] | None = None, currency: str | None = None, ) -> str: """Schedule a budget increase + Pre-schedule a temporary budget increase (Black Friday, a launch, a sale) instead of + editing the budget by hand on the day. Same target rule as the GET: exactly one of + `campaignId` / `adSetId`. + + Two Meta constraints worth knowing before you call it. `timeStart` / `timeEnd` must + fall on a 15-minute boundary, and a campaign cannot mix `ABSOLUTE` and `MULTIPLIER` + across its schedules; the second type is rejected with \"Can't mix your budget scaling + selection\". Window rules (must sit inside the campaign's run dates, minimum lead time, + no overlap) are Meta's and its message is forwarded verbatim. + + Platforms: meta + Args: account_id: Zernio SocialAccount id used to resolve the Meta token. (required) campaign_id: Platform campaign id. Exactly one of campaignId / adSetId. @@ -2435,6 +2985,26 @@ def ad_accounts_list_value_rule_sets( ) -> str: """List value rule sets + Lists the ad account's value rule sets (Meta's `/act_X/value_rule_set`). A value rule + set adjusts the auction bid up or down for audience segments you value differently; + attach one to an ad set with `valueRuleSetId` on `POST /v1/ads/create` or + `PUT /v1/ads/ad-sets/{adSetId}`. + + Rows are returned in the same camelCase shape the `PUT` body takes, ids included, so a + set round-trips 1:1: **the update is a full replace, not a patch**, so you GET, mutate + and send the whole thing back. + + Limits: 6 rule sets per ad account, 10 rules per set, 4 criteria per rule. + + **Rule order is semantic.** Rules are evaluated in array order and only the FIRST + matching rule adjusts the bid for an overlapping audience. The order you send is the + order that is stored and returned. + + Eligibility: value rule sets apply only to ad sets on the `LOWEST_COST_WITHOUT_CAP` + (auto-bid) or `COST_CAP` bid strategies. Meta rejects the rest server-side. + + Platforms: meta + Args: account_id: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token. (required) ad_account_id: Meta ad account id (act_). (required) @@ -2468,6 +3038,27 @@ def ad_accounts_create_value_rule_set( ) -> str: """Create a value rule set + Creates a value rule set on the ad account (Meta's `POST /act_X/value_rule_set`). + Attach the returned id to an ad set with `valueRuleSetId` on `POST /v1/ads/create` or + `PUT /v1/ads/ad-sets/{adSetId}`. + + **Rule order is semantic**: rules are evaluated in array order and only the first + matching rule adjusts the bid for an overlapping audience. + + `adjustValue` is an unsigned magnitude in percent; the direction lives in `adjustSign`. + `INCREASE` accepts 1-1000, `DECREASE` accepts 1-90. There is no signed field and 0 is + out of range. + + `criteriaValueTypes` is positionally paired with `criteriaValues` (same length, same + order). Every type is the literal `\"NONE\"` except on `LOCATION`, which uses + `LOCATION_COUNTRY` / `LOCATION_REGION` / `LOCATION_CITY` / `LOCATION_COMSCORE_MARKET` + and may mix them within one criterion. Location values are Targeting-Search keys: a + two-letter country code for `LOCATION_COUNTRY`, a numeric key for the rest. + + `LOCATION_DMA` was replaced by `LOCATION_COMSCORE_MARKET` ... + + Platforms: meta + Args: account_id: Zernio SocialAccount id (posting or ads variant); its platform decides where the campaign is created. (required) ad_account_id: Platform ad account id (Meta act_, Google customer id, LinkedIn account id, ...). (required) @@ -2496,6 +3087,16 @@ def ad_accounts_create_value_rule_set( def ad_accounts_get_value_rule_set(value_rule_set_id: str, account_id: str) -> str: """Read a value rule set + Reads one value rule set including every nested rule id and criterion id. This is step + one of any edit: `PUT` is a full replace, so you need the ids before you can keep the + objects you are not changing. + + Meta's own read returns `GENDER` values lowercase (`\"male\"`) while writes require + `\"MALE\"`. Values are passed through untouched, so never case-compare a stored rule + against a fetched one. + + Platforms: meta + Args: value_rule_set_id: Platform value rule set id. (required) account_id: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token. (required)""" @@ -2524,6 +3125,25 @@ def ad_accounts_update_value_rule_set( ) -> str: """Replace a value rule set + **THIS IS A FULL REPLACE, NOT A PATCH.** Meta's update is declarative: the body you + send becomes the rule set. + + - `GET /v1/ads/value-rule-sets/{valueRuleSetId}` FIRST. + - Keep a rule or criterion by echoing its `id`. + - Create one by including the object WITHOUT an `id`. + - Delete one by OMITTING it from the array. There is no warning and no undo. + + `name` and `rules` are both required for exactly this reason: a partial body would + silently destroy every rule left out. + + **Rule order is semantic**: the array order you send is the evaluation order, and only + the first matching rule adjusts the bid for an overlapping audience. + + Existing rule sets created elsewhere may contain `LOCATION_DMA` criteria. Those went + inert on 2026-06-22 and are rejected here; migrate them to `LOCATION_COMSCORE_MARKET`. + + Platforms: meta + Args: value_rule_set_id: Platform value rule set id. (required) account_id: Zernio SocialAccount id (posting or ads variant); its platform decides where the campaign is created. (required) @@ -2554,6 +3174,13 @@ def ad_accounts_delete_value_rule_set( ) -> str: """Delete a value rule set + Deletes the rule set (Meta's `POST /{value-rule-set-id}/delete_rule_set`, a custom + action edge rather than an HTTP DELETE on its side). Ad sets pointing at it are not + modified here; detach them first with `valueRulesApplied: false` on + `PUT /v1/ads/ad-sets/{adSetId}`. + + Platforms: meta + Args: value_rule_set_id: Platform value rule set id. (required) account_id: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token. (required)""" @@ -2578,10 +3205,24 @@ def ad_accounts_list_ad_negative_keyword_lists( account_id: str, ad_account_id: str | None = None, customer_id: str | None = None, - platform: str | None = None, + platform: Literal[ + "facebook", + "instagram", + "tiktok", + "linkedin", + "pinterest", + "google", + "twitter", + "openai", + ] + | None = None, ) -> str: """List negative keyword lists + Google Ads shared negative keyword lists (shared_set type NEGATIVE_KEYWORDS). Reads are cached for 10 minutes; quota exhaustion may return the last successful result for up to 7 days with stale=true. Customer selection is limited to this connection and its account scope. + + Platforms: google + Args: account_id: (required) ad_account_id @@ -2612,11 +3253,25 @@ def ad_accounts_create_ad_negative_keyword_list( name: str, ad_account_id: str | None = None, customer_id: str | None = None, - platform: str | None = None, + platform: Literal[ + "facebook", + "instagram", + "tiktok", + "linkedin", + "pinterest", + "google", + "twitter", + "openai", + ] + | None = None, keywords: list[Any] | None = None, ) -> str: """Create a negative keyword list + Creates one Google Ads shared negative keyword list with optional initial keywords in a single atomic mutation. Daily quota is reserved for every mutate item, so large batches may return 429 before any change. This operation is not idempotent. The list is not attached to any campaign. + + Platforms: google + Args: account_id: Zernio SocialAccount id. (required) ad_account_id: Platform ad account ID (Google customer ID, digits only). Required when the connection has multiple customers. @@ -2651,10 +3306,24 @@ def ad_accounts_get_ad_negative_keyword_list( account_id: str, ad_account_id: str | None = None, customer_id: str | None = None, - platform: str | None = None, + platform: Literal[ + "facebook", + "instagram", + "tiktok", + "linkedin", + "pinterest", + "google", + "twitter", + "openai", + ] + | None = None, ) -> str: """Get a negative keyword list + Google Ads shared negative keyword lists (shared_set type NEGATIVE_KEYWORDS). Reads are cached for 10 minutes; quota exhaustion may return the last successful result for up to 7 days with stale=true. Customer selection is limited to this connection and its account scope. Includes the keywords and their criterion ids. + + Platforms: google + Args: list_id: (required) account_id: (required) @@ -2688,10 +3357,24 @@ def ad_accounts_update_ad_negative_keyword_list( name: str, ad_account_id: str | None = None, customer_id: str | None = None, - platform: str | None = None, + platform: Literal[ + "facebook", + "instagram", + "tiktok", + "linkedin", + "pinterest", + "google", + "twitter", + "openai", + ] + | None = None, ) -> str: """Rename a negative keyword list + Renames a shared negative keyword list. Keywords and campaign associations are unchanged. Use the keywords endpoint to edit the desired keyword set. + + Platforms: google + Args: list_id: (required) account_id: Zernio SocialAccount id. (required) @@ -2726,10 +3409,24 @@ def ad_accounts_delete_ad_negative_keyword_list( account_id: str, ad_account_id: str | None = None, customer_id: str | None = None, - platform: str | None = None, + platform: Literal[ + "facebook", + "instagram", + "tiktok", + "linkedin", + "pinterest", + "google", + "twitter", + "openai", + ] + | None = None, ) -> str: """Delete a negative keyword list + Removes the Google shared negative keyword list. Detach it from all campaigns first; an in-use list is rejected. Only NEGATIVE_KEYWORDS shared sets are supported. + + Platforms: google + Args: list_id: (required) account_id: (required) @@ -2763,10 +3460,24 @@ def ad_accounts_replace_ad_negative_keyword_list_keywords( keywords: list[Any] | None, ad_account_id: str | None = None, customer_id: str | None = None, - platform: str | None = None, + platform: Literal[ + "facebook", + "instagram", + "tiktok", + "linkedin", + "pinterest", + "google", + "twitter", + "openai", + ] + | None = None, ) -> str: """Replace negative list keywords + Replaces the full desired keyword set. Existing keywords are diffed by normalized text and match type; creates and removals are applied atomically in one mutation. Unchanged criteria retain their ids. Send an empty keywords array to clear the list. Changes affect every campaign using this list. Each create or removal consumes one daily operation; the entire batch must fit the remaining quota. + + Platforms: google + Args: list_id: (required) account_id: Zernio SocialAccount id. (required) @@ -2803,6 +3514,10 @@ def ad_accounts_list_account_callouts( ) -> str: """List account callouts + Lists directly attached Google assets. Fresh reads are cached for 10 minutes; exhausted quota may return the last successful read with stale=true. Inherited assets are not included. Preserves Google RMF C.75 account-level callouts. + + Platforms: google + Args: account_id: (required) ad_account_id @@ -2834,6 +3549,10 @@ def ad_accounts_add_account_callouts( ) -> str: """Add account callouts + Creates assets and customer_asset links for this Google customer. Links apply at account level. + + Platforms: google + Args: account_id: Zernio Google Ads connection id. (required) ad_account_id: Platform ad account ID (Google customer ID, digits only). Required when the connection has multiple customers. @@ -2867,6 +3586,10 @@ def ad_accounts_update_account_callouts( ) -> str: """Update account callouts + Edits existing Google assets in place. Send updates with assetResourceName and the fields to change. An asset is shared: changes affect every attachment using it. Omitted fields stay unchanged. The operation consumes the Google operations budget and invalidates affected cached lists. + + Platforms: google + Args: account_id: Zernio Google Ads connection id. (required) ad_account_id: Platform ad account ID (Google customer ID, digits only). Required when the connection has multiple customers. @@ -2900,6 +3623,10 @@ def ad_accounts_remove_account_callout( ) -> str: """Remove account callout + Removes the customer_asset attachment only. The underlying shared asset and its campaign or ad-group attachments remain. + + Platforms: google + Args: account_id: Zernio Google Ads connection id. (required) ad_account_id: Platform ad account ID (Google customer ID, digits only). Required when the connection has multiple customers. @@ -2932,6 +3659,10 @@ def ad_accounts_list_account_sitelinks( ) -> str: """List account sitelinks + Lists directly attached Google assets. Fresh reads are cached for 10 minutes; exhausted quota may return the last successful read with stale=true. Inherited assets are not included. + + Platforms: google + Args: account_id: (required) ad_account_id @@ -2963,6 +3694,10 @@ def ad_accounts_add_account_sitelinks( ) -> str: """Add account sitelinks + Creates assets and customer_asset links for this Google customer. Links apply at account level. + + Platforms: google + Args: account_id: Zernio Google Ads connection id. (required) ad_account_id: Platform ad account ID (Google customer ID, digits only). Required when the connection has multiple customers. @@ -2996,6 +3731,10 @@ def ad_accounts_update_account_sitelinks( ) -> str: """Update account sitelinks + Edits existing Google assets in place. Send updates with assetResourceName and the fields to change. An asset is shared: changes affect every attachment using it. Omitted fields stay unchanged. The operation consumes the Google operations budget and invalidates affected cached lists. + + Platforms: google + Args: account_id: Zernio Google Ads connection id. (required) ad_account_id: Platform ad account ID (Google customer ID, digits only). Required when the connection has multiple customers. @@ -3029,6 +3768,10 @@ def ad_accounts_remove_account_sitelink( ) -> str: """Remove account sitelink + Removes the customer_asset attachment only. The underlying shared asset and its campaign or ad-group attachments remain. + + Platforms: google + Args: account_id: Zernio Google Ads connection id. (required) ad_account_id: Platform ad account ID (Google customer ID, digits only). Required when the connection has multiple customers. @@ -3061,6 +3804,10 @@ def ad_accounts_list_account_structured_snippets( ) -> str: """List account snippets + Lists directly attached Google assets. Fresh reads are cached for 10 minutes; exhausted quota may return the last successful read with stale=true. Inherited assets are not included. + + Platforms: google + Args: account_id: (required) ad_account_id @@ -3092,6 +3839,10 @@ def ad_accounts_add_account_structured_snippets( ) -> str: """Add account snippets + Creates assets and customer_asset links for this Google customer. Links apply at account level. + + Platforms: google + Args: account_id: Zernio Google Ads connection id. (required) ad_account_id: Platform ad account ID (Google customer ID, digits only). Required when the connection has multiple customers. @@ -3125,6 +3876,10 @@ def ad_accounts_update_account_structured_snippets( ) -> str: """Update account snippets + Edits existing Google assets in place. Send updates with assetResourceName and the fields to change. An asset is shared: changes affect every attachment using it. Omitted fields stay unchanged. The operation consumes the Google operations budget and invalidates affected cached lists. + + Platforms: google + Args: account_id: Zernio Google Ads connection id. (required) ad_account_id: Platform ad account ID (Google customer ID, digits only). Required when the connection has multiple customers. @@ -3158,6 +3913,10 @@ def ad_accounts_remove_account_structured_snippet( ) -> str: """Remove account snippet + Removes the customer_asset attachment only. The underlying shared asset and its campaign or ad-group attachments remain. + + Platforms: google + Args: account_id: Zernio Google Ads connection id. (required) ad_account_id: Platform ad account ID (Google customer ID, digits only). Required when the connection has multiple customers. @@ -3190,6 +3949,10 @@ def ad_accounts_get_ad_account_hierarchy( ) -> str: """Get manager account hierarchy + Live manager (MCC) and client tree for a Google Ads connection. Starts from every customer the Google user behind the connection can access directly and walks each tree to any depth with `customer_client`, then reads each manager's own client links so every client carries its direct parent, the `managerLinkId` and the link status. Invitations a manager sent that the client has not accepted yet appear as clients with `linkStatus: PENDING` (Google returns no name or currency for them). Refused, canceled and ended links are history and are omitted. `managerLinks` on a root lists the managers linked to that account, including invitations it can still accept with PATCH /v1/ads/accounts/manager-links. A directly accessible account that is also nested in another tree appears only once, inside that tree. `directCustomers` lists every account the Google user accesses directly (the ones this connection can accept or decline invitations for) with their pending invitations, including accounts ... + + Platforms: google + Args: account_id: Google ads SocialAccount id. (required) ad_account_id: Only return the tree rooted at this customer id (digits only). It must be an account the Google user accesses directly. Omit to list every tree. @@ -3221,6 +3984,10 @@ def ad_accounts_invite_ad_account_to_manager( ) -> str: """Invite a client account to a manager + Sends a manager-to-client link invitation from `managerCustomerId` to `clientCustomerId` (Google's CustomerClientLinkService). The manager must be one the connection's Google user reaches, directly or under another manager (see GET /v1/ads/accounts/hierarchy); the client can be any Google Ads account. The link stays `PENDING` until someone with access to the client accepts it in Google Ads or through PATCH on this path. Not idempotent: Google refuses a second invitation while one is pending. Send `validateOnly: true` to have Google check the request without sending anything. + + Platforms: google + Args: account_id: Google ads SocialAccount id. (required) manager_customer_id: Manager customer id, digits only. (required) @@ -3251,11 +4018,15 @@ def ad_accounts_update_ad_account_manager_link( manager_customer_id: str, client_customer_id: str, manager_link_id: str, - action: str, + action: Literal["accept", "decline", "cancel", "unlink"], validate_only: bool = False, ) -> str: """Accept, decline, cancel or end a manager link + Changes one manager-client link, identified by `managerCustomerId`, `clientCustomerId` and `managerLinkId` (all from GET /v1/ads/accounts/hierarchy). `accept` and `decline` answer a pending invitation as the client (CustomerManagerLinkService), so the connection's Google user needs direct access to the client account; access through a manager is not enough, because the link does not exist yet. `cancel` withdraws a pending invitation and `unlink` ends an active link, both as the manager (CustomerClientLinkService). Send `validateOnly: true` to have Google check the change without applying it. + + Platforms: google + Args: account_id: Google ads SocialAccount id. (required) manager_customer_id: Manager customer id, digits only. (required) @@ -3290,6 +4061,11 @@ def ad_accounts_list_ad_account_users( ) -> str: """Ad account users + People of a business portfolio assigned to a Meta ad account, with their tasks. + Ids are business-scoped user ids (see `GET /v1/ads/businesses/users`). + + Platforms: meta + Args: account_id: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token. (required) ad_account_id: Meta ad account id (act_). (required) @@ -3314,10 +4090,19 @@ def ad_accounts_list_ad_account_users( ) ) def ad_accounts_assign_ad_account_user( - account_id: str, ad_account_id: str, user_id: str, tasks: list[str] | None + account_id: str, + ad_account_id: str, + user_id: str, + tasks: list[Literal["MANAGE", "ADVERTISE", "ANALYZE", "DRAFT"]] | None, ) -> str: """Assign a user to an ad account + Gives a person of the portfolio tasks on the ad account. `MANAGE` is admin, `ADVERTISE` + creates and edits ads, `ANALYZE` reads reports, `DRAFT` edits drafts only. Assigning an + already assigned user replaces their task set. + + Platforms: meta + Args: account_id: Zernio SocialAccount id used to resolve the Meta token. (required) ad_account_id: Meta ad account id (act_). (required) @@ -3348,6 +4133,8 @@ def ad_accounts_remove_ad_account_user( ) -> str: """Remove a user from an ad account + Platforms: meta + Args: account_id: Zernio SocialAccount id used to resolve the Meta token. (required) ad_account_id: Meta ad account id (act_). (required) @@ -3372,6 +4159,12 @@ def ad_accounts_remove_ad_account_user( def ad_accounts_get_ad_account_finance(account_id: str, ad_account_id: str) -> str: """Ad account finances + Finances of one Meta ad account: prepaid `balance`, lifetime `amountSpent`, account + `spendCap` (null = no cap) and the `fundingSource`. Money values are converted from + Meta's minor units to whole units of `currency`. + + Platforms: meta + Args: account_id: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token. (required) ad_account_id: Meta ad account id (act_). (required)""" @@ -3411,6 +4204,24 @@ def ad_accounts_create_ad_account( ) -> str: """Create Meta ad account + Creates a durable Meta ad account in the end user's own business portfolio using + their connected Meta Ads token. Requires an active metaads accountId, Ads access, + business_management permission and business admin access. Discover portfolios with + GET /v1/ads/businesses. System-user tokens may return an empty businesses list; + supply the known business ID in that case. + + The self-serve account starts without a payment method. The user must add a payment + method in Ads Manager before ads can deliver. Zernio cannot add payment methods. + Meta may require business verification and limits how many accounts a business can + create. Closing an account does not guarantee more capacity. An ad account cannot + truly be deleted, even after closing it and removing it from a business. + + timezoneId is Meta's numeric ID, not an IANA timezone name. Select it from + https://developers.facebook.com/docs/marketing-api/reference/ad-account/timezone-ids/. + For example, 1 is America/Los_Angeles. Meta validates ... + + Platforms: meta + Args: account_id: Zernio metaads SocialAccount ID. (required) business_id: Business portfolio that will own the account. (required) @@ -3463,6 +4274,23 @@ def ad_accounts_list_ad_accounts( ) -> str: """List ad accounts + Returns the platform ad accounts available for the given account (e.g. Meta ad + accounts, TikTok advertiser IDs, Google Ads customer IDs). + Meta business-login accounts use their own system-user token. Fresh Meta discovery + includes businessId and businessName from the owning Business Manager when available; + cached entries gain these fields after the next discovery refresh. + + For TikTok agencies: enumerates every advertiser under every Business Center the token + can read (paginated server-side), then chunks the lookup against TikTok's + `/advertiser/info/` endpoint (which has a per-call cap of ≤100 IDs). Solo advertisers + without a BC fall back to the OAuth-time `advertiser_ids` list. Cached for 1h on the + SocialAccount; lazy-refreshed on first call after expiry. + + For Google Ads: responds `429` when Google's API quota is temporarily exhausted + (instead of an empty list). Retry after a delay. + + Platforms: meta, google, tiktok, linkedin, pinterest, x + Args: account_id: Account ID (required) ad_account_id: Filter response to a single platform ad account ID (e.g. `act_123` for Meta, advertiser_id for TikTok). Returns at most one item. @@ -3495,6 +4323,25 @@ def ad_accounts_update_ad_account( ) -> str: """Update ad account settings + Updates a Meta ad account in place: its name, its account-level spend cap, and its + default DSA beneficiary and payor. Pass any combination of fields. + + **Spend cap.** `spendCap` is the total the account may spend before Meta pauses every + campaign in it, in whole units of the account currency. `spendCap: null` removes the + cap and `resetAmountSpent: true` restarts the amount counted against it from zero. + When `name`, `spendCap` or `resetAmountSpent` is passed, the response carries + `settings`, the account's finances re-read after the write (same shape as + `GET /v1/ads/accounts/finance`), so the effective cap can be confirmed in one call. + + **DSA defaults.** Sets the default DSA beneficiary and payor on the ad account (EU DSA, Article 26). + Set them once and every EU-targeted call to `/v1/ads/create`, `/v1/ads/boost` and + `/v1/ads/ctwa` on that ad account can omit `dsaBeneficiary`/`dsaPayor`: Meta applies + the defaults automatically. + + The values are written to the ad account on Meta, the same ... + + Platforms: meta + Args: account_id: Account ID (metaads, or a facebook/instagram posting account) (required) ad_account_id: Meta ad account ID (act_...) (required) @@ -3529,6 +4376,12 @@ def ad_accounts_update_ad_account( def ad_accounts_get_dsa_defaults(account_id: str, ad_account_id: str) -> str: """Get ad account DSA defaults + Returns the default DSA beneficiary and payor currently set on a Meta ad account, + whether they were set via `PATCH /v1/ads/accounts` or in Meta Ads Manager. Fields + are omitted when no default is configured. Meta accounts only. + + Platforms: meta + Args: account_id: Account ID (metaads, or a facebook/instagram posting account) (required) ad_account_id: Meta ad account ID (act_...) (required)""" @@ -3552,6 +4405,17 @@ def ad_accounts_get_dsa_defaults(account_id: str, ad_account_id: str) -> str: def ad_accounts_get_dsa_recommendations(account_id: str, ad_account_id: str) -> str: """Get DSA recommendations + Returns Meta's suggested beneficiary/payor names for an ad account, derived by Meta + from the account's recent activity. Useful for prefilling `dsaBeneficiary`/`dsaPayor` + inputs, or the defaults sent to `PATCH /v1/ads/accounts`, in your own UI. + + Meta returns a single flat list. Entries are not labeled as beneficiary or payor, + and since these are legal disclosures Zernio never applies them automatically: let + your user pick the right entity. The list may be empty for accounts with little + activity. Meta accounts only. + + Platforms: meta + Args: account_id: Account ID (metaads, or a facebook/instagram posting account) (required) ad_account_id: Meta ad account ID (act_...) (required)""" @@ -3575,6 +4439,10 @@ def ad_accounts_get_dsa_recommendations(account_id: str, ad_account_id: str) -> def ad_accounts_list_custom_conversions(account_id: str, ad_account_id: str) -> str: """List custom conversions + The ad account's Meta custom conversions, including archived ones (`isArchived`). + + Platforms: meta + Args: account_id: Meta ads SocialAccount id. (required) ad_account_id: Meta ad account id (act_). (required)""" @@ -3605,6 +4473,21 @@ def ad_accounts_create_custom_conversion( ) -> str: """Create custom conversion + Provision the Meta custom conversion an ads flow optimises toward, and hand back the + `customConversionId` for `promotedObject.customConversionId` on POST /v1/ads/create. + Removes the manual \"create it in Ads Manager first\" step. + + **Reuse is ours, not Meta's.** Meta's create is not idempotent, so a retried request + would otherwise mint a duplicate carrying none of the original's optimisation history. + A non-archived conversion with the same `name` on the same `pixelId` is returned + instead of created, with `reused: true` and a 200 rather than a 201. + + `rule` is forwarded verbatim in Meta's own grammar (e.g. + `{\"url\": {\"i_contains\": \"thank-you\"}}`); Meta validates it and rejects a malformed one + with \"A conversion rule is required at creation time\". + + Platforms: meta + Args: account_id: Meta ads SocialAccount id. (required) ad_account_id: Platform ad account id (Meta act_, Google customer id, LinkedIn account id, ...). (required) @@ -3642,6 +4525,10 @@ def ad_accounts_list_tik_tok_ad_pixels( ) -> str: """List TikTok ad pixels + Lists pixels and their supported optimization events for a connected TikTok Ads account. The advertiser defaults to the first advertiser on the connection. Reconnect if Pixel Management permission has not been granted. + + Platforms: tiktok + Args: account_id: Zernio SocialAccount ID. (required) ad_account_id: Platform ad account ID (TikTok advertiser id, digits only). Defaults to the first advertiser on the connection. @@ -3672,11 +4559,37 @@ def ad_accounts_list_tik_tok_ad_pixels( def ad_audiences_list_ad_audiences( account_id: str, ad_account_id: str, - platform: str | None = None, - type: str | None = None, + platform: Literal[ + "facebook", + "instagram", + "googleads", + "tiktok", + "tiktokads", + "pinterest", + "linkedin", + "linkedinads", + "twitter", + "xads", + ] + | None = None, + type: Literal[ + "customer_list", + "company_list", + "engagement", + "meta_engagement", + "website", + "website_retargeting", + "lookalike", + "saved_targeting", + ] + | None = None, ) -> str: """List custom audiences + Returns custom audiences for the given ad account. Supports Meta, Google, TikTok, Pinterest, LinkedIn, and X. + + Platforms: meta, google, tiktok, linkedin, pinterest, x + Args: account_id: Account ID (required) ad_account_id: Platform ad account ID (required) @@ -3705,6 +4618,22 @@ def ad_audiences_list_ad_audiences( def ad_audiences_create_ad_audience(body: dict[str, Any]) -> str: """Create custom audience + 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 ... + + Platforms: meta, google, tiktok, linkedin, pinterest, x + Args: body: Full request body as documented in the API reference. (required)""" client = _get_client() @@ -3725,6 +4654,10 @@ def ad_audiences_create_ad_audience(body: dict[str, Any]) -> str: def ad_audiences_get_ad_audience(audience_id: str) -> str: """Get audience details + Returns the local audience record and fresh data from Meta (if available). + + Platforms: meta, google, tiktok, linkedin, pinterest, x + Args: audience_id: The Zernio audience id (the id field of GET /v1/ads/audiences), not the platform segment id. (required)""" client = _get_client() @@ -3750,6 +4683,16 @@ def ad_audiences_update_ad_audience( ) -> str: """Update an audience + 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. + + Platforms: meta, google, tiktok, linkedin, pinterest, x + Args: audience_id: (required) name @@ -3775,6 +4718,17 @@ def ad_audiences_update_ad_audience( def ad_audiences_delete_ad_audience(audience_id: str) -> str: """Delete custom audience + 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`. + + Platforms: meta, google, tiktok, linkedin, pinterest, x + Args: audience_id: (required)""" client = _get_client() @@ -3797,6 +4751,15 @@ def ad_audiences_add_users_to_ad_audience( ) -> str: """Add users to audience + 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. + + Platforms: meta, google, tiktok, linkedin, pinterest, x + Args: audience_id: The Zernio audience id (the id field of GET /v1/ads/audiences), not the platform segment id. (required) users: (required)""" @@ -3822,6 +4785,28 @@ def ad_audiences_replace_ad_audience_companies( ) -> str: """Replace audience companies + 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 ... + + Platforms: linkedin + Args: audience_id: The Zernio audience id (the id field of GET /v1/ads/audiences), not the platform segment id. (required) companies: The complete company list. Each row needs at least one of name, domain, website or linkedinPageUrl. (required)""" @@ -3847,9 +4832,28 @@ def ad_audiences_replace_ad_audience_companies( def ad_campaigns_list_ads( page: int = 1, limit: int = 50, - source: str = "all", - status: str | None = None, - platform: str | None = None, + source: Literal["zernio", "all"] = "all", + status: Literal[ + "active", + "paused", + "pending_review", + "rejected", + "completed", + "cancelled", + "error", + ] + | None = None, + platform: Literal[ + "facebook", + "instagram", + "tiktok", + "linkedin", + "pinterest", + "google", + "twitter", + "openai", + ] + | None = None, account_id: str | None = None, ad_account_id: str | None = None, page_id: str | None = None, @@ -3864,6 +4868,18 @@ def ad_campaigns_list_ads( ) -> str: """List ads + Returns a paginated list of ads with metrics computed over an optional date range. + Use source=all to include externally-synced ads from platform ad managers. + If no date range is provided, defaults to the last 90 days. Date range is capped at 730 days max. + + To find the Zernio ad behind a comment you see in Meta Business Manager, filter by + platformAdId (the Meta ad ID), effectiveObjectStoryId (Facebook), or + effectiveInstagramMediaId (Instagram). Those are the post/media the ad's engagement + lives on, and are also returned on each ad's `creative` object. Then call + GET /v1/ads/{adId}/comments with the returned ad id. + + Platforms: meta, google, tiktok, linkedin, pinterest, x + Args: page: Page number limit @@ -3922,6 +4938,10 @@ def ad_campaigns_list_google_recommendations( ) -> str: """List Google Ads recommendations + Google's optimization recommendations for one ad account: type, estimated impact (base vs potential metrics, cost in account currency units), the campaign, ad group or budget they target, and the type-specific payload Google returns (`details`, in Google's own shape with micros). Filter by campaignId and types. Cached for 10 minutes and cleared by apply or dismiss; served stale when Google quota is exhausted. + + Platforms: google + Args: account_id: Google ads SocialAccount id. (required) ad_account_id: Google customer id, digits only. Defaults to the connection's only customer. @@ -3956,6 +4976,10 @@ def ad_campaigns_apply_google_recommendations( ) -> str: """Apply Google Ads recommendations + Apply up to 100 recommendations. This changes the account (budgets, bidding, keywords, assets) and is not reversible or idempotent; Google offers no validate-only mode for it. Items run in partial-failure mode, so one stale recommendation does not block the rest. `parameters` is optional and takes exactly one key named for the recommendation type, in Google's ApplyRecommendationOperation shape (for example `campaignBudget: { newBudgetAmountMicros }` or `keyword: { matchType, cpcBidMicros }`); omit it to apply Google's suggested values. + + Platforms: google + Args: account_id: Google ads SocialAccount id. (required) ad_account_id: Google customer id, digits only. Required when the connection has several customers. @@ -3986,6 +5010,10 @@ def ad_campaigns_dismiss_google_recommendations( ) -> str: """Dismiss Google Ads recommendations + Dismiss up to 100 recommendations so Google stops suggesting them. Items run in partial-failure mode. + + Platforms: google + Args: account_id: Google ads SocialAccount id. (required) ad_account_id: Google customer id, digits only. Required when the connection has several customers. @@ -4018,6 +5046,10 @@ def ad_campaigns_list_bid_strategies( ) -> str: """List portfolio bid strategies + Bidding strategy report: type, status, campaign count, clicks, cost, cost per conversion, impressions, average CPC and conversions over the date range (default last 30 days). Reads Google's `bidding_strategy` resource, cached for the quota window. Draws on the shared Google Ads operations budget. The response carries `cachedAt` and `stale`, set when a quota-exhausted call falls back to the last-good copy instead of a live read. + + Platforms: google + Args: account_id: Google ads SocialAccount id. (required) ad_account_id: Platform ad account ID (Google customer ID, digits only). Defaults to the account's connected customer. @@ -4048,7 +5080,12 @@ def ad_campaigns_list_bid_strategies( def ad_campaigns_create_bid_strategy( account_id: str, name: str, - type: str, + type: Literal[ + "TARGET_CPA", + "TARGET_ROAS", + "MAXIMIZE_CONVERSIONS", + "MAXIMIZE_CONVERSION_VALUE", + ], ad_account_id: str | None = None, customer_id: str | None = None, target_cpa: float | None = None, @@ -4056,6 +5093,10 @@ def ad_campaigns_create_bid_strategy( ) -> str: """Create portfolio bid strategy + Creates a standalone bid strategy shared across campaigns. Attach it to a campaign with `portfolioBidStrategyId` on POST /v1/ads/create, PUT /v1/ads/campaigns/{campaignId}, or PUT /v1/ads/ad-sets/{adSetId}. Attaching a strategy aligned to a shared budget fails there with a 400 (Google's `BIDDING_STRATEGY_AND_BUDGET_MUST_BE_ALIGNED`); this is not retryable. + + Platforms: google + Args: account_id: Google ads SocialAccount id. (required) ad_account_id: Platform ad account ID (Google customer ID, digits only). Defaults to the account's connected customer. @@ -4093,12 +5134,22 @@ def ad_campaigns_update_bid_strategy( ad_account_id: str | None = None, customer_id: str | None = None, name: str | None = None, - type: str | None = None, + type: Literal[ + "TARGET_CPA", + "TARGET_ROAS", + "MAXIMIZE_CONVERSIONS", + "MAXIMIZE_CONVERSION_VALUE", + ] + | None = None, target_cpa: float | None = None, target_roas: float | None = None, ) -> str: """Update portfolio bid strategy + Renames or retargets a portfolio bid strategy. The strategy's status is output only on Google's side, so it cannot be changed here; remove a strategy in Google Ads. `type` is only needed alongside `targetCpa`/`targetRoas` to disambiguate the field Google writes to (TARGET_CPA and MAXIMIZE_CONVERSIONS both take a target CPA; TARGET_ROAS and MAXIMIZE_CONVERSION_VALUE both take a target ROAS); the strategy's family is otherwise immutable once created. + + Platforms: google + Args: strategy_id: Numeric Google Ads bid strategy id. (required) account_id: Google ads SocialAccount id. (required) @@ -4140,13 +5191,25 @@ def ad_campaigns_list_ad_keywords( profile_id: str | None = None, campaign_id: str | None = None, ad_set_id: str | None = None, - status: str | None = None, - match_type: str | None = None, + status: Literal["active", "paused"] | None = None, + match_type: Literal["exact", "phrase", "broad", "unknown"] | None = None, negative: bool | None = None, search: str | None = None, ) -> str: """List Search keywords + Returns the Google Search keyword criteria (positive and negative) synced from + connected Google Ads accounts, one row per ad-group keyword. Refreshed about + once a day per Google Ads customer (the keyword sweep rides the ads discovery + pass on a slower slot), so keywords added on Google can take up to a day to + appear. A customer synced for the first time is populated on the next discovery + pass rather than waiting for its daily slot, and connecting an account or + triggering a manual sync refreshes it immediately. + Campaign-level negative keywords are not included; only ad-group-level + criteria are. + + Platforms: google + Args: page: Page number limit @@ -4194,6 +5257,14 @@ def ad_campaigns_add_ad_keywords( ) -> str: """Add Search ad-group keywords + Adds one or more keyword criteria to an existing Google Search ad group, + without touching the keywords already there (unlike the whole-set diff on + `PUT /v1/ads/{adId}`, `keywords`/`negativeKeywords` in `platformSpecificData`, + which replaces the set). Set `negative: true` to add ad-group-level negatives + instead of positive keywords. + + Platforms: google + Args: account_id: Account ID (Google Ads) (required) ad_set_id: Google ad group ID to add the keywords to (required) @@ -4219,9 +5290,16 @@ def ad_campaigns_add_ad_keywords( openWorldHint=True, ) ) - def ad_campaigns_update_ad_keyword(keyword_id: str, status: str) -> str: + def ad_campaigns_update_ad_keyword( + keyword_id: str, status: Literal["active", "paused"] + ) -> str: """Pause or enable a Search keyword + Changes `ad_group_criterion.status` for one keyword criterion (M.140). + Negative keywords have no status on Google and cannot be paused or enabled. + + Platforms: google + Args: keyword_id: Zernio keyword ID (`id`), or Google's native `{adSetId}~{platformCriterionId}` (the tail of `resourceName`, e.g. 1234567890~987654321). A bare criterion id is rejected because it is only unique within its ad group. (required) status: (required)""" @@ -4245,6 +5323,10 @@ def ad_campaigns_update_ad_keyword(keyword_id: str, status: str) -> str: def ad_campaigns_remove_ad_keyword(keyword_id: str) -> str: """Remove a Search keyword + Removes one keyword criterion (positive or negative) from its ad group (M.140). + + Platforms: google + Args: keyword_id: Zernio keyword ID (`id`), or Google's native `{adSetId}~{platformCriterionId}` (the tail of `resourceName`, e.g. 1234567890~987654321). A bare criterion id is rejected because it is only unique within its ad group. (required)""" client = _get_client() @@ -4266,9 +5348,28 @@ def ad_campaigns_list_ad_campaigns( include_empty: bool | None = None, page: int = 1, limit: int = 20, - source: str = "all", - platform: str | None = None, - status: str | None = None, + source: Literal["zernio", "all"] = "all", + platform: Literal[ + "facebook", + "instagram", + "tiktok", + "linkedin", + "pinterest", + "google", + "twitter", + "openai", + ] + | None = None, + status: Literal[ + "active", + "paused", + "pending_review", + "rejected", + "completed", + "cancelled", + "error", + ] + | None = None, ad_account_id: str | None = None, page_id: str | None = None, account_id: str | None = None, @@ -4280,6 +5381,21 @@ def ad_campaigns_list_ad_campaigns( ) -> str: """List campaigns + Returns campaigns as virtual aggregations over ad documents grouped by platform campaign ID. + Metrics (spend, impressions, clicks, etc.) are summed across all ads in each campaign. + Campaign status is derived from child ad statuses (active > pending_review > paused > error > completed > cancelled > rejected). + Google campaign budgets include amountMicros, explicitlyShared, resourceName and + deliveryMethod after the next successful sync. This endpoint does not fetch Google live. + + **Status freshness.** `status`, `configuredStatus`, `platformStatus`, `platformAdSetStatus` + and `platformCampaignStatus` are the values Zernio last stored. Background sync refreshes + them, typically within 15 to 60 minutes (Google up to about 3 hours), and ended or + long-paused objects may be refreshed less often. Zernio's own status writes re-read the + switches they change. A change made in the platform's own ads manager therefore shows up + here only after the next sync. Controllers that act on a switch should pass ... + + Platforms: meta, google, tiktok, linkedin, pinterest, x + Args: include_empty: Meta only. Campaign reads aggregate over ad documents, so a campaign with ZERO ads is normally invisible here, the state the two-step create (campaign, then ads via `existingCampaignId`) leaves behind whenever Meta rejects the ad step. Set true to list those too, with `adCount: 0` and zeroed metrics. Requires `accountId` and `adAccountId`, since an empty campaign has no ad row to resolve a token or ad account from. page: Page number @@ -4329,23 +5445,70 @@ def ad_campaigns_create_ad_campaign( account_id: str, ad_account_id: str, name: str, - goal: str, + goal: Literal[ + "engagement", + "traffic", + "awareness", + "video_views", + "lead_generation", + "lead_conversion", + "job_applicants", + "conversions", + "app_promotion", + "catalog_sales", + "page_likes", + "page_visits", + ], is_skadnetwork_attribution: bool | None = None, promoted_object: dict[str, Any] | None = None, - buying_type: str | None = None, + buying_type: Literal["AUCTION", "RESERVED"] | None = None, validate_only: bool | None = None, - special_ad_categories: list[str] | None = None, + special_ad_categories: list[ + Literal[ + "HOUSING", + "EMPLOYMENT", + "CREDIT", + "ISSUES_ELECTIONS_POLITICS", + "FINANCIAL_PRODUCTS_SERVICES", + "ONLINE_GAMBLING_AND_GAMING", + ] + ] + | None = None, budget_amount: float | None = None, - budget_type: str | None = None, - status: str = "PAUSED", - location_targeting_type: str | None = None, - bid_strategy: str | None = None, + budget_type: Literal["daily", "lifetime"] | None = None, + status: Literal["ACTIVE", "PAUSED"] = "PAUSED", + location_targeting_type: Literal["presence", "presence_or_interest"] + | None = None, + bid_strategy: Literal[ + "LOWEST_COST_WITHOUT_CAP", + "LOWEST_COST_WITH_BID_CAP", + "COST_CAP", + "LOWEST_COST_WITH_MIN_ROAS", + ] + | None = None, bid_amount: float | None = None, roas_average_floor: float | None = None, portfolio_bid_strategy_id: str | None = None, ) -> str: """Create a standalone campaign + Creates a campaign WITHOUT its first ad set / ad, on the platform of the given + `accountId`. Ad sets join it later via `existingCampaignId` on the create endpoints. + Platform notes: on Meta a budget here is campaign-level (CBO) by definition; omit it + for ABO (each ad set carries its own budget), and `specialAdCategories` is Meta-only + (400 elsewhere); `bidStrategy` is Meta and Google (400 elsewhere), and Google also + accepts `portfolioBidStrategyId` instead. Google, X and OpenAI require a budget + (422 without one; OpenAI accepts daily or lifetime, Google only + `budgetType: daily`). On OpenAI `goal` sets the campaign objective, and + `conversions` needs an active standard conversion event on the account. LinkedIn creates the + campaign GROUP (our campaign level) and rejects a budget, which lives on the + campaign (ad set) level there; it comes back `status: DRAFT`. Created `PAUSED` + (TikTok `DISABLE`) unless `status: ACTIVE` where the platform supports it. + + **Idempotency:** send an ... + + Platforms: meta, google, linkedin, tiktok, x, pinterest, openai + Args: account_id: Zernio SocialAccount id (posting or ads variant); its platform decides where the campaign is created. (required) ad_account_id: Platform ad account id (Meta act_, Google customer id, LinkedIn account id, ...). (required) @@ -4398,10 +5561,37 @@ def ad_campaigns_create_ad_campaign( ) ) def ad_campaigns_update_ad_campaign_status( - campaign_id: str, status: str, platform: str + campaign_id: str, + status: Literal["active", "paused"], + platform: Literal[ + "facebook", + "instagram", + "tiktok", + "linkedin", + "pinterest", + "google", + "twitter", + "openai", + ], ) -> str: """Pause or resume a campaign + Writes the campaign's own on/off switch and nothing else, on every platform (Meta, TikTok, + Google, LinkedIn campaign group, Pinterest, X, ChatGPT (OpenAI)). Its ad sets and ads keep + their own switches: pausing stops their delivery through the campaign, and resuming lets + each of them deliver again only if its own switch is on. An ad set or ad you paused + individually stays paused; resume it with PUT /v1/ads/ad-sets/{adSetId}/status or + PUT /v1/ads/{adId}/status. See the Status model in the Ad Campaigns tag. + + **Live read, then write.** The campaign's switch is read from the platform first. When that + live read shows it already in the requested state nothing is written (`updated: 0`, + `skipped: 1`, with the reason). Otherwise the switch is written (`updated: 1`), read back and + stored, and the delivery status of the ads under it (up to 20) is re-read and stored, so an + immediate GET returns what the platform now reports. A stored switch never skips a write, and + when the platform cannot be ... + + Platforms: meta, google, tiktok, linkedin, pinterest, x, openai + Args: campaign_id: Platform campaign ID (required) status: (required) @@ -4425,7 +5615,7 @@ def ad_campaigns_update_ad_campaign_status( ) def ad_campaigns_get_campaign_ad_schedule( campaign_id: str, - platform: str | None = None, + platform: Literal["google"] | None = None, include_performance: bool | None = None, window_days: int = 30, from_date: str | None = None, @@ -4433,6 +5623,22 @@ def ad_campaigns_get_campaign_ad_schedule( ) -> str: """Read a campaign's ad schedule (dayparting) + The windows a Google campaign serves in, with the bid modifier on each, plus the + criterion ids Google minted for them. + + An EMPTY `schedule` is meaningful and is not a failed lookup: Google has no + \"all day\" criterion, so a campaign with no ad schedule serves around the clock. + `servesAroundTheClock` states that explicitly. + + Set `includePerformance=true` to also get delivery split by day of week and by hour, + which is the evidence for deciding what the schedule should be. It is one extra + Google call segmented by both dimensions at once, so the two views always agree. + + Google Ads only. The response carries `cachedAt` and `stale`, set when a + quota-exhausted call falls back to the last-good copy instead of a live read. + + Platforms: google + Args: campaign_id: Numeric Google platform campaign id. (required) platform: Disambiguates the campaign id when the connection spans platforms. @@ -4467,6 +5673,27 @@ def ad_campaigns_update_campaign_ad_schedule( ) -> str: """Replace a campaign's ad schedule (dayparting) + Replaces the campaign's whole ad schedule with the windows you send. This is a + REPLACE, not a merge: windows you leave out stop serving. + + Send `schedule: []` to clear dayparting, which returns the campaign to serving around + the clock. + + Google rules enforced here, so you get a named field instead of a criterion error: + at most 6 windows per day, a window must end after it starts, windows on the same day + may not overlap, `endHour` 24 is midnight and cannot carry minutes, and minutes are + quarter-hours only (0, 15, 30, 45). `bidModifier` is 0.1-10.0; Google's 0 means + \"off\" for devices only, so a window is switched off by leaving it out. + + Windows are half-open (Google is exclusive of the end minute), so 09:00-12:00 and + 12:00-17:00 on the same day are adjacent and both valid. + + Google cannot edit an ad schedule in place (every AdScheduleInfo field is prohibited on + update), so this removes the live criteria and creates the new ones in a single atomic + mutate. The response is read back from ... + + Platforms: google + Args: campaign_id: Numeric Google platform campaign id. (required) schedule: The complete set of windows. Required, so clearing the schedule is always deliberate rather than an omission. (required)""" @@ -4490,12 +5717,28 @@ def ad_campaigns_update_campaign_ad_schedule( def ad_campaigns_get_campaign_bidding( campaign_id: str, account_id: str, - platform: str, + platform: Literal["google"], ad_account_id: str | None = None, customer_id: str | None = None, ) -> str: """Read a campaign's current bidding + Read of the campaign's bidding strategy on Google, cached for the quota window, for + pre-filling the bid strategy block before a PUT to /v1/ads/campaigns/{campaignId}. + Google Ads only; `platform` is required and rejected when it is anything else, since + a `campaignId` is not globally unique. The response carries `cachedAt` and `stale`, + set when a quota-exhausted call falls back to the last-good copy instead of a live + read. + + Maps Google's bidding strategy onto the same triplet PUT accepts: `LOWEST_COST_WITHOUT_CAP` + (Maximize Conversions, no target), `COST_CAP` + `bidAmount` (Target CPA), `LOWEST_COST_WITH_MIN_ROAS` + + `roasAverageFloor` (Target ROAS), `LOWEST_COST_WITH_BID_CAP` + `bidAmount` (Maximize Clicks with + a CPC ceiling). A campaign on a portfolio strategy returns `portfolio` (id + name) and + `bidSpec.portfolioBidStrategyId` instead of the triplet. Anything else (Manual CPC, Target + Impression Share, ...) returns `bidSpec: null`; show `biddingStrategyType` as-is. + + Platforms: google + Args: campaign_id: Numeric Google platform campaign id. (required) account_id: Zernio Google Ads SocialAccount id: resolves the customer id + refresh token. (required) @@ -4528,6 +5771,15 @@ def ad_campaigns_get_ad_campaign_details( ) -> str: """Get live campaign details + Reads one campaign live from Meta, returned verbatim, so a caller that knows a + campaign id no longer has to page `GET /v1/ads/campaigns` to find it. The default + projection covers name, status, objective, buying type, bid strategy, budgets, + spend cap, schedule and `issues_info`. `fields` is a raw-passthrough override; + unknown fields return Meta's 400 verbatim. A campaign the resolved connection + cannot see comes back as Meta's own 400, not a 404. + + Platforms: meta + Args: campaign_id: Meta campaign id (platformCampaignId). (required) account_id: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token. (required) @@ -4551,9 +5803,15 @@ def ad_campaigns_get_ad_campaign_details( ) def ad_campaigns_update_ad_campaign( campaign_id: str, - platform: str, + platform: Literal["facebook", "instagram", "google"], account_id: str | None = None, - bid_strategy: str | None = None, + bid_strategy: Literal[ + "LOWEST_COST_WITHOUT_CAP", + "LOWEST_COST_WITH_BID_CAP", + "COST_CAP", + "LOWEST_COST_WITH_MIN_ROAS", + ] + | None = None, bid_amount: float | None = None, roas_average_floor: float | None = None, portfolio_bid_strategy_id: str | None = None, @@ -4564,6 +5822,29 @@ def ad_campaigns_update_ad_campaign( ) -> str: """Update a campaign + Campaign-level edits. Send at least one of `budget`, `bidStrategy`, + `portfolioBidStrategyId`, `name` or `platformSpecificData`. An unsupported + field is always an error, never a silent drop. + + | Body field | Meta | Google | Others | + |---|---|---|---| + | `bidStrategy` | Yes | Yes | 501 | + | `bidAmount`, `roasAverageFloor` | 400 (ad-set level) | Yes | 400 | + | `portfolioBidStrategyId` | 400 | Yes | 400 | + | `budget` (CBO; ABO returns 409) | Yes | Daily only | OpenAI: daily or lifetime; others 501 | + | `name` | Yes | 501 | 501 | + | `platformSpecificData.spendCap` | Yes | 400 | 400 | + | `accountId` (empty campaigns) | Yes | - | - | + + Meta budget edits check the live campaign budget, so an older local ABO stamp + cannot block a CBO campaign. A successful edit repairs local ad budget fields. + A live ABO campaign still returns 409 with the ad-set budget endpoint. + + On Google: `LOWEST_COST_WITHOUT_CAP` = Maximize Conversions, `COST_CAP` + + `bidAmount` = Target CPA, `LOWEST_COST_WITH_MIN_ROAS` + ... + + Platforms: meta, google, tiktok, linkedin, pinterest, x, openai + Args: campaign_id: Platform campaign ID (required) platform: Required: platform campaign IDs are not globally unique. (required) @@ -4604,10 +5885,26 @@ def ad_campaigns_update_ad_campaign( ) ) def ad_campaigns_delete_ad_campaign( - campaign_id: str, platform: str, account_id: str | None = None + campaign_id: str, + platform: Literal["facebook", "instagram", "google"], + account_id: str | None = None, ) -> str: """Delete a campaign + Deletes the whole campaign on the platform, cascading to its ad sets + and ads. Locally, all Ad documents for this campaign are marked + `status: cancelled`. + + **Empty campaigns.** A campaign with zero ads has no local Ad documents + to resolve, so it is invisible to `/v1/ads/tree` and this endpoint would + 404. That state is produced by the two-step create flow (campaign, then + ads via `existingCampaignId`) whenever Meta rejects the ad step. To + delete such a shell, send `accountId` in the body: we skip the local + lookup entirely and forward the delete to Meta. `accountId` is ignored + when the campaign does have ads. + + Platforms: meta, google, tiktok, linkedin, pinterest, x, openai + Args: campaign_id: Platform campaign ID (required) platform: (required) @@ -4630,10 +5927,33 @@ def ad_campaigns_delete_ad_campaign( ) ) def ad_campaigns_list_campaign_negative_keywords( - campaign_id: str, platform: str | None = None + campaign_id: str, + platform: Literal[ + "facebook", + "instagram", + "tiktok", + "linkedin", + "pinterest", + "google", + "twitter", + "openai", + ] + | None = None, ) -> str: """List campaign-level negative keywords + Returns the campaign-level negative keywords (`campaign_criterion.negative`), + distinct from the ad-group-level negatives under `GET /v1/ads/keywords`. Cached + for the quota window (not synced to Postgres), and gated by the shared Google + Ads operations budget like every other on-demand Google surface. The response + carries `cachedAt` and `stale`, set when a quota-exhausted call falls back to + the last-good copy instead of a live read. + + The platform is always discovered from the campaign itself; a non-Google + campaign returns 501 rather than 404, whether or not `platform` was passed. + + Platforms: google + Args: campaign_id: Platform campaign ID (required) platform: Optional and NOT authoritative: the resolved campaign's own platform decides 200 vs 501, never this hint.""" @@ -4655,10 +5975,32 @@ def ad_campaigns_list_campaign_negative_keywords( ) ) def ad_campaigns_replace_campaign_negative_keywords( - campaign_id: str, keywords: list[Any] | None, platform: str | None = None + campaign_id: str, + keywords: list[Any] | None, + platform: Literal[ + "facebook", + "instagram", + "tiktok", + "linkedin", + "pinterest", + "google", + "twitter", + "openai", + ] + | None = None, ) -> str: """Replace campaign-level negative keywords + Replaces the FULL set of campaign-level negative keywords (C.270): the desired + list is diffed against what Google already has, and the difference is applied + as one `create`/`remove` mutate. Send an empty array to clear every campaign + negative. + + The platform is always discovered from the campaign itself; a non-Google + campaign returns 501 rather than 404, whether or not `platform` was sent. + + Platforms: google + Args: campaign_id: Platform campaign ID (required) platform: Optional and NOT authoritative: the resolved campaign's own platform decides 200 vs 501, never this hint. @@ -4681,10 +6023,19 @@ def ad_campaigns_replace_campaign_negative_keywords( ) ) def ad_campaigns_bulk_update_ad_campaign_status( - status: str, campaigns: list[dict[str, Any]] | None + status: Literal["active", "paused"], campaigns: list[dict[str, Any]] | None ) -> str: """Pause or resume many campaigns + Process up to 50 campaigns in one call. Each campaign is updated + concurrently and the response contains a per-campaign result so a + single bad row does not fail the whole batch. Each campaign is read, + written and re-read exactly as PUT /v1/ads/campaigns/{campaignId}/status + describes: only the campaign's own switch is written, never its ad sets' + or ads'. `updated` / `skipped` count campaigns. + + Platforms: meta, google, tiktok, linkedin, pinterest, x + Args: status: (required) campaigns: (required)""" @@ -4707,18 +6058,40 @@ def ad_campaigns_bulk_update_ad_campaign_status( ) def ad_campaigns_duplicate_ad_campaign( campaign_id: str, - platform: str, + platform: Literal["facebook", "instagram", "tiktok", "linkedin"], deep_copy: bool = True, - status_option: str = "PAUSED", + status_option: Literal["ACTIVE", "PAUSED", "INHERITED_FROM_SOURCE"] = "PAUSED", start_time: str | None = None, end_time: str | None = None, - rename_strategy: str | None = None, + rename_strategy: Literal["DEEP_RENAME", "ONLY_TOP_LEVEL_RENAME", "NO_RENAME"] + | None = None, rename_prefix: str | None = None, rename_suffix: str | None = None, sync_after: bool = True, ) -> str: """Duplicate a campaign + Duplicates a campaign, including its ad sets, ads, creatives, and + targeting by default (`deepCopy: true`). The copy is created paused + so callers can review before launching. + + Per-platform implementation: + - **Meta** uses the native `POST /{campaign-id}/copies` endpoint. + - **TikTok** has no native copy primitive; Zernio walks the source + graph (`/v2/campaign/get/`, `/v2/adgroup/get/`, `/v2/ad/get/`) and + recreates each entity via the corresponding `/create/` endpoints, + carrying over budget / targeting / bid_type / bid_price / + deep_bid_type / creative fields. Spark Ad linkage (`tiktok_item_id`) + is preserved. + - **LinkedIn** has no native copy primitive; Zernio walks the source + CampaignGroup → Campaigns → Creatives and recreates each entity, + carrying over `type` / `costType` / `unitCost` / + `optimizationTargetType` / `creativeSelection` / `objectiveType` / + `format` / `dailyBudget` / `totalBudget` / `targetingCriteria` / + `runSchedule` and every Creative's `content` object ... + + Platforms: meta, tiktok, linkedin + Args: campaign_id: Source platform campaign ID (required) platform: (required) @@ -4757,10 +6130,24 @@ def ad_campaigns_duplicate_ad_campaign( ) ) def ad_campaigns_get_campaign_targeting( - campaign_id: str, platform: str | None = None + campaign_id: str, platform: Literal["google"] | None = None ) -> str: """Read a Google campaign's device, location, and language targeting + Google Ads compliance requires geo, language, budget, and bidding targeting + set at creation to stay editable afterwards; this reads the campaign state + so an integrator can build an editor around it. Cached for the quota window + (10 minutes fresh, up to 7 days last-good), not always a live read. Google + only; every other platform returns 501. + + `devices` lists the device criteria the campaign carries, which depends on + its channel: Search campaigns have MOBILE, DESKTOP and TABLET, Display + campaigns also have CONNECTED_TV. `bidModifier` is Google's bid adjustment + for that device, `null` when it has none, and `0` when the device is + switched off; `included` is false for exactly that case. + + Platforms: google + Args: campaign_id: Google platform campaign ID (required) platform: Disambiguates when the same campaignId string exists on more than one connected platform.""" @@ -4782,10 +6169,30 @@ def ad_campaigns_get_campaign_targeting( ) ) def ad_campaigns_update_campaign_targeting( - campaign_id: str, platform: str, targeting: dict[str, Any] | None + campaign_id: str, platform: Literal["google"], targeting: dict[str, Any] | None ) -> str: """Edit a Google campaign's device, location, or language targeting + Google Ads compliance row M.10: geo and language targeting set at + creation must stay editable afterwards. Send at least one of `devices`, + `locations`, `languages`, `locationTargetingType`; each provided field REPLACES that field's + existing criteria on the campaign (a full set, not a delta). Fields left + out of the body are untouched. Google only; every other platform returns + 501. + + `devices` is the full set of device bid modifiers: a supported device you + leave out is switched off with a bid modifier of 0, since Google cannot + remove a device criterion. A device the campaign's channel does not carry, + and a set that switches every device off, both return 422. + + `locations` accepts the same shapes as campaign creation: a bare array of + ISO country codes, or an object with `countries`/`regions`/`cities`/`zips`/`metros` + key lists (`key` from GET /v1/ads/targeting/search?dimension=geo). Negative + (excluded) locations are left untouched by this endpoint. An empty location list + returns 400 instead ... + + Platforms: google + Args: campaign_id: Google platform campaign ID (required) platform: (required) @@ -4811,10 +6218,38 @@ def ad_campaigns_list_ad_sets( account_id: str | None = None, campaign_id: str | None = None, ad_set_id: str | None = None, - platform: str | None = None, + platform: Literal[ + "facebook", + "instagram", + "tiktok", + "linkedin", + "pinterest", + "google", + "twitter", + "openai", + ] + | None = None, ) -> str: """List ad sets + Ad sets (Google ad groups) synced for the connection, optionally + filtered by platform and campaignId. Reads the `ad_sets` table + directly, independent of the `ads` rollup GET /v1/ads/tree uses, so a + newly created standalone ad group with no ad yet (POST /v1/ads/ad-sets, + Google only) is visible here even though it is invisible in the tree + until an ad joins it via `adSetId` on POST /v1/ads/create. Returns at most 500 + rows, newest first. + + **Status freshness.** `status`, `configuredStatus`, `platformStatus`, `platformAdSetStatus` + and `platformCampaignStatus` are the values Zernio last stored. Background sync refreshes + them, typically within 15 to 60 minutes (Google up to about 3 hours), and ended or + long-paused objects may be refreshed less often. Zernio's own status writes re-read the + switches they change. A change made in the platform's own ads manager therefore shows up + here only after the next sync. Controllers that act on a switch should pass `live=true`, + which reads the switches ... + + Platforms: meta, google, tiktok, linkedin, pinterest, x + Args: account_id: Account ID campaign_id: Platform campaign ID @@ -4842,15 +6277,37 @@ def ad_campaigns_list_ad_sets( ) def ad_campaigns_create_ad_set( account_id: str, - platform: str, + platform: Literal[ + "facebook", + "instagram", + "tiktok", + "linkedin", + "pinterest", + "google", + "twitter", + "openai", + ], campaign_id: str, name: str, - status: str = "PAUSED", + status: Literal["ACTIVE", "PAUSED"] = "PAUSED", ad_account_id: str | None = None, customer_id: str | None = None, ) -> str: """Create a standalone ad group + Google Ads compliance row C.190: creates an ad group WITHOUT an ad, + under an existing campaign. Ads join it later via `adSetId` + on POST /v1/ads/create. Google only; every other platform returns 501. + + Created `PAUSED` unless `status: ACTIVE`. The new ad group has no ad + yet, so it will not appear in GET /v1/ads/tree (built purely from `ads` + rows) until one is added; use GET /v1/ads/ad-sets to see it in the + meantime. + + **Idempotency:** send an `Idempotency-Key` header to make retries safe. + + Platforms: google + Args: account_id: Zernio SocialAccount id owning the Google Ads connection. (required) platform: Only "google" is implemented today; every other value returns 501. (required) @@ -4884,19 +6341,34 @@ def ad_campaigns_create_ad_set( ) def ad_campaigns_duplicate_ad_set( ad_set_id: str, - platform: str, + platform: Literal["facebook", "instagram"], campaign_id: str | None = None, deep_copy: bool = True, - status_option: str = "PAUSED", + status_option: Literal["ACTIVE", "PAUSED", "INHERITED_FROM_SOURCE"] = "PAUSED", start_time: str | None = None, end_time: str | None = None, - rename_strategy: str | None = None, + rename_strategy: Literal["DEEP_RENAME", "ONLY_TOP_LEVEL_RENAME", "NO_RENAME"] + | None = None, rename_prefix: str | None = None, rename_suffix: str | None = None, sync_after: bool = True, ) -> str: """Duplicate an ad set + Duplicates an ad set. The copy is created paused so callers can review before launching. + `campaignId` retargets the copy into another campaign; omitted = the source's own campaign. + + Meta: ads and creatives are included by default (`deepCopy: true`) via Meta's native + `POST /{adset-id}/copies`; the new hierarchy materializes asynchronously and sync discovery + is triggered automatically (`syncAfter: false` to skip). + + TikTok: the ad group is read and recreated under the campaign with its targeting, bidding, + budget and schedule (start reset to now); `deepCopy: true` recreates its ads too (default + false). `startTime`, `endTime` and `renameStrategy` are ignored and `statusOption` must be + PAUSED or absent. The copy appears on the next discovery sync. + + Platforms: meta, tiktok + Args: ad_set_id: Source platform ad set ID (required) platform: (required) @@ -4939,8 +6411,9 @@ def ad_campaigns_duplicate_ad_set( def ad_campaigns_duplicate_ad( ad_id: str, ad_set_id: str | None = None, - status_option: str = "PAUSED", - rename_strategy: str | None = None, + status_option: Literal["ACTIVE", "PAUSED", "INHERITED_FROM_SOURCE"] = "PAUSED", + rename_strategy: Literal["DEEP_RENAME", "ONLY_TOP_LEVEL_RENAME", "NO_RENAME"] + | None = None, rename_prefix: str | None = None, rename_suffix: str | None = None, sync_after: bool = True, @@ -4948,6 +6421,19 @@ def ad_campaigns_duplicate_ad( ) -> str: """Duplicate an ad + Duplicates a single ad via Meta's native `POST /{ad-id}/copies`. The copy is created + paused. `adSetId` retargets the copy into another ad set; omitted = the source's own ad + set. Accepts the Zernio ad id or the platform ad id. Sync discovery is triggered + automatically (`syncAfter: false` to skip). Creative settings returned by Meta, + including explicit promotion metadata and creativeFeatures, are preserved when the + native copy requires a creative rebuild. Metadata Meta does not return cannot be recovered. + When Meta refuses the native copy with its capability error (code 3), which happens for + some creatives built by other tools, the ad is rebuilt instead: a new creative from the + source's returned spec and a new ad in the target ad set, carrying the source name, + status option, rename options and tracking specs. + + Platforms: meta + Args: ad_id: Zernio ad ID or platform ad ID (required) ad_set_id: Destination platform ad set id (defaults to the source's ad set) @@ -4986,6 +6472,14 @@ def ad_campaigns_get_ad_set_details( ) -> str: """Get live ad-set details + Reads the ad set live from Meta, returned verbatim. The default projection includes + `learning_stage_info` (learning-phase status: LEARNING / SUCCESS / FAIL / WAIVING; Meta + omits its `status` key on paused ad sets), delivery settings, budgets, schedule and + targeting. `fields` is a raw-passthrough override; unknown fields return Meta's 400 + verbatim. + + Platforms: meta + Args: ad_set_id: Meta ad set id (platformAdSetId). (required) account_id: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token. (required) @@ -5009,11 +6503,26 @@ def ad_campaigns_get_ad_set_details( ) def ad_campaigns_update_ad_set( ad_set_id: str, - platform: str, + platform: Literal[ + "facebook", + "instagram", + "tiktok", + "linkedin", + "pinterest", + "google", + "twitter", + "openai", + ], budget: dict[str, Any] | None = None, - status: str | None = None, + status: Literal["active", "paused"] | None = None, name: str | None = None, - bid_strategy: str | None = None, + bid_strategy: Literal[ + "LOWEST_COST_WITHOUT_CAP", + "LOWEST_COST_WITH_BID_CAP", + "COST_CAP", + "LOWEST_COST_WITH_MIN_ROAS", + ] + | None = None, smart_targeting: dict[str, Any] | None = None, bid_amount: float | None = None, roas_average_floor: float | None = None, @@ -5023,6 +6532,25 @@ def ad_campaigns_update_ad_set( ) -> str: """Update an ad set + Ad-set-level writes. Use this for ABO budget updates, ad-set-scoped + pause/resume, bid-strategy edits, Meta value-rule-set attach/detach, and + Meta-only post-launch delivery settings via `platformSpecificData`. At + least one updatable field is required. + + Value rule sets (Meta only, see `/v1/ads/value-rule-sets`): + - ATTACH or REPLACE: send `valueRuleSetId`. Attachment is driven by the id's + presence, so `valueRulesApplied: true` is optional. Sending a different id + replaces the previous association; there is no separate replace call. + - DETACH: send `valueRulesApplied: false` and OMIT `valueRuleSetId`. + - Sending `valueRulesApplied: false` TOGETHER with `valueRuleSetId` returns 400 + `mutually_exclusive_fields`. This is deliberate: Meta attaches the rule set + whenever `value_rule_set_id` is present, even with `value_rules_applied` false, + so echoing stored state while asking to detach would silently keep the bid + adjustments live. + - Eligibility: only ad sets on ... + + Platforms: meta, google, tiktok, linkedin, pinterest, x, openai + Args: ad_set_id: Platform ad set ID (required) platform: (required) @@ -5094,6 +6622,17 @@ def ad_campaigns_update_ad_set( def ad_campaigns_delete_ad_set(ad_set_id: str) -> str: """Delete an ad set + Deletes the ad set on the platform, cascading to its ads only (never the + campaign). Locally, every Ad document under the ad set is marked + `status: cancelled`. + + Delete is soft on platforms that have no hard delete: LinkedIn moves the + campaign to `PENDING_DELETION`, Pinterest archives the ad group, and X + soft-flags the line item. Google removes the ad group. All remain readable + for reporting. + + Platforms: meta, google, tiktok, linkedin, pinterest, x + Args: ad_set_id: Platform ad set ID (required)""" client = _get_client() @@ -5112,10 +6651,42 @@ def ad_campaigns_delete_ad_set(ad_set_id: str) -> str: ) ) def ad_campaigns_update_ad_set_status( - ad_set_id: str, status: str, platform: str + ad_set_id: str, + status: Literal["active", "paused"], + platform: Literal[ + "facebook", + "instagram", + "tiktok", + "linkedin", + "pinterest", + "google", + "twitter", + "openai", + ], ) -> str: """Pause or resume a single ad set + Ad-set-scoped pause/resume (doesn't touch sibling ad sets). Thin wrapper + over PUT /v1/ads/ad-sets/{adSetId} for callers that only want the + status toggle and prefer a symmetric URL to + /v1/ads/campaigns/{campaignId}/status. + + Writes the ad set's own on/off switch and nothing else, on every platform + (Meta `configured_status`, TikTok ad group `operation_status`, Google ad + group status, LinkedIn campaign, Pinterest ad group, X line item, ChatGPT + (OpenAI) ad group). Its ads keep their own switches: an ad you paused + individually stays paused when the ad set resumes. The campaign above is + not touched either, so an ad set resumed under a paused campaign reads + `status: paused` until the campaign is resumed too. See the Status model + in the Ad Campaigns tag. + + **Live read, then write.** The ad set's switch is read from the platform + first. When that live read shows it already in the requested state + nothing is written (`updated: 0`, `skipped: 1`, with the reason). + Otherwise the switch is written ... + + Platforms: meta, google, tiktok, linkedin, pinterest, x, openai + Args: ad_set_id: Platform ad set ID (required) status: (required) @@ -5140,9 +6711,28 @@ def ad_campaigns_update_ad_set_status( def ad_campaigns_get_ad_tree( page: int = 1, limit: int = 20, - source: str = "all", - platform: str | None = None, - status: str | None = None, + source: Literal["zernio", "all"] = "all", + platform: Literal[ + "facebook", + "instagram", + "tiktok", + "linkedin", + "pinterest", + "google", + "twitter", + "openai", + ] + | None = None, + status: Literal[ + "active", + "paused", + "pending_review", + "rejected", + "completed", + "cancelled", + "error", + ] + | None = None, ad_account_id: str | None = None, page_id: str | None = None, account_id: str | None = None, @@ -5154,12 +6744,29 @@ def ad_campaigns_get_ad_tree( to_date: str | None = None, has_delivery: bool | None = None, min_spend: float | None = None, - sort: str = "newest", + sort: Literal["newest", "oldest", "spend_desc", "spend_asc"] = "newest", time_increment: int | None = None, - daily_level: str = "campaign", + daily_level: Literal["campaign", "adset", "ad"] = "campaign", ) -> str: """Get campaign tree + Returns a nested Campaign > Ad Set > Ad hierarchy with rolled-up metrics at each level. + Uses a two-stage aggregation: ads are grouped into ad sets, then ad sets into campaigns. + Metrics are computed over an optional date range, then rolled up from ad level to ad set + and campaign levels. Pagination is at the campaign level. Ads without a campaign or ad set + ID are grouped into synthetic \"Ungrouped\" buckets. + If no date range is provided, defaults to the last 90 days. Date range is capped at 730 days max. + + Pass `timeIncrement=1` to also get a daily breakdown: each node gains a `daily[]` array of + per-day metrics (same fields as the aggregated `metrics`) in the same call. Use `dailyLevel` + (`campaign` default, or `adset` / `ad`) to choose which levels carry the series. This replaces + calling the tree once per day for per-campaign daily trends. + + **Deleted objects stay in the tree.** Deleting an ad or a campaign is a soft delete: the Ad + documents move to `status: cancelled` and are kept ... + + Platforms: meta, google, tiktok, linkedin, pinterest, x + Args: page: Page number limit: Campaigns per page @@ -5220,10 +6827,38 @@ def ad_campaigns_get_ads_timeline( ad_account_id: str | None = None, from_date: str | None = None, to_date: str | None = None, - platform: str | None = None, + platform: Literal[ + "facebook", + "instagram", + "tiktok", + "linkedin", + "pinterest", + "google", + "twitter", + "openai", + ] + | None = None, ) -> str: """Get daily account metrics + Returns daily aggregate metrics across all ads in a SocialAccount as a single + time series, one row per calendar day in the requested range. Use this for + dashboards that draw a daily-spend or daily-conversions chart, instead of + calling `/v1/ads/tree` once per day. + + `accountId` is required. The lookup is sibling-expanded so passing the `metaads` + ID also includes ads under the linked `facebook` / `instagram` posting account + (and vice-versa), the same convention as `/v1/ads/tree` and `/v1/ads`. + + Date range defaults to the last 90 days. Capped at 730 days. Ranges older + than the ingested history return a `202` immediately with the covered part + and `backfillPending: true` while the rest is backfilled in the background; + repeat the request shortly until it returns 200 with full data. + + With adAccountId set to a Google customer id this is the customer-level performance report (clicks, cost, impressions, conversions, all conversions per day). + + Platforms: meta, google, tiktok, linkedin, pinterest, x + Args: account_id: Account ID. Sibling-expanded to its linked posting↔ads pair. (required) ad_account_id: Optional platform-native ad account ID (e.g. Meta `act_…`, TikTok advertiser ID). Use when the connection wraps multiple platform ad accounts and the chart should show one only. Note: rows ingested before 2026-05-13 don't carry this column; the recurring 7-day re-sync repopulates them naturally. @@ -5254,6 +6889,23 @@ def ad_campaigns_get_ads_timeline( def ad_campaigns_get_ad(ad_id: str) -> str: """Get ad details + Returns an ad with its creative, targeting, status, and performance metrics. + Google Search ads include current creative.headlines, creative.descriptions and creative.finalUrls, + preserving pinnedField. Top-level cachedAt and stale report cache freshness. Google mutations invalidate this read. + RSA enrichment requires a stored advertisingChannelType of SEARCH. Ads with an unknown or other channel + return their stored details without a Google read. If RSA enrichment fails, the stored ad is returned + with HTTP 200 and without cache metadata. + + The `{adId}` path segment accepts any identifier dialect Zernio indexes for the ad: + - the Zernio internal `_id` (24-char hex) + - Meta's numeric `platformAdId` (the value shipped in `comment.received` webhooks as `comment.ad.id`) + - the creative's `effective_object_story_id` (`{pageId}_{postId}` shape, Facebook side) + - the creative's `effective_instagram_media_id` (Instagram side) + + Any of the four resolve to the same ad. Caller doesn't need a translation ... + + Platforms: meta, google, tiktok, linkedin, pinterest, x + Args: ad_id: Zernio `_id` (hex), Meta `platformAdId` (numeric), or one of the creative's effective story/media IDs. See description for details. (required)""" @@ -5279,7 +6931,7 @@ def ad_campaigns_update_ad( final_urls: list[str] | None = None, asset_group: dict[str, Any] | None = None, demand_gen: dict[str, Any] | None = None, - status: str | None = None, + status: Literal["active", "paused"] | None = None, budget: dict[str, Any] | None = None, targeting: dict[str, Any] | None = None, creative: dict[str, Any] | None = None, @@ -5287,6 +6939,25 @@ def ad_campaigns_update_ad( ) -> str: """Update ad + Patch one or more fields on an ad. Status, budget, targeting, and creative changes + are propagated to the platform. + + Per-platform support: + - **Meta** (Facebook + Instagram): all fields supported. + - **TikTok**: status, budget, `name` (renames the ad), targeting (via `/v2/adgroup/update/`), and creative + (via `/v2/ad/update/` patch-style: `headline` is ignored, `body` becomes `ad_text`). + - **Google**: status, budget, KEYWORD edits via `targeting.keywords` / + `targeting.negativeKeywords`, DEVICE bid adjustments via `targeting.devices`, + LOCATION edits via `targeting.locations` (or the equivalent top-level + `targeting.countries` / `regions` / `cities` / `zips` / `metros`), and LANGUAGE + edits via `targeting.languages`. + Each list you send becomes the FULL new set of its kind (criteria not in the + list are removed, except devices, which Google cannot remove and which are + switched off with a bid modifier of 0 instead); a kind left out is untouched. + Any other `targeting` field ... + + Platforms: meta, google, tiktok, linkedin, pinterest, x + Args: ad_id: (required) headlines: Google Search and Display only. Replaces the complete headline list. Search takes 3-15, Display 1-5 and rejects pinnedField; the count is checked once the ad's channel is known. No padding or truncation on update. @@ -5365,6 +7036,10 @@ def ad_campaigns_update_ad( def ad_campaigns_delete_ad(ad_id: str) -> str: """Cancel an ad + Cancels the ad on the platform and marks it as cancelled in the database. The ad is preserved for history. OpenAI Ads has no delete API; the ad is archived instead (a terminal state, the closest equivalent). + + Platforms: meta, google, tiktok, linkedin, pinterest, x + Args: ad_id: (required)""" client = _get_client() @@ -5382,9 +7057,32 @@ def ad_campaigns_delete_ad(ad_id: str) -> str: openWorldHint=True, ) ) - def ad_campaigns_update_ad_status(ad_id: str, status: str) -> str: + def ad_campaigns_update_ad_status( + ad_id: str, status: Literal["active", "paused"] + ) -> str: """Pause or resume a single ad + Ad-scoped pause/resume: flips ONLY this ad's own switch (Meta + `configured_status`, TikTok `operation_status`, Google ad group ad + status, LinkedIn creative, Pinterest ad), never its parent ad set or + campaign, so sibling ads keep running. X is the exception: its smallest + switch is the line item. Thin wrapper over the `status` field of + PUT /v1/ads/{adId}, for callers that want a URL symmetric to + /v1/ads/campaigns/{campaignId}/status and /v1/ads/ad-sets/{adSetId}/status. + + The ad's own switch is independent of its delivery status. An ad paused + only because its campaign or ad set is off (`status: paused`, + `configuredStatus: ACTIVE`) can still be switched off here, and + switching an ad on under a paused campaign leaves it `paused` until the + campaign is resumed. After the write the switch is read back from the + platform and returned as `configuredStatus`, together with the + resulting delivery `status`. + + `{adId}` accepts the same identifier dialects as GET/PUT /v1/ads/{adId} + (Zernio hex `_id`, ... + + Platforms: meta, google, tiktok, linkedin, pinterest, x + Args: ad_id: Zernio `_id` (hex), Meta `platformAdId` (numeric), or one of the creative's effective story/media IDs. (required) status: (required)""" @@ -5411,6 +7109,10 @@ def ad_campaigns_list_campaign_assets( ) -> str: """List campaign assets + Lists directly attached Google assets. Fresh reads are cached for 10 minutes; exhausted quota may return the last successful read with stale=true. Inherited assets are not included. + + Platforms: google + Args: campaign_id: Numeric Google platform id. (required) account_id: (required) @@ -5447,6 +7149,10 @@ def ad_campaigns_attach_campaign_assets( ) -> str: """Attach campaign assets + Creates and attaches sitelinks, callouts and structured snippets in one Google mutation. + + Platforms: google + Args: campaign_id: Numeric Google platform id. (required) account_id: Zernio Google Ads connection id. (required) @@ -5487,6 +7193,10 @@ def ad_campaigns_update_campaign_assets( ) -> str: """Update campaign assets + Edits existing Google assets in place. Send updates with assetResourceName and the fields to change. An asset is shared: changes affect every attachment using it. Omitted fields stay unchanged. The operation consumes the Google operations budget and invalidates affected cached lists. + + Platforms: google + Args: campaign_id: Numeric Google platform id. (required) account_id: Zernio Google Ads connection id. (required) @@ -5524,6 +7234,10 @@ def ad_campaigns_remove_campaign_assets( ) -> str: """Remove campaign assets + Removes the specified attachments only. Google assets cannot be deleted. Other attachments remain. assetResourceNames is retained for compatibility. + + Platforms: google + Args: campaign_id: Numeric Google platform id. (required) account_id: Zernio Google Ads connection id. (required) @@ -5561,6 +7275,10 @@ def ad_campaigns_list_ad_group_assets( ) -> str: """List ad-group assets + Lists directly attached Google assets. Fresh reads are cached for 10 minutes; exhausted quota may return the last successful read with stale=true. Inherited assets are not included. + + Platforms: google + Args: ad_set_id: Numeric Google platform id. (required) account_id: (required) @@ -5597,6 +7315,10 @@ def ad_campaigns_attach_ad_group_assets( ) -> str: """Attach ad-group assets + Creates and attaches sitelinks, callouts and structured snippets in one Google mutation. + + Platforms: google + Args: ad_set_id: Numeric Google platform id. (required) account_id: Zernio Google Ads connection id. (required) @@ -5637,6 +7359,10 @@ def ad_campaigns_update_ad_group_assets( ) -> str: """Update ad-group assets + Edits existing Google assets in place. Send updates with assetResourceName and the fields to change. An asset is shared: changes affect every attachment using it. Omitted fields stay unchanged. The operation consumes the Google operations budget and invalidates affected cached lists. + + Platforms: google + Args: ad_set_id: Numeric Google platform id. (required) account_id: Zernio Google Ads connection id. (required) @@ -5674,6 +7400,10 @@ def ad_campaigns_remove_ad_group_assets( ) -> str: """Remove ad-group assets + Removes the specified attachments only. Google assets cannot be deleted. Other attachments remain. assetResourceNames is retained for compatibility. + + Platforms: google + Args: ad_set_id: Numeric Google platform id. (required) account_id: Zernio Google Ads connection id. (required) @@ -5706,6 +7436,17 @@ def ad_campaigns_remove_ad_group_assets( def ad_campaigns_get_ad_review(ad_id: str) -> str: """Read the platform's review verdict for an ad + Reads the ad's review verdict from the platform now: whether it was approved, where it may + not deliver, and every rejection reason with TikTok's suggestion and the piece of content it + refers to. Read-only, so it works on a paused ad without re-enabling it. + + TikTok only (`/ad/review_info/`); every other platform returns 501. Use it alongside the ad's + `platformStatus`: TikTok reports `AD_STATUS_AUDIT` while the ad is in review and + `AD_STATUS_AD_PRE_ONLINE` once it passed and is about to deliver (both map to + `status: pending_review`); `AD_STATUS_AUDIT_DENY` maps to `rejected`. + + Platforms: tiktok + Args: ad_id: Zernio ad id (24-char hex) or the platform ad id. (required)""" client = _get_client() @@ -5724,10 +7465,25 @@ def ad_campaigns_get_ad_review(ad_id: str) -> str: ) ) def ad_campaigns_list_campaign_negative_keyword_lists( - campaign_id: str, platform: str | None = None + campaign_id: str, + platform: Literal[ + "facebook", + "instagram", + "tiktok", + "linkedin", + "pinterest", + "google", + "twitter", + "openai", + ] + | None = None, ) -> str: """List campaign negative lists + Returns shared negative keyword lists attached to the campaign, separate from campaign-level negative keywords. Google Ads shared negative keyword lists (shared_set type NEGATIVE_KEYWORDS). Reads are cached for 10 minutes; quota exhaustion may return the last successful result for up to 7 days with stale=true. Customer selection is limited to this connection and its account scope. + + Platforms: google + Args: campaign_id: (required) platform""" @@ -5749,10 +7505,26 @@ def ad_campaigns_list_campaign_negative_keyword_lists( ) ) def ad_campaigns_replace_campaign_negative_keyword_lists( - campaign_id: str, list_ids: list[str] | None, platform: str | None = None + campaign_id: str, + list_ids: list[str] | None, + platform: Literal[ + "facebook", + "instagram", + "tiktok", + "linkedin", + "pinterest", + "google", + "twitter", + "openai", + ] + | None = None, ) -> str: """Replace campaign negative lists + Sets the full desired set of shared negative keyword list associations on this campaign. Send listIds=[] to detach all negative keyword lists. Only campaign_shared_set links are changed; the lists and their keywords are preserved. Every list must belong to the campaign customer and have type NEGATIVE_KEYWORDS. + + Platforms: google + Args: campaign_id: (required) platform: Optional courtesy field. The resolved account or campaign determines support; other platforms return 501. @@ -5778,7 +7550,15 @@ def ad_campaigns_boost_post( account_id: str, ad_account_id: str, name: str, - goal: str, + goal: Literal[ + "engagement", + "traffic", + "awareness", + "video_views", + "lead_generation", + "conversions", + "app_promotion", + ], creative_features: dict[str, Any] | None = None, post_id: str | None = None, platform_post_id: str | None = None, @@ -5787,26 +7567,52 @@ def ad_campaigns_boost_post( ad_set_id: str | None = None, existing_campaign_id: str | None = None, identity_id: str | None = None, - identity_type: str | None = None, + identity_type: Literal["TT_USER", "CUSTOMIZED_USER", "BC_AUTH_TT"] + | None = None, budget_amount: float | None = None, - budget_type: str | None = None, + budget_type: Literal["daily", "lifetime"] | None = None, budget: dict[str, Any] | None = None, instagram_account_id: str | None = None, - destination_type: str | None = None, + destination_type: Literal[ + "INSTAGRAM_PROFILE", + "WEBSITE", + "ON_AD", + "MESSENGER", + "WHATSAPP", + "INSTAGRAM_DIRECT", + ] + | None = None, whatsapp_phone_number: str | None = None, currency: str | None = None, start_date: str | None = None, end_date: str | None = None, schedule: dict[str, Any] | None = None, targeting: dict[str, Any] | None = None, - location_targeting_type: str | None = None, + location_targeting_type: Literal["presence", "presence_or_interest"] + | None = None, raw_targeting: dict[str, Any] | None = None, - bid_strategy: str | None = None, + bid_strategy: Literal[ + "LOWEST_COST_WITHOUT_CAP", + "LOWEST_COST_WITH_BID_CAP", + "COST_CAP", + "LOWEST_COST_WITH_MIN_ROAS", + ] + | None = None, bid_amount: float | None = None, roas_average_floor: float | None = None, platform_specific_data: dict[str, Any] | None = None, tracking: dict[str, Any] | None = None, - special_ad_categories: list[str] | None = None, + special_ad_categories: list[ + Literal[ + "HOUSING", + "EMPLOYMENT", + "CREDIT", + "FINANCIAL_PRODUCTS_SERVICES", + "ISSUES_ELECTIONS_POLITICS", + "ONLINE_GAMBLING_AND_GAMING", + ] + ] + | None = None, special_ad_category_country: list[str] | None = None, regional_regulated_categories: list[str] | None = None, regional_regulation_identities: dict[str, Any] | None = None, @@ -5820,8 +7626,8 @@ def ad_campaigns_boost_post( dsa_beneficiary: str | None = None, dsa_payor: str | None = None, lead_gen_form_id: str | None = None, - status: str | None = None, - budget_level: str | None = None, + status: Literal["ACTIVE", "PAUSED"] | None = None, + budget_level: Literal["adset", "campaign"] | None = None, attribution_spec: list[dict[str, Any]] | None = None, bodies: list[str] | None = None, smart_targeting: dict[str, Any] | None = None, @@ -5829,6 +7635,30 @@ def ad_campaigns_boost_post( ) -> str: """Boost post as ad + Creates a paid ad from an existing published post, keeping the post's + engagement. By default it provisions the whole hierarchy (campaign, ad + set, ad). + + **Attach shape (Meta).** Send `adSetId` to put the ad under an EXISTING + ad set instead, so that ad set keeps its learning phase. It then owns + `budget`, `schedule` and `targeting`, and sending any of those alongside + `adSetId` is a 400 rather than a silent drop. `budget` is required only + without `adSetId`. + + `instagramAccountId`, `destinationType`, `whatsappPhoneNumber` and `adSetId` + are Meta-only and return 400 on other platforms. + + `accountId` may be a Facebook, Instagram or Meta ads (business login) + connection. A business-login connection has no posting account, so pass + the post as `platformPostId` (Facebook `pageId_postId` or an Instagram + media id); a Zernio `postId` is a 400 there. + + **Messaging boosts (Meta).** Use `goal: engagement` with + `callToAction: WHATSAPP_MESSAGE`, `MESSAGE_PAGE`, or `INSTAGRAM_MESSAGE`. + The CTA implies ... + + Platforms: meta, google, tiktok, linkedin, pinterest, x + Args: creative_features post_id: Zernio post ID (provide this or platformPostId) @@ -6060,6 +7890,10 @@ def ad_campaigns_boost_post( def ad_campaigns_list_google_asset_groups(campaign_id: str) -> str: """List Performance Max asset groups + Read Performance Max asset groups and their linked text, image and YouTube assets. campaignId is the platform campaign id returned by creation or the campaign list. The campaign must be visible to the caller. Uses a 10-minute cache, with the last successful response served as stale when Google quota is exhausted. Removed groups and asset links are excluded. Campaign-level brand assets on campaigns with brand guidelines enabled are not included. + + Platforms: google + Args: campaign_id: Google Ads campaign id. (required)""" client = _get_client() @@ -6086,13 +7920,17 @@ def ad_campaigns_create_google_asset_group( final_mobile_urls: list[str] | None = None, path1: str | None = None, path2: str | None = None, - status: str = "PAUSED", + status: Literal["ENABLED", "PAUSED"] = "PAUSED", assets: list[dict[str, Any]] | None = None, listing_group_filter: dict[str, Any] | None = None, validate_only: bool = False, ) -> str: """Create a Performance Max asset group + Add an asset group to an existing Performance Max campaign. The group, any new assets, their links and an optional listing-group tree are created in one atomic request, so Google checks the asset minimums (for non-retail campaigns) against the whole set. Created PAUSED unless status is ENABLED. validateOnly: true runs Google's validation without creating anything. + + Platforms: google + Args: campaign_id: Google Ads campaign id. (required) name: Unique within the campaign. (required) @@ -6135,6 +7973,10 @@ def ad_campaigns_get_google_asset_group( ) -> str: """Get a Performance Max asset group + One asset group with its linked assets, ad strength, primary status and listing-group tree. Uses a 10-minute cache, served stale when Google quota is exhausted; any write below clears it. + + Platforms: google + Args: campaign_id: Google Ads campaign id. (required) asset_group_id: Google asset group id. (required)""" @@ -6159,7 +8001,7 @@ def ad_campaigns_update_google_asset_group( campaign_id: str, asset_group_id: str, name: str | None = None, - status: str | None = None, + status: Literal["ENABLED", "PAUSED"] | None = None, final_urls: list[str] | None = None, final_mobile_urls: list[str] | None = None, path1: str | None = None, @@ -6168,6 +8010,10 @@ def ad_campaigns_update_google_asset_group( ) -> str: """Update a Performance Max asset group + Change the name, status (ENABLED or PAUSED), final URLs or display paths. Only the fields sent are written; null on path1 or path2 clears it. Change assets with the /assets endpoint and product targeting with /listing-group-filters. validateOnly: true validates without writing. + + Platforms: google + Args: campaign_id: Google Ads campaign id. (required) asset_group_id: Google asset group id. (required) @@ -6208,6 +8054,10 @@ def ad_campaigns_remove_google_asset_group( ) -> str: """Remove a Performance Max asset group + Removes the asset group on Google (status REMOVED, not reversible). Pass validateOnly=true to validate without removing. + + Platforms: google + Args: campaign_id: (required) asset_group_id: (required) @@ -6240,6 +8090,10 @@ def ad_campaigns_edit_google_asset_group_assets( ) -> str: """Link or unlink asset group assets + Link existing assets or new content to the asset group, and unlink assets, in one atomic request. Links are applied before unlinks, so swapping the last asset of a role does not trip Google's per-role minimum. Unlinking removes the link only; the asset stays in the account library. validateOnly: true validates without writing. + + Platforms: google + Args: campaign_id: Google Ads campaign id. (required) asset_group_id: Google asset group id. (required) @@ -6275,6 +8129,10 @@ def ad_campaigns_replace_google_listing_group_filters( ) -> str: """Replace an asset group's listing-group tree + Replace the product (listing-group) tree of a Performance Max retail asset group. The current tree is removed and the new one created in one atomic request. Read the current tree with GET on the asset group. Requires a campaign linked to Merchant Center; other campaigns return 400 LISTING_SOURCE_NOT_ALLOWED from Google. validateOnly: true validates without writing. + + Platforms: google + Args: campaign_id: Google Ads campaign id. (required) asset_group_id: Google asset group id. (required) @@ -6308,22 +8166,36 @@ def ad_campaigns_create_standalone_ad( ad_set_name: str | None = None, ad_name: str | None = None, tracking: dict[str, Any] | None = None, - goal: str | None = None, + goal: Literal[ + "engagement", + "traffic", + "awareness", + "video_views", + "lead_generation", + "lead_conversion", + "conversions", + "app_promotion", + "catalog_sales", + "page_likes", + "page_visits", + "job_applicants", + ] + | None = None, smart_targeting: dict[str, Any] | None = None, optimization_goal: str | None = None, billing_event: str | None = None, - buying_type: str = "AUCTION", + buying_type: Literal["AUCTION", "RESERVED"] = "AUCTION", rf_prediction_id: str | None = None, promotion: str | None = None, creative_features: dict[str, Any] | None = None, - multi_advertiser: str | None = None, - ai_disclosure: str | None = None, + multi_advertiser: Literal["OPT_IN", "OPT_OUT"] | None = None, + ai_disclosure: Literal["OPT_IN", "OPT_OUT"] | None = None, validate_only: bool | None = None, budget_amount: float | None = None, - budget_type: str | None = None, - status: str | None = None, - campaign_status: str | None = None, - budget_level: str = "adset", + budget_type: Literal["daily", "lifetime"] | None = None, + status: Literal["ACTIVE", "PAUSED"] | None = None, + campaign_status: Literal["ACTIVE", "PAUSED"] | None = None, + budget_level: Literal["adset", "campaign"] = "adset", currency: str | None = None, headline: str | None = None, long_headline: str | None = None, @@ -6332,7 +8204,43 @@ def ad_campaigns_create_standalone_ad( bodies: list[str] | None = None, headlines: list[str] | None = None, descriptions: list[str] | None = None, - call_to_action: str | None = None, + call_to_action: Literal[ + "LEARN_MORE", + "SHOP_NOW", + "SIGN_UP", + "BOOK_TRAVEL", + "CONTACT_US", + "DOWNLOAD", + "GET_OFFER", + "GET_QUOTE", + "SUBSCRIBE", + "WATCH_MORE", + "ADD_TO_CART", + "APPLY_NOW", + "BOOK_NOW", + "BUY_TICKETS", + "DONATE", + "DONATE_NOW", + "GET_DIRECTIONS", + "GET_SHOWTIMES", + "LISTEN_NOW", + "ORDER_NOW", + "PLAY_GAME", + "REQUEST_TIME", + "SEE_MENU", + "START_ORDER", + "INSTALL_MOBILE_APP", + "USE_APP", + "REGISTER", + "JOIN", + "ATTEND", + "REQUEST_DEMO", + "VIEW_QUOTE", + "APPLY", + "SEE_MORE", + "BUY_NOW", + ] + | None = None, link_url: str | None = None, lead_gen_form_id: str | None = None, image_url: str | None = None, @@ -6348,7 +8256,31 @@ def ad_campaigns_create_standalone_ad( organization_id: str | None = None, targeting: dict[str, Any] | None = None, countries: list[str] | None = None, - country_groups: list[str] | None = None, + country_groups: list[ + Literal[ + "africa", + "asia", + "europe", + "north_america", + "south_america", + "oceania", + "central_america", + "caribbean", + "eea", + "euro_area", + "nafta", + "mercosur", + "afta", + "apec", + "gcc", + "cisfta", + "emerging_markets", + "itunes_app_store", + "android_free_store", + "android_paid_store", + ] + ] + | None = None, cities: list[Any] | None = None, regions: list[Any] | None = None, age_min: int | None = None, @@ -6361,12 +8293,22 @@ def ad_campaigns_create_standalone_ad( work_positions: list[dict[str, Any]] | None = None, work_employers: list[dict[str, Any]] | None = None, work_industries: list[dict[str, Any]] | None = None, - income_tier: str | None = None, + income_tier: Literal["top_5", "top_10", "top_10_25", "top_25_50"] | None = None, languages: list[str] | None = None, placements: dict[str, Any] | None = None, saved_targeting_id: str | None = None, raw_targeting: dict[str, Any] | None = None, - special_ad_categories: list[str] | None = None, + special_ad_categories: list[ + Literal[ + "HOUSING", + "EMPLOYMENT", + "CREDIT", + "FINANCIAL_PRODUCTS_SERVICES", + "ISSUES_ELECTIONS_POLITICS", + "ONLINE_GAMBLING_AND_GAMING", + ] + ] + | None = None, special_ad_category_country: list[str] | None = None, regional_regulated_categories: list[str] | None = None, regional_regulation_identities: dict[str, Any] | None = None, @@ -6380,8 +8322,9 @@ def ad_campaigns_create_standalone_ad( translations: list[dict[str, Any]] | None = None, placement_assets: dict[str, Any] | None = None, audience_id: str | None = None, - campaign_type: str = "display", - location_targeting_type: str | None = None, + campaign_type: Literal["display", "search", "pmax", "demand_gen"] = "display", + location_targeting_type: Literal["presence", "presence_or_interest"] + | None = None, asset_group: dict[str, Any] | None = None, demand_gen: dict[str, Any] | None = None, keywords: list[Any] | None = None, @@ -6394,8 +8337,14 @@ def ad_campaigns_create_standalone_ad( structured_snippets: list[dict[str, Any]] | None = None, advantage_audience: int | None = None, attribution_spec: list[dict[str, Any]] | None = None, - gender: str = "all", - bid_strategy: str | None = None, + gender: Literal["all", "male", "female"] = "all", + bid_strategy: Literal[ + "LOWEST_COST_WITHOUT_CAP", + "LOWEST_COST_WITH_BID_CAP", + "COST_CAP", + "LOWEST_COST_WITH_MIN_ROAS", + ] + | None = None, bid_amount: float | None = None, roas_average_floor: float | None = None, portfolio_bid_strategy_id: str | None = None, @@ -6406,16 +8355,33 @@ def ad_campaigns_create_standalone_ad( dsa_payor: str | None = None, brand_identity: dict[str, Any] | None = None, identity_id: str | None = None, - identity_type: str | None = None, + identity_type: Literal["TT_USER", "CUSTOMIZED_USER", "BC_AUTH_TT"] + | None = None, smart_plus: bool | None = None, user_os: list[str] | None = None, user_device: list[str] | None = None, is_skadnetwork_attribution: bool | None = None, - campaign_attribution: str | None = None, + campaign_attribution: Literal["AEM", "SKADNETWORK"] | None = None, promoted_object: dict[str, Any] | None = None, ) -> str: """Create standalone ad + Create a paid ad with custom creative across Meta, Google Ads, Pinterest, TikTok, X, LinkedIn, and OpenAI Ads (ChatGPT Ads). + + Google Performance Max: set `campaignType: \"pmax\"` and supply `assetGroup` with + text, images by role, business name and finalUrl. Creates a daily budget, PAUSED + campaign and asset group atomically. `validateOnly: true` validates the complete + request with Google without creating or persisting resources. Read assets with + `GET /v1/ads/campaigns/{campaignId}/asset-groups`. The logo is required; video is + optional via `assetGroup.youtubeVideoId`. Brand guidelines are disabled at creation. + All supplied asset links are validated together against Google's minimum asset requirements. + PMax rejects ACTIVE creation, portfolio bidding, bid caps, legacy creative fields + and attach shapes. Geo and language targeting are supported; omitted geo targets + all locations. PMax does not require top-level goal, headline, body or linkUrl. + Supported bidding: omitted or ... + + Platforms: meta, google, tiktok, linkedin, pinterest, x, openai + Args: account_id: (required) ad_account_id: (required) @@ -7054,6 +9020,13 @@ def ad_campaigns_create_standalone_ad( def ad_campaigns_get_campaign_conversion_goals(campaign_id: str) -> str: """Get campaign conversion goals + A Google campaign's conversion goals (CampaignConversionGoal, `biddable` per category + and origin) and its goal config (ConversionGoalCampaignConfig): `goalConfigLevel` + CUSTOMER means the campaign follows the account-default goals, CAMPAIGN means it uses + its own goals or `customConversionGoalId`. + + Platforms: google + Args: campaign_id: Google campaign id (required)""" client = _get_client() @@ -7076,11 +9049,19 @@ def ad_campaigns_get_campaign_conversion_goals(campaign_id: str) -> str: def ad_campaigns_update_campaign_conversion_goals( campaign_id: str, goals: list[dict[str, Any]] | None = None, - goal_config_level: str | None = None, + goal_config_level: Literal["CUSTOMER", "CAMPAIGN"] | None = None, custom_conversion_goal_id: str | None = None, ) -> str: """Update campaign conversion goals + Sets `biddable` on campaign goals, switches `goalConfigLevel`, and/or points the + campaign at a custom conversion goal, in one mutate. `customConversionGoalId: null` + clears it; Google refuses that (400) while the campaign stays at CAMPAIGN level with + no biddable goals, so send `goalConfigLevel: CUSTOMER` with it to fall back to the + account goals. Returns the re-read campaign goals. + + Platforms: google + Args: campaign_id: Google campaign id (required) goals @@ -7117,6 +9098,13 @@ def ad_creatives_generate_ad_previews( ) -> str: """Render pre-create ad previews + Renders how a creative would look per placement BEFORE any ad exists, via Meta's + `/generatepreviews`. Provide exactly one creative source: `existingCreativeId` or `creativeSpec`. + Each preview is an HTML `