Google Search Console integration for Model Context Protocol (MCP)
Hermoso: Ads Manager is installed from its publisher's own source and answers where it runs, so this marketplace is not in the path of a single call. There is no address here to send one to, and a panel that pretended otherwise would be showing you an answer we made up. Install it and call it from your own client — the Installation tab has the entry for each one.
What it does
AI marketing agent for paid ads: run campaigns on 11 ad networks and read your web analytics.
Quickstart
# 1 — add it to Claude Code
claude mcp add --scope user --transport http hermoso-ads-manager-mcp 'https://app.hermoso.ai/mcp?tools=app-ads&src=registry'
# 2 — ask your agent for something these tools do
# enable_tools, find_tools, call_tool and 497 more
Hermoso: Ads Manager costs nothing on mcprush: there is no plan to choose here, no cap to set and nothing mcprush can bill you for.
Collected from a public index. Nobody has claimed this account, so nothing here was written by its author — claim it if it is yours.
Where are you running it?
Every route below installs the same thing and ends at the same approval screen. Nothing here runs on your machine and nothing runs on ours: this server answers at its publisher’s own address, and what you install is the connection straight to it.
This is a public server: it answers at its publisher’s own address and this marketplace is not in the path. Claude Code registers it in one command; --scope user makes it available in every project. The transport below is http, as the publisher's own manifest declares it.
claude mcp add --scope user --transport http hermoso-ads-manager-mcp 'https://app.hermoso.ai/mcp?tools=app-ads&src=registry'Reconnect, or start a new session, and the tools appear in the model’s tool list.
One config entry pointing straight at the publisher’s own address — this marketplace is not in the path, and nothing from this listing executes on your machine.
500 tools, with what each one reads, writes and reaches shown before you agree — the same list on every route above. Read the tool surface.
Tool surface
What the model actually sees. Descriptions are diffed on every release — see version history.
LIST a group of tools that is not in this session's roster. IT IS NOT HOW YOU REACH A TOOL — call_tool runs any Hermoso tool whether or not it is listed, and that works everywhere. Use this when the session will use MANY tools from one area and you want them in your list. WORKS ON CLIENTS THAT RE-READ THE TOOL LIST (stdio, the CLI, Cursor, Claude Code); a host that fixed its roster at connect time
| Parameter | Type | Required | Notes |
|---|---|---|---|
| groups | array | yes | Groups to switch on, e.g. ['ads']. Unknown names are refused by name rather than ignored. |
Search EVERY Hermoso tool — your starting list is deliberately short, and everything else in the product is here — by name, task or group. Each row gives the tool's PARAMETERS in one line, its CREDIT COST (free means free on every plan; a tool that runs a model quotes the live per-model figure) and its recent HEALTH on this server (failure rate and typical duration, or "no recent calls", which mea
| Parameter | Type | Required | Notes |
|---|---|---|---|
| query | string | no | words from the task or the tool name, e.g. "lead form", "whatsapp", "google ads keyword", "meta insights" |
| group | string | no | limit to one group: core, research, create, channels, channel_admin, analytics, ads, files, workspace |
| limit | number | no | how many to return (default 12, max 40) |
| onlyHealthy | boolean | no | leave out tools that are failing their recent calls or that need a connector this workspace has not made. Default false — nothing is hidden unless you ask, because a missing row reads as a missing cap |
Run ANY Hermoso tool by name — including the paid-campaign, analytics and channel-admin tools that are not in this session's starting list — with the same permissions, the same account and the same result as calling it directly. Get the exact name and its args from find_tools first. This is the route on hosts that cannot reload their tool list mid-conversation (claude.ai, ChatGPT): enable_tool
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | the tool name exactly as find_tools returned it, e.g. create_meta_lead_form |
| args | object | no | the tool's arguments as an object, exactly as its own schema takes them |
Probe what this Hermoso account can do RIGHT NOW: available image/video model ids + their credit costs (estimates: a render is charged its actual cost), aspect ratios, video durations, the recipe ids, and the canEdit/canAvatar flags. Call it when you need a specific model id, a credit cost, or a capability you are not sure of. Read-only, free.
No parameter schema on file.
Return the credit balance, recent priced calls with their credits, and costModel (the one-sentence rule of what costs credits). THE RULE: only AI model runs and Ad Spy research spend credits; publishing, scheduling, ads management, analytics, comments, DMs and connectors cost no credits on any plan (X is the single per-call exception; xCost gives its exact credits per post, with and without a link
No parameter schema on file.
Out of credits? Top up with a credit PACK. Call with no argument to list the available packs (id · credits · price). If the account has a saved card and you have billing-admin rights, calling with pack quotes the exact charge and calling again with confirm:true AND the quote's quote_token charges the saved card instantly (same one-click top-up as the app — no redirect). If there's no saved card
| Parameter | Type | Required | Notes |
|---|---|---|---|
| pack | string | no | the pack id to buy (e.g. pack-2k) — omit to list the available packs first |
| confirm | boolean | no | set true to actually charge the saved card for pack (required for the one-click charge; ignored on the checkout-link path) |
| quote_token | string | no | the quoteToken returned by the quote step — REQUIRED (with confirm:true) to charge; it binds the exact pack + price you quoted (10-minute validity) and makes a retried confirm idempotent |
Report a bug in Hermoso to the team. Use this when something in Hermoso genuinely misbehaves — a tool errors unexpectedly, returns a wrong or malformed result, a render comes back broken, or documented behaviour doesn't match what happened. Include what you were trying to do, the exact tool call and arguments, and what came back. Do NOT use it for out-of-credits, a policy refusal, or a missing cap
| Parameter | Type | Required | Notes |
|---|---|---|---|
| summary | string | yes | one-line summary of the bug |
| details | string | yes | what you were doing, the tool + arguments you called, what you expected, and what actually happened (paste the exact error) |
| severity | string | no | high = blocks the task or loses paid work; medium = wrong output but workable; low = cosmetic |
Ask the Hermoso team for a capability that doesn't exist yet. Use this when you need something Hermoso genuinely can't do — an unsupported platform or channel, a missing model, an export format, a tool that would have completed the user's task but isn't available. Say what the user was trying to achieve, not just the feature name — the use case is what gets built. Free, no credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| summary | string | yes | one line: the capability you need |
| details | string | yes | what the user was actually trying to achieve, why the current tools couldn't do it, and what you'd expect the capability to do |
Show this account's billing at a glance: current plan (quote plan.priceUsd per plan.period), credit balance, whether auto-reload is on, whether a card is on file, and whether YOU (this key) have ADMIN rights to change billing. Read-only, free. Call it before upgrade_plan / set_auto_reload to know what's possible — members have read-only billing. IN A SHARED TEAM WORKSPACE A MEMBER SEES THE PLAN AN
No parameter schema on file.
Change the SUBSCRIPTION plan (admin only). No argument lists the monthly plans. Basic ($9.99) is the posting plan: unlimited posting and Autopilot with NO monthly credits (it includes the same 250+ free credits as the Free plan, monthly credits start on Starter, and Autopilot spends credits). Starter, Pro and Studio include monthly credits and no markup on models. Every paid plan posts without a l
| Parameter | Type | Required | Notes |
|---|---|---|---|
| plan | string | no | the plan id to move to (e.g. pro) — omit to list the available plans first |
| period | string | no | monthly only ('yr' buys monthly) |
| confirm | boolean | no | true (with quote_token) to make a quoted upgrade |
| quote_token | string | no | the quoteToken from the quote step |
Turn automatic credit reloads on or off (admin only): when the balance drops below a threshold, the card on file is charged for a top-up pack — SERVER-SIDE, even with no app open. Requires a saved card, added once in the app at first checkout/top-up; if there's none the tool tells you exactly where to add it. After that one-time card setup, agents can manage auto-reload, top-ups and plan links ful
| Parameter | Type | Required | Notes |
|---|---|---|---|
| enabled | boolean | yes | true to turn auto-reload on, false to turn it off |
| thresholdCredits | integer | no | reload when the balance drops below this many credits |
| reloadCredits | integer | no | how many credits to add each reload — must match a credit pack size (see buy_credits) |
List every profile on this account (id + name; a profile is a brand, a creator or a personal workspace) and which one this connection acts on, PLUS any profile another account shared with you. Call this, then use_brand to switch; a SHARED profile switches the same way (pass its name or the profile id printed here). If a profile looks empty when the app shows it full, you are acting on a different
No parameter schema on file.
Pin which profile this connection acts on. Pass the profile id or exact name from list_brands. Works for your own profiles AND one another account SHARED with you (pass its name or the profile id list_brands prints); access is verified against your real membership before it is pinned. Persists for this API key until changed, on every surface (hosted connector included — no environment variables, n
| Parameter | Type | Required | Notes |
|---|---|---|---|
| brand | string | yes | profile id (e.g. default / p_xxx), its exact name from list_brands, or the profile id of one shared with you |
Add a NEW profile (a brand, client, creator or personal workspace) and switch to it. Each has its OWN brand details, memory, swipefile, Library, avatars, skills, playbooks and connectors; nothing leaks between them. Use draft_brand to FILL it and update_brand to edit it. Re-running with the same name returns the existing profile instead of a duplicate. Creating it is free. Drafting a NEW brand int
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | the name for the new profile (a brand, client or creator name) |
| activate | boolean | no | switch this connection to the new profile (default true); everything you do next scopes to it |
PERMANENTLY delete a profile and EVERYTHING in it — brand details, memory, swipefile, Library creations, generated assets, avatars, skills, playbooks, chats — and disconnect its connected accounts. Irreversible, and it applies to everyone the workspace is shared with. Call it WITHOUT confirm first: it reports exactly what that workspace holds. Show the user that inventory verbatim, get an unambigu
| Parameter | Type | Required | Notes |
|---|---|---|---|
| brand | string | yes | profile id or exact name from list_brands |
| confirm | boolean | no | REQUIRED true — this destroys the whole profile and cannot be undone |
| confirmName | string | no | the workspace's EXACT name, required when it is not empty — copy it from the inventory this tool returned, after the user has agreed to it |
| confirmConnectors | number | no | the number of connected accounts the inventory reported, required when there is at least one — the user must specifically agree to losing them, because reconnecting each needs a browser and no agent c |
Persist an ARBITRARY user file (image, video, audio, PDF or document, up to 150MB) into Hermoso and get back a durable public URL that EVERY publish, schedule and ad-build tool accepts. Kept in the Library. THIS IS THE BRING-YOUR-OWN-CREATIVE PATH: it is for files that have NOTHING to do with a Hermoso render (media on the user's desktop, an agency's finished ad, a photo they shot), and it means y
| Parameter | Type | Required | Notes |
|---|---|---|---|
| getUploadUrl | boolean | no | ASK FOR A ONE-TIME UPLOAD URL instead of uploading now — use this whenever the file is on the user’s machine and you can run a shell or an HTTP request. Returns a uploadUrl you PUT the raw bytes to (a |
| url | string | no | a PUBLIC http(s) URL Hermoso fetches server-side (private/internal addresses are refused, and every redirect hop is re-checked). Works on every surface including the hosted connector, and the bytes ne |
| dataUri | string | no | base64 data: URI of the file bytes (data:<mime>;base64,<…>) — bytes travel over this connection, so keep it small |
| name | string | no | original file name — helps pick the right extension |
The DEEP Pinterest ad report — Pinterest’s ASYNCHRONOUS lane, which reaches 914 DAYS back (2.5 years) where pinterest_ads_report stops at 90, and carries roughly three times the metric columns (conversion, ROAS and cross-device families the quick report does not have). Use it for anything older than three months, and for revenue questions. Levels: ADVERTISER / CAMPAIGN / AD_GROUP / PIN_PROMOTION /
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id; omit when only one is shared with this profile |
| token | string | no | RESUME a pending report — pass the token back instead of re-submitting |
| since | string | no | YYYY-MM-DD (default 30 days ago) |
| until | string | no | YYYY-MM-DD (default today) |
| granularity | string | no | time bucket: TOTAL, DAY, HOUR, WEEK or MONTH |
| level | string | no | ADVERTISER | CAMPAIGN | AD_GROUP | PIN_PROMOTION | KEYWORD | PRODUCT_GROUP | PRODUCT_ITEM (+ _TARGETING variants) — default CAMPAIGN. An unknown level is refused with the list. |
| columns | array | no | Pinterest async metric columns — omit for the standard spend/impressions/clicks/CTR/conversions set |
| campaignIds | array | no | limit the report to these campaign ids |
| adGroupIds | array | no | limit the report to these ad group ids |
| adIds | array | no | limit the report to these ad ids |
| targetingTypes | array | no | only valid with a *_TARGETING level |
| reportFormat | string | no | JSON or CSV |
THE IDS AN AD GROUP’S targetingSpec NEEDS. create_pinterest_ads_campaign and create_pinterest_ads_ad_group accept interests, locales, locations, age buckets and keywords — this is where those values come from, and guessing one is the worst option available: Pinterest ACCEPTS a well-formed but wrong interest id and the campaign then quietly targets the wrong people. Pass targetingType (INTEREST, GE
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id; omit when only one is shared with this profile |
| targetingType | string | no | which catalog to list — required unless you pass interestId |
| query | string | no | narrow the list to options mentioning this text (filtered here, not by Pinterest) |
| interestId | string | no | drill INTO one interest and return its child interests — how you walk the interest tree |
| limit | number | no | 1–500, default 100 |
Upload creative(s) — a finished Hermoso ad OR arbitrary user files (e.g. A folder of media from the user’s desktop) — into a connected ad account’s ASSET LIBRARY so the user or a later ad-build step can use them in their OWN campaigns. Pass url for one file, or urls (up to 20) to BULK-upload in a single call. Each accepts a public https URL, a data: URI, or a Hermoso /generated path; for LOCAL
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | yes | Ad account id |
| url | string | no | a single public https URL / data: URI / /generated path; a URL, not a local file path; use upload_file for local files |
| urls | array | no | up to 20 media URLs/paths for a one-call BULK upload |
| kind | string | no | inferred from the URL if omitted |
| name | string | no | a label for the asset |
Use this when the user asks to create a new Facebook or Instagram ad campaign on their own ad account. Create a campaign on a connected Meta ad account. Always created PAUSED — it spends NOTHING until you activate it with set_meta_campaign_status(confirm:true). Optionally set a dailyBudgetUsd (an amount in the AD ACCOUNT’s currency, e.g. CAD on a CAD account — the name is historical, it is not USD
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | campaign name |
| adAccountId | string | yes | Ad account id |
| objective | string | no | default OUTCOME_TRAFFIC |
| bidStrategy | string | no | with dailyBudgetUsd: how the campaign bids across its ad sets — LOWEST_COST_WITHOUT_CAP (default: highest volume), LOWEST_COST_WITH_BID_CAP / COST_CAP (each ad set then needs bidAmountUsd), LOWEST_COS |
| dailyBudgetUsd | number | no | optional campaign daily budget in the AD ACCOUNT's currency (not necessarily USD — the reply names it); real spend once ACTIVE. With it, the campaign holds the budget (Advantage+ campaign budget) and |
| productCatalogId | string | no | CATALOG (dynamic product) ADS. This is the CAMPAIGN-level half of a catalog ad; the AD SET takes a productSetId and the creative takes the same set. Passing a product SET id here is refused by name wi |
Turn a campaign ON (ACTIVE) or OFF (PAUSED). ACTIVATING STARTS REAL AD SPEND — you MUST first show the user the campaign name + its daily budget, get an explicit yes, then call with status:"ACTIVE" and confirm:true. Pausing is always safe. Needs ads-management permission.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| campaignId | string | yes | the campaign id (from create_meta_campaign) |
| status | string | yes | ACTIVE = start spending; PAUSED = stop |
| confirm | boolean | no | REQUIRED true to activate (real spend) — set only after the user explicitly approved the budget |
Upload a PLAYABLE ad (an interactive HTML game the advertiser already has) to a Meta ad account. Pass url: ONE HTML file up to 2 MB, or a zip up to 5 MB (under 100 files) with index.html at the top; it must call FbPlayableAd.onCTAClick() and load nothing from outside. Hermoso checks those rules first and refuses a file Meta would reject. Returns playableAssetId: build the ad with create_meta_ad(pl
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | yes | ad account id |
| url | string | yes | public https link to the HTML file or zip; upload_file turns a local file into one |
| name | string | no | display name for the playable in Meta; optional |
List the playables uploaded to a Meta ad account (id and name), for create_meta_ad's playableAssetId. Free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | yes | act_… or the bare number |
| limit | number | no | maximum number of playables to return |
Build a complete, ready-to-run Meta ad: campaign -> ad set (FULL targeting + budget + schedule + bidding) -> creative -> ad(s), ALL created PAUSED — it spends NOTHING until you activate the campaign with set_meta_campaign_status(confirm:true). This is the "create a campaign and put the ads on it" path. IMAGE, VIDEO (uploaded, transcoded and thumbnailed for you) and CAROUSEL (format:"carousel", 2–1
| Parameter | Type | Required | Notes |
|---|---|---|---|
| boostPostId | string | no | Promote a post that ALREADY EXISTS instead of building a new ad from media. Accepts the numeric post id, <pageId>_<postId>, or a permalink (an Instagram post is its NUMERIC media id — an instagram.com |
| boostTarget | string | no | Which surface the boosted post lives on. Default facebook. REQUIRED for an Instagram post: an IG media id and a Facebook post id are both bare digits, so this is never inferred — Meta takes a differen |
| adAccountId | string | yes | Ad account id |
| format | string | no | auto = one ad per asset (image or video); carousel = ONE multi-card ad |
| imageUrl | string | no | public https image URL for the ad creative |
| imageUrls | array | no | several image URLs -> one ad each, or the carousel cards in order |
| videoUrl | string | no | a video URL -> a real Meta VIDEO ad (uploaded + transcoded + thumbnailed for you) |
| thumbnailUrl | string | no | custom video thumbnail (otherwise Meta picks a frame) |
| message | string | no | primary ad text |
| headline | string | no | headline |
| description | string | no | the smaller description line under the headline |
| cards | array | no | carousel cards in order — each may set its own headline/description/link |
| carouselEndCard | boolean | no | append the Page end card to a carousel |
| link | string | no | destination URL (defaults to the brand domain) |
| cta | string | no | call-to-action, e.g. SHOP_NOW / LEARN_MORE / SIGN_UP (default LEARN_MORE); WHATSAPP_MESSAGE for a click-to-WhatsApp ad (see destinationType). Any Meta call-to-action type works (SEE_DETAILS…); unknown |
| leadFormId | string | no | LEAD ADS — attach an instant form (from list_meta_lead_forms / create_meta_lead_form) so the button opens that form INSIDE Facebook/Instagram instead of sending the click to a website. Requires object |
| objective | string | no | default OUTCOME_TRAFFIC |
| brandedContentSponsorIgId | string | no | PARTNERSHIP AD — the Instagram account id shown as the PAID PARTNER on this ad (Meta’s instagram_branded_content.sponsor_id), so the ad runs as a creator collaboration rather than as a plain brand a |
| brandedContentSponsorPageId | string | no | PARTNERSHIP AD — the Facebook Page id of the paid partner (Meta’s facebook_branded_content.sponsor_page_id). Give BOTH halves when the ad runs across both surfaces: supplying only one leaves the oth |
| productSetId | string | no | CATALOG (dynamic product) ADS. Meta binds a catalog at TWO levels and they are different ids on different objects: the CAMPAIGN takes productCatalogId, the AD SET takes productSetId. Passing a CATALOG |
Create an AD SET on an EXISTING Meta campaign — the level that holds the audience, budget, schedule and bidding. Use it to hang SEVERAL ad sets off ONE campaign, which is how you actually test audiences on Meta (one ad set per audience, same campaign, same creative). Takes the same full targeting, pixelId/conversionEvent, bidStrategy, schedule and budget fields as create_meta_ad. Created PAUSED
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | yes | ad account id (act_… or digits) |
| campaignId | string | yes | the campaign this ad set belongs to |
| name | string | no | ad set name |
| brandedContentSponsorIgId | string | no | PARTNERSHIP AD — the Instagram account id shown as the PAID PARTNER on this ad (Meta’s instagram_branded_content.sponsor_id), so the ad runs as a creator collaboration rather than as a plain brand a |
| brandedContentSponsorPageId | string | no | PARTNERSHIP AD — the Facebook Page id of the paid partner (Meta’s facebook_branded_content.sponsor_page_id). Give BOTH halves when the ad runs across both surfaces: supplying only one leaves the oth |
| productSetId | string | no | CATALOG (dynamic product) ADS. Meta binds a catalog at TWO levels and they are different ids on different objects: the CAMPAIGN takes productCatalogId, the AD SET takes productSetId. Passing a CATALOG |
| dailyBudgetUsd | number | no | ad-set daily budget in the AD ACCOUNT's currency (historical name — not necessarily USD; default 10, or 1,000 in a whole-unit currency like JPY) — spends only once ACTIVE. Under a campaign that HAS it |
| lifetimeBudgetUsd | number | no | a fixed total instead of a daily budget, in the ad account's currency — REQUIRES endTime |
| country | string | no | 2-letter shorthand when you are not passing full targeting (default US) |
| targeting | object | no | full Meta ad-set targeting — age, gender, geo, interests, behaviours, audiences, languages, placements, devices. Use EXACTLY these key names: an unrecognised one (e.g. geoLocations) is REFUSED by name |
| pixelId | string | no | Meta Pixel id — with this the ad set optimizes for a real CONVERSION instead of falling back to link clicks |
| conversionEvent | string | no | PURCHASE | LEAD | COMPLETE_REGISTRATION | ADD_TO_CART | INITIATED_CHECKOUT | … |
| customConversionId | string | no | Meta custom conversion id (digits), from Meta Events Manager |
| applicationId | string | no | app-promotion ads |
| objectStoreUrl | string | no | app store link for an app-install ad |
| optimizationGoal | string | no | override, e.g. OFFSITE_CONVERSIONS / LANDING_PAGE_VIEWS / THRUPLAY / VALUE |
| billingEvent | string | no | default IMPRESSIONS |
| bidStrategy | string | no | bidding: lowest cost, bid cap, cost cap or minimum ROAS (Meta's strategy names) |
| bidAmountUsd | number | no | REQUIRED for a bid cap / cost cap |
| minRoas | number | no | REQUIRED for LOWEST_COST_WITH_MIN_ROAS, e.g. 1.1 |
Look up the Meta targeting ids you need before building an ad set — interests, behaviours, cities/regions/zips/DMAs, languages, employers, job titles and schools. type:"adinterest" (q:"yoga") returns interest ids + audience size; type:"adTargetingCategory" with class:"behaviors" returns behaviour ids; type:"adgeolocation" (q:"Toronto", optionally locationTypes:"city") returns the geo KEYS that go
| Parameter | Type | Required | Notes |
|---|---|---|---|
| type | string | yes | what kind of targeting object to search |
| q | string | no | what to search for |
| class | string | no | for adTargetingCategory, e.g. "behaviors" or "interests" |
| locationTypes | string | no | comma-separated: country,region,city,zip,geo_market |
| countryCode | string | no | 2-letter hint to disambiguate a city name |
| adAccountId | string | no | search with that ad account’s token |
| limit | number | no | maximum matches to return; the reply shows up to 25 |
Read the EXISTING campaigns, ad sets, or ads on a connected Meta ad account — id, name, status, budget, objective, and at level ad WHY Meta rejected or flagged an ad (its review feedback, in Meta’s words). Pass adAccountId and level (campaign|adset|ad). Scope to a parent with campaignId (-> its ad sets/ads) or adsetId (-> its ads), and filter by status (ACTIVE/PAUSED/…). Read-only — use it to insp
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | yes | Ad account id |
| level | string | no | what to list (default campaign) |
| campaignId | string | no | list the ad sets / ads under this campaign |
| adsetId | string | no | list the ads under this ad set |
| status | string | no | filter by effective status, e.g. ACTIVE / PAUSED |
| limit | number | no | max rows (1–200, default 50) |
Use this when the user asks how their paid ad campaigns are doing or what to scale or pause. ONE verdict across EVERY ad platform this workspace has connected. It pulls each platform's own report (Meta, Google Ads, ChatGPT Ads, TikTok, LinkedIn, Microsoft Advertising, Pinterest, Reddit, Snapchat, X, Apple Ads), normalises every campaign / ad set / ad into ONE table — spend, impressions, clicks, CT
| Parameter | Type | Required | Notes |
|---|---|---|---|
| days | number | no | how many complete days back, ending yesterday (default 14, max 90) |
| platforms | array | no | meta, google_ads, openai_ads, tiktok_ads, linkedin, microsoft_ads, pinterest_ads, reddit_ads, snapchat_ads, x_ads, apple_ads — omit for every connected ad platform |
| goal | string | no | what to optimise for, in the user's own words |
| level | string | no | default campaign |
Use this when the user asks how their Facebook or Instagram ads performed. Pull performance INSIGHTS (spend, impressions, reach, clicks, CTR, CPC, CPM, conversions) for a connected ad account, or a specific campaign / ad set / ad. Pass adAccountId (for auth); optionally objectId to scope to one object and level to break the numbers down. BREAKDOWNS are what make the numbers actionable — a flat tot
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | yes | ad account id (act_… or digits) |
| objectId | string | no | a campaign / ad set / ad id to scope to (default: the whole account) |
| level | string | no | break the numbers down by this level |
| breakdowns | string | no | comma-separated, e.g. "age,gender" | "publisher_platform,platform_position" | "country" | "impression_device" |
| actionBreakdowns | string | no | comma-separated, e.g. "action_type,action_device" — splits the conversion/action counts |
| datePreset | string | no | Meta's date_preset enum, default last_30d: today | yesterday | last_3d | last_7d | last_14d | last_28d | last_30d | last_90d | this_week_mon_today | this_week_sun_today | last_week_mon_sun | last_week |
| since | string | no | start date YYYY-MM-DD (use with until). Meta keeps 37 months: an older start is moved to the oldest day Meta has, and the note says so |
| until | string | no | end date YYYY-MM-DD |
Render a REAL preview of a Meta ad, per placement — Meta returns a link that shows exactly what a person scrolling Facebook or Instagram would see. Pass adAccountId + adId (from list_meta_ads), or creativeId. Optional placements (comma-separated): facebook_feed, facebook_feed_desktop, facebook_story, facebook_reels, facebook_profile_feed, facebook_marketplace, facebook_right_column, facebook_video
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | yes | ad account id (act_… or digits) |
| adId | string | no | the ad to preview (from list_meta_ads) |
| creativeId | string | no | preview a creative directly instead of an ad |
| placements | string | no | comma-separated placements (see the list above) |
Ask Meta how many people a targeting spec can actually reach — BEFORE any budget is committed. Two ways: pass adSetId to size an ad set you already built (Meta uses its own saved targeting), or pass the same targeting object you would give create_meta_ad (plus optional objective / optimizationGoal / country / pixelId) to size an audience you are considering. Returns the monthly-active range, a d
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | yes | ad account id (act_… or digits) |
| adSetId | string | no | size an EXISTING ad set using its own saved targeting |
| targeting | object | no | a targeting object, same shape as create_meta_ad.targeting |
| objective | string | no | OUTCOME_TRAFFIC | OUTCOME_SALES | … — picks the matching optimization goal |
| optimizationGoal | string | no | override the goal, e.g. REACH / LINK_CLICKS / OFFSITE_CONVERSIONS |
| country | string | no | 2-letter fallback country when targeting names no geo |
| pixelId | string | no | estimate a conversion goal against this pixel |
| conversionEvent | string | no | e.g. PURCHASE — used with pixelId |
List the custom audiences and lookalikes on a connected Meta ad account — id, name, type, approximate size, and whether Meta says it is ready to target. Call it before create_meta_audience (so you never build a duplicate) and before targeting one: the ids go straight into create_meta_ad’s targeting.customAudiences / excludedCustomAudiences. Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | yes | ad account id (act_… or digits) |
| limit | number | no | max rows (1–200, default 50) |
Build a retargeting audience on a connected Meta ad account. Four kinds: kind:"website" (people whose visited URL contains urlContains, seen by pixelId — pass the brand’s own domain for "all visitors"; retentionDays up to 180), kind:"engagement" (people who did event on the brand’s Facebook Page, or its Instagram business profile with source:"instagram"; retentionDays up to 730), or kind:"lookal
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | yes | ad account id (act_… or digits) |
| kind | string | yes | which kind of audience to build — customerfile = your own customer list |
| name | string | no | audience name (required unless adding rows to an existing list with audienceId) |
| description | string | no | optional free-text note stored with the audience |
| retentionDays | number | no | how long someone stays in it — website max 180, engagement max 730 (default 30) |
| pixelId | string | no | website: the Meta Pixel that sees the traffic |
| urlContains | string | no | website: the URL fragment that defines the audience (your domain = all visitors) |
| pageId | string | no | engagement: which connected Page (required only if the profile has several) |
| source | string | no | engagement: Facebook Page (default) or the linked Instagram business profile |
| event | string | no | engagement: page_engaged | page_visited | page_liked | page_messaged | page_cta_clicked | page_or_post_save | page_post_interaction — or ig_business_profile_all | ig_business_profile_engaged | ig_user |
| sourceAudienceId | string | no | lookalike: the existing audience to model (from list_meta_audiences) |
| country | string | no | lookalike: REQUIRED 2-letter country you will run it in — Meta delivers a lookalike to the locations of the ad set that targets it, so target this country there |
| ratio | number | no | lookalike: 0.01–0.20 = the top 1%–20% most similar people (default 0.01) |
| startingRatio | number | no | lookalike: optional lower bound for a band (e.g. 0.01 with ratio 0.03 = the 1%–3% band) — at least 0.01 and less than ratio; not combinable with lookalikeType |
| lookalikeType | string | no | lookalike: similarity (tighter) or reach (broader) — default similarity |
| prefill | boolean | no | website/engagement: seed it with activity from BEFORE the audience existed (default true) |
| customerFileSource | string | no | customerfile: REQUIRED attestation of where the list came from (customers gave it to you = USER_PROVIDED_ONLY) |
| schema | array | no | customerfile: the column names of rows, in order, e.g. ["EMAIL","PHONE"] |
| rows | array | no | customerfile: the customers, one array per person in schema order (up to 10,000 per call). Plain values — Hermoso normalizes and hashes them before they leave the server |
| audienceId | string | no | customerfile: ADD these rows to this existing customer-list audience (from list_meta_audiences) instead of creating a new one |
List the META PIXELS on one of the brand’s ad accounts — id, name, when it was created, and WHEN IT LAST FIRED. This is where the pixelId every conversion tool needs comes from: create_meta_ad takes it (with conversionEvent) to optimise an ad set for OFFSITE_CONVERSIONS instead of link clicks, and create_meta_audience needs it to build a website retargeting audience. Without this tool that id coul
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | yes | The ad account, with or without the act_ prefix |
| includeCode | boolean | no | also return the full pixel <script> snippet — only ask when the user is about to install it |
Read a Meta AD ACCOUNT’s BILLING and DELIVERY state: account status in words (ACTIVE, DISABLED, UNSETTLED unpaid balance, IN_GRACE_PERIOD failed payment, PENDING_RISK_REVIEW, CLOSED…) with the disable reason; currency and time zone; BALANCE (the bill amount due); AMOUNT SPENT; SPEND CAP and how much is left under it (none means no account spending limit); the minimum daily budget; whether it is a
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | yes | The ad account, with or without the act_ prefix |
Create a META PIXEL on one of the brand’s ad accounts, so their ads can optimise for real website conversions instead of link clicks and so website retargeting audiences become possible. A pixel is a DEFINITION: it cannot serve, cannot spend, and RECORDS NOTHING until its snippet is installed on the site — the reply hands back that snippet and says so. TWO THINGS TO SAY OUT LOUD BEFORE CALLING IT:
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | yes | The ad account, with or without the act_ prefix |
| name | string | yes | what it will be called in Events Manager — name it after the website or brand it measures |
List the INSTANT LEAD FORMS on a connected Facebook Page — id, name, status, how many leads each has collected and what each one asks. This is where the formId every other lead tool needs comes from, and calling it before create_meta_lead_form is how you avoid building a duplicate. Read-only, free. NEEDS the pages_manage_ads permission, which Hermoso’s Meta consent screen asks for. If Meta answers
| Parameter | Type | Required | Notes |
|---|---|---|---|
| pageId | string | no | Which connected Page — required only if the profile has more than one |
| limit | number | no | max rows (1–100, default 25) |
Create an INSTANT LEAD FORM on a connected Facebook Page — the in-app form a Meta lead ad opens instead of sending someone to a website, so a person can sign up without leaving the app. questions takes Meta’s own type names, e.g. ["FULL_NAME","EMAIL","PHONE"]; for anything bespoke pass {type:"CUSTOM", label:"What size fleet do you run?"} and add options:[…] to make it a dropdown. Ask FEWER quest
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | internal name for the form — not shown to the person filling it in |
| questions | array | yes | Meta question types as strings ("FULL_NAME","EMAIL","PHONE","COMPANY_NAME","JOB_TITLE","CITY","ZIP","WORK_EMAIL","DATE_TIME"…) or objects {type,label,key,options[]} for CUSTOM |
| privacyPolicyUrl | string | yes | REQUIRED — the business’s privacy policy URL (https://…) |
| privacyPolicyLinkText | string | no | link text shown for the privacy policy URL on the form |
| pageId | string | no | which connected Page (required only if the profile has more than one) |
| headline | string | no | headline shown above the questions |
| locale | string | no | form language, e.g. EN_US, ES_ES, PT_BR, DE_DE |
| followUpActionUrl | string | no | https URL the person is sent to after submitting the form |
| contextCard | object | no | {title, bullets[], body, buttonText, style} — the screen shown before the questions |
| thankYou | object | no | {title, body, buttonType, buttonText, websiteUrl} — shown after submitting |
| isOptimizedForQuality | boolean | no | adds a review-and-confirm step: fewer leads, better ones |
| blockNonTargeted | boolean | no | true = hide the form from people the ad's targeting does not match |
| trackingParameters | object | no | extra tracking parameters for the form, as a key/value object |
Retire an INSTANT LEAD FORM so it stops collecting — the only way to tidy a Page’s Instant Forms list, because META DOES NOT SUPPORT DELETING A LEAD FORM AT ALL, only archiving it. This is REVERSIBLE: pass status:"ACTIVE" to switch an archived form back on. Archiving publishes nothing, spends nothing and DELETES NO LEADS — everything the form already collected stays readable with read_meta_leads.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| formId | string | yes | the form to retire (from list_meta_lead_forms) |
| status | string | no | default ARCHIVED; "ACTIVE" restores a form that was archived |
| pageId | string | no | which connected Page (required only if the profile has more than one) |
| confirm | boolean | no | must be true to actually change it — without it nothing changes and you get the read-back to show the user |
Read the LEADS a form (formId) or a single ad (adId) has collected — the real answers people submitted, fetched live from Meta. THIS RETURNS REAL PEOPLE’S CONTACT DETAILS (names, emails, phone numbers). Treat it as the user’s own customer data: show only what they asked for, never post a lead list anywhere public, and do not copy it into an unrelated document. Hermoso PULLS these on demand and kee
| Parameter | Type | Required | Notes |
|---|---|---|---|
| formId | string | no | every lead this form has ever collected (from list_meta_lead_forms) |
| adId | string | no | just the leads this ONE ad produced (from list_meta_ads) — pass this OR formId, never both |
| pageId | string | no | which connected Page (required only if the profile has more than one) |
| since | string | no | ISO date (2026-08-01) or epoch ms — only leads created after this |
| until | string | no | ISO date or epoch ms — only leads created before this |
| limit | number | no | 1–500, default 100 |
| cursor | string | no | paging cursor returned by a previous call |
| redact | boolean | no | mask the contact values, keeping counts / dates / attribution — use when the user only wants to know the form is working |
PERMANENTLY delete a Meta custom audience or lookalike. Meta’s own warning: "When you delete a custom audience, it will be permanently removed from your account and your ads using it will stop running." An audience is the one ad object whose value is its CONTENTS — a big retargeting list cannot be rebuilt, it has to re-accumulate — so CALL IT WITHOUT confirm FIRST: nothing is deleted, and you get
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | yes | ad account id (act_… or digits) |
| audienceId | string | yes | the custom audience id (from list_meta_audiences) |
| confirm | boolean | no | REQUIRED true — the deletion is permanent |
| confirmName | string | no | the audience’s EXACT name, required when it holds people or has lookalikes |
| confirmChildren | number | no | the exact number of derived lookalikes reported by the unconfirmed call, required when it has any |
ONE-TIME SETUP, and the FIRST thing to run when Merchant Center calls are being refused. Google blocks every Merchant API call until Hermoso's Google Cloud project is registered against the merchant's account, and says so verbatim ("GCP project ... is not registered with the merchant account"). This performs that link. It is NOT a broken connection and reconnecting cannot fix it. Google asks for u
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | the numeric id shown top-right in Merchant Center |
| developerEmail | string | yes | a contact address Google records against the registration; it is not used to sign in |
List the Google Merchant Center accounts this brand's connected Google account can reach. EVERY other Merchant tool needs the merchantCenterId this returns, and an agency often has several, so never guess one — ask the user which store. Read-only and free. If it is refused, the Merchant Center permission has not been granted yet; that is not a broken connection and reconnecting will not change it.
No parameter schema on file.
Read the products in a Merchant Center feed — what the merchant actually offers, which is what Shopping and retail Performance Max campaigns serve. Needs merchantCenterId from list_merchant_accounts. Read-only and free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | from list_merchant_accounts |
| limit | number | no | max products (default 50, max 250) |
Record an ad or campaign the merchant is running (on Meta, Google, TikTok, email, anywhere) in their Shopify admin under Marketing, so Shopify's own reports attribute storefront visits and sales to it by its UTM. REPORTING ONLY: it publishes nothing and spends nothing. Pass title, remoteUrl (where the ad is managed, e.g. the Ads Manager link), the utm {campaign, source, medium} the ad's links carr
| Parameter | Type | Required | Notes |
|---|---|---|---|
| title | string | no | what the merchant sees in Shopify > Marketing; required on the first recording |
| remoteUrl | string | no | https link where this ad or campaign is managed or viewed; required on the first recording |
| remoteId | string | no | your stable id for this activity (e.g. the ad platform campaign id); derived from platform + campaignId or the utm campaign when omitted |
| platform | string | no | where it runs, e.g. meta, google_ads, tiktok_ads; only used to derive remoteId |
| campaignId | string | no | the ad platform campaign id; only used to derive remoteId |
| utm | object | no | the UTM the ad links carry; Shopify matches visits on all three |
| tactic | string | no | Shopify marketing tactic, e.g. AD, RETARGETING, POST, NEWSLETTER; default AD |
| channel | string | no | SOCIAL, SEARCH, DISPLAY, EMAIL or REFERRAL; default SOCIAL |
| status | string | no | ACTIVE, PAUSED, SCHEDULED, INACTIVE, DELETED_EXTERNALLY or UNDEFINED; default ACTIVE |
| budget | object | no | budget object: {type DAILY or LIFETIME, amount, currency} |
| adSpend | object | no | amount spent so far |
| start | string | no | ISO date the ad started |
| end | string | no | ISO date it ended |
| scheduledEnd | string | no | ISO date the activity is scheduled to end |
| previewImageUrl | string | no | https image shown next to the activity (the ad creative works) |
The marketing activities recorded in the merchant's Shopify > Marketing by Hermoso (and other apps): title, tactic, channel, status, UTM, budget, ad spend, dates and the remoteId each was recorded under. Filter by remoteId or a Shopify search query (e.g. title:summer). Newest first; pass cursor for more. Read-only, free. Needs the store to have approved Hermoso's marketing permission. Only works f
| Parameter | Type | Required | Notes |
|---|---|---|---|
| limit | number | no | 1–100, default 25 |
| cursor | string | no | pageInfo cursor from the previous reply, to fetch the next page |
| remoteId | string | no | return only the activity recorded under this remoteId |
| query | string | no | Shopify search syntax, e.g. title:summer or tactic:AD |
Remove a marketing activity Hermoso recorded from the merchant's Shopify > Marketing, by remoteId or id. Shopify stops attributing visits and sales to it and it cannot be undone, so without confirm:true NOTHING is deleted and the reply names exactly what would go: show the user, get a yes, then call again with confirm:true. Read back afterwards. Only works for accounts created by installing Hermos
| Parameter | Type | Required | Notes |
|---|---|---|---|
| remoteId | string | no | remoteId of the activity to delete; use this or id |
| id | string | no | gid://shopify/MarketingActivity/… |
| confirm | boolean | no | true to delete; without it the reply names what would be removed and deletes nothing |
COMPETITOR INTELLIGENCE GOOGLE COMPUTES FOR FREE, for any retail brand with a Merchant Center. Ten report views, read with a SQL-like MCQL query. The three worth reaching for first: competitive_visibility_competitor_view (WHICH other domains appear beside this merchant, their rank, page-overlap and higher-position rates — i.e. who is actually beating them), best_sellers_product_cluster_view (w
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | from list_merchant_accounts — a STANDALONE account or a SUBACCOUNT, never a multi-client (MCA) account |
| query | string | yes | the MCQL query, e.g. "SELECT id, title, price, suggested_price, effectiveness FROM price_insights_product_view" |
| pageSize | number | no | rows per response, 1–100000 (default 1000). 100,000 is GOOGLE's ceiling, not ours. |
| pageToken | string | no | nextPageToken from a previous call — resend the IDENTICAL query and pageSize with it, which Google requires |
Read the account-level issues Google reports on a Merchant Center — the answer to "why is this product not showing?". Needs merchantCenterId from list_merchant_accounts. Read-only and free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | from list_merchant_accounts |
The sale and discount promotions on a Merchant Center account, with each one's PER-DESTINATION status and any issues Google has raised against it. GOOGLE VALIDATES PROMOTIONS ASYNCHRONOUSLY, so 'created' and 'running' are different states and only destinationStatuses tells them apart — never report a sale as live off a successful create. Read-only, 0 credits. Needs Google Ads connected (Merchant C
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | from list_merchant_accounts |
| limit | number | no | up to 250, default 50 |
PUT A SALE BADGE ON THE MERCHANT'S SHOPPING LISTINGS — the most seasonal thing a retail advertiser does. It creates the promotion and resolves (or creates) the promotion DATA SOURCE that country/language needs automatically; a promotion data source is its own kind and cannot be the product feed. IT IS NOT LIVE ON CREATE: Google validates promotions asynchronously and can take days to approve one,
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | from list_merchant_accounts |
| promotionId | string | yes | YOUR OWN id for the promotion, e.g. 'black-friday-2026' |
| targetCountry | string | yes | two-letter CLDR territory code, e.g. US. Part of the promotion identity and not changeable later |
| contentLanguage | string | yes | two-letter ISO 639-1 code, e.g. en. Must match the language of the products |
| longTitle | string | yes | the promotion text shoppers see, e.g. '20% off all winter coats' — describe the offer, do not name the campaign |
| couponValueType | string | yes | the kind of discount |
| offerType | string | yes | NO_CODE when the discount applies automatically, GENERIC_CODE when the shopper types a code |
| genericRedemptionCode | string | no | REQUIRED when offerType is GENERIC_CODE |
| percentOff | number | no | REQUIRED for PERCENT_OFF — a plain number, e.g. 20 |
| moneyOffAmount | object | no | REQUIRED for MONEY_OFF — {"amountMicros":"10000000","currencyCode":"USD"} is $10 off |
| redemptionChannel | array | yes | at least one; ONLINE for a webshop sale |
| promotionDestinations | array | yes | at least one, e.g. ['SHOPPING_ADS'] for paid Shopping or ['FREE_LISTINGS'] for the unpaid surface |
| productApplicability | string | no | required unless eventApplicability is set — Google allows exactly one of the two |
| eventApplicability | string | no | creates a SALES EVENT instead of a product promotion — a different thing |
| startDate | string | yes | ISO date, e.g. 2026-11-27 |
| endDate | string | yes | ISO date; widened to the END of that day, because Google's interval end is exclusive |
| promotionUrl | string | no | the page on the merchant's site where the promotion shows |
| attributes | object | no | any other promotion attribute Google publishes — item/brand/product-type filters, minimum purchase, store filters, display period — forwarded as given |
| customAttributes | array | no | merchant-provided attributes, {name, value} |
GOOGLE'S OWN FIX STEPS, not just the name of the problem. list_merchant_issues stops at the symptom; this returns the remediation content Google writes for each issue, plus the ACTIONS available on it — request a review, claim the website, edit a named attribute — with the exact opaque ids trigger_merchant_issue_action needs. Pass an offerId (with feedLabel, and contentLanguage if it is not 'en')
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | from list_merchant_accounts |
| offerId | string | no | for one PRODUCT's issues; omit for the account's issues |
| feedLabel | string | no | REQUIRED alongside offerId — from list_merchant_products. A product is addressed by contentLanguage + feedLabel + offerId |
| contentLanguage | string | no | with offerId; defaults to 'en' |
| languageCode | string | no | BCP-47 language for the fix text, e.g. 'en-US' |
| timeZone | string | no | IANA zone for times inside the content, e.g. 'America/Los_Angeles' |
FIRE one of the actions merchant_issue_help returned — most usefully 'request a review' on a disapproval. SEVERAL OF THESE ARE ONE-SHOT and Google says so in its own dialog callout ('You can only request a review for disagreeing with this issue once. If it's not approved, you'll need to fix the issue and wait'), so read that callout back to the user before confirming: a review spent on the wrong i
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | from list_merchant_accounts |
| actionContext | string | yes | the opaque token from merchant_issue_help, on the action you are firing |
| actionFlowId | string | yes | the id of the flow you picked from that action's flows — each asks for different inputs |
| inputValues | array | no | values for the flow's input fields, shaped as Google described them: {inputFieldId, textInputValue|choiceInputValue|checkboxInputValue} |
| languageCode | string | no | BCP-47 language for the response message |
| confirm | boolean | no | must be true to actually fire it |
The per-store and per-region price and availability overrides on one Shopping product. An override REPLACES the product's own price and availability for that store or region; where there is no override the product-level values apply, so an empty result is the normal answer and does NOT mean the product is missing. Needs the product's offerId plus its contentLanguage and feedLabel, because in the M
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | from list_merchant_accounts |
| offerId | string | yes | the product's own id in the feed, from list_merchant_products |
| contentLanguage | string | yes | two-letter feed language, e.g. "en". Part of the product's address, not a filter |
| feedLabel | string | yes | usually the feed target country, e.g. "US". Part of the product's address, not a filter |
| kind | string | no | omit to read both |
SET the price, availability or stock level of one Shopping product at ONE physical store (kind:"local", keyed by storeCode) or in ONE region (kind:"regional", keyed by region). This is what stops a Shopping ad advertising something the nearest store has sold out of. Google's insert REPLACES the whole entry, so this tool reads what is already stored, merges your fields over it, and reports which st
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | from list_merchant_accounts |
| offerId | string | yes | the product's own id in the feed |
| contentLanguage | string | yes | two-letter feed language, e.g. "en" |
| feedLabel | string | yes | usually the feed target country, e.g. "US" |
| kind | string | yes | local = one physical store (storeCode), regional = one Google region (region) |
| storeCode | string | no | required for kind:'local' — the store ID from the merchant's Business Profile. Immutable |
| region | string | no | required for kind:'regional' — the Merchant Center region id. Immutable |
| availability | string/null | no | no API enum exists; Google's local spec publishes In stock, Limited availability, On display to order, Out of stock, in English. Anything else is warned about, not refused. null clears it |
| price | string/null | no | "12.99 USD". A bare number is refused because a price with no currency is not a price. null clears it |
| salePrice | string/null | no | "9.99 USD". Google requires salePriceEffectiveDate alongside it if that is set. null clears it |
| quantity | number/null | no | local only. null clears it |
| pickupMethod | string/null | no | local only. Must be sent WITH pickupSla unless the value is "not supported" |
| pickupSla | string/null | no | local only |
| instoreProductLocation | string/null | no | local only, max 20 bytes |
WHY THE MERCHANT CENTER ACCOUNT IS OR IS NOT SERVING, in one read. Do not trust the program state on its own: an account can report Shopping ads and free listings as ENABLED and serve in ZERO countries, because Google counts a region as active only where every requirement for that program is met. This reports the active regions, the unmet requirements Google names with its own help link for each,
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | from list_merchant_accounts |
FIX THE MERCHANT CENTER SETTINGS that stop an account serving (merchant_account_status names them). Pick section + action. shipping: get · upsert (add or update services by name; services you do not mention are KEPT) · remove (serviceNames) · replace (exactly this list). A service is {name, countries, currency, rate, freeOver?, handlingDays:[min,max], transitDays:[min,max], active?} or Google's ow
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | from list_merchant_accounts |
| section | string | yes | shipping, returns, business_info, homepage or programs; the Merchant Center area to act on |
| action | string | no | defaults to the section's read (get/list) |
| services | array | no | shipping upsert/replace |
| serviceNames | array | no | shipping remove |
| warehouses | array | no | shipping: Google Warehouse objects; omitted = existing ones kept |
| policy | object | no | returns create |
| returnPolicyId | string | no | returns delete, from list |
| address | object | no | business_info: regionCode, street or addressLines, locality, administrativeArea, postalCode |
| customerService | object | no | business_info: uri, email, phone (E.164) |
| koreanBusinessRegistrationNumber | string | no | Korean business registration number; business_info update only |
| uri | string | no | homepage set_uri |
| overwrite | boolean | no | homepage claim: take the claim from another account |
| program | string | no | programs enable/disable |
| confirm | boolean | no | true ONLY after the user approved the change the first call described |
Merchant Center API quota and account limits, which is the difference between "the API refused" and "you are out of daily quota". Two different things in one answer: quota groups are how many API CALLS each method group has left today, and account limits are how many PRODUCTS the account may hold per destination. Google resets the daily quota at 12:00 PM MIDDAY UTC, not at midnight. If one half of
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | from list_merchant_accounts |
PUSH INSTEAD OF POLLING for Merchant Center product status. Google POSTs to a URL the moment a product's status changes — a disapproval is otherwise found whenever someone happens to run list_merchant_issues, which for a feed of thousands means money burning unnoticed. action:'list' shows the subscriptions, 'create' adds one, 'delete' removes one. BE HONEST ABOUT WHERE IT GOES: callBackUri must be
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | from list_merchant_accounts |
| action | string | no | defaults to 'list' |
| callBackUri | string | no | HTTPS endpoint Google will POST to (create). Not a Hermoso URL and not localhost |
| registeredEvent | string | no | defaults to PRODUCT_STATUS_CHANGE, the one that fires on a disapproval |
| allManagedAccounts | boolean | no | subscribe for every managed account instead of just this one — mutually exclusive with targetAccount |
| targetAccount | string | no | accounts/{id} to receive notifications for; defaults to this account |
| subscriptionId | string | no | for action:'delete', from action:'list' |
WHERE MERCHANT CENTER GETS ITS CONVERSION DATA FROM, which is what free-listing and Shopping performance reporting is built on — a merchant with no conversion source sees clicks and no outcomes, and nothing in Merchant Center says why. Two kinds, and they are different jobs: a MERCHANT CENTER DESTINATION is a Google tag target, and creating one returns a destination id (MC-…) that is the only plac
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | from list_merchant_accounts |
| action | string | no | defaults to 'list' |
| kind | string | no | for action:'create' — which kind to make. Passing propertyId implies the Google Analytics one |
| displayName | string | no | the name that identifies a tag destination in the Merchant Center UI, e.g. "Website purchases". Required to create one; may also name an existing source for update/delete |
| currencyCode | string | no | three-letter ISO 4217 code the conversions are reported in, e.g. USD. Required when creating a tag destination — it decides what every number reported through it means |
| attributionModel | string | no | CROSS_CHANNEL_LAST_CLICK (default) | ADS_PREFERRED_LAST_CLICK | CROSS_CHANNEL_DATA_DRIVEN | CROSS_CHANNEL_FIRST_CLICK | CROSS_CHANNEL_LINEAR | CROSS_CHANNEL_POSITION_BASED | CROSS_CHANNEL_TIME_DECAY |
| attributionLookbackWindowDays | number | no | 7, 30 or 40 — Google publishes no other value. Defaults to 30 |
| conversionTypes | array | no | names of the conversion types events may be classified as. IMMUTABLE once set, and Google returns it on no read, so it can never be confirmed afterwards — leave it off and Google creates a standard "p |
| propertyId | string | no | NUMERIC GA4 property id for a Google Analytics link (from list_analytics_properties), not a G- measurement id |
| conversionSourceId | string | no | for update, delete and undelete — from action:'list' |
| showDeleted | boolean | no | include archived sources in a list |
| confirm | boolean | no | must be true to actually archive |
List the data sources (feeds) on a Merchant Center account and say which of them can actually take a product write. Do this BEFORE creating or deleting a product: writes go into a data source, and Google only accepts them into an API-input product feed — a file feed, the feed Merchant Center's own UI creates, and an autofeed all list here and all REFUSE writes, so picking the first row would pick
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | from list_merchant_accounts |
Create an API product feed on a Merchant Center account — the container upsert_merchant_product writes into. Most accounts have none until one is made: a store set up through the Merchant Center UI has a UI feed, which is read-only. feedLabel and contentLanguage must be set TOGETHER or not at all and BOTH ARE IMMUTABLE — leave them off and the feed accepts products for any market and language, whi
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | from list_merchant_accounts |
| displayName | string | yes | the name the merchant will see in Merchant Center |
| feedLabel | string | no | market bucket, e.g. "US" — max 20 chars, A-Z/0-9/dashes. IMMUTABLE, and must be set together with contentLanguage |
| contentLanguage | string | no | two-letter ISO 639-1, e.g. "en". IMMUTABLE, and must be set together with feedLabel |
| countries | array | no | CLDR territory codes the items may be shown in |
| destinations | array | no | omit to inherit the account's own program participation (SHOPPING_ADS, FREE_LISTINGS, …) |
Create or replace a product in a Merchant Center feed — this is how a merchant's catalogue gets populated, and it is what Shopping ads, free listings and retail Performance Max actually serve. It is an UPSERT and a WHOLE-ROW write: calling it again for the same offerId REPLACES the product rather than patching it, so send every field you want kept each time. You do not have to find a feed first —
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | from list_merchant_accounts |
| offerId | string | yes | your own unique id for the product — IMMUTABLE, and what you update or delete it by later |
| dataSource | string | no | the feed to write into, from list_merchant_data_sources. Omit to resolve the account's one writable feed (created if there is none, refused by name if there are several) |
| dataSourceName | string | no | display name for a feed created on the fly |
| contentLanguage | string | no | two-letter, e.g. "en" — taken from the feed when the feed declares one |
| feedLabel | string | no | market bucket, e.g. "US" — taken from the feed when the feed declares one |
| title | string | no | product title; Google needs it for the product to serve |
| description | string | no | product description; Google needs it for the product to serve |
| link | string | no | the product page URL on the merchant's store |
| imageLink | string | no | main product image URL (https); Google needs it for the product to serve |
| additionalImageLinks | array | no | extra product image URLs, as a list |
| availability | string | no | in_stock | out_of_stock | preorder | backorder |
| condition | string | no | new | refurbished | used |
| price | object | no | price object: {value in major units or amountMicros, currencyCode} |
| salePrice | object | no | sale price, same shape as price |
| brand | string | no | brand name |
| gtin | string | no | GTIN barcode (UPC, EAN or similar) |
| mpn | string | no | manufacturer part number |
| identifierExists | boolean | no | false for a product with no GTIN/MPN/brand, such as a service or a bundle |
| googleProductCategory | string | no | Google product taxonomy category |
Delete a product from a Merchant Center feed. CONFIRM-GATED: without confirm:true nothing is deleted and it reports the real product it WOULD delete — title, price and availability read back from Google — because the id you were given proves nothing about what is actually there. It is recoverable: re-inserting the same offerId re-creates the product. Needs the same offerId + contentLanguage + feed
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | from list_merchant_accounts |
| offerId | string | yes | your offer id of the product to delete |
| dataSource | string | no | the feed it lives in, from list_merchant_data_sources |
| contentLanguage | string | no | two-letter language code of the product; taken from the feed when it declares one |
| feedLabel | string | no | market label of the product, e.g. US; taken from the feed when it declares one |
| confirm | boolean | no | must be true to actually delete; without it nothing is deleted and the real product is reported back |
Change SOME fields on a product already in a Merchant Center feed — the everyday operation, because price and availability move daily. This is a PATCH: only the attributes you name are touched and everything else on the product survives, which is the difference from upsert_merchant_product (a whole-row write that wipes anything you leave out). It refuses if the product does not exist rather than q
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | from list_merchant_accounts |
| offerId | string | yes | the product to change |
| dataSource | string | no | the feed it lives in, from list_merchant_data_sources |
| contentLanguage | string | no | two-letter language code; taken from the feed when it declares one |
| feedLabel | string | no | market label, e.g. US; taken from the feed when it declares one |
| title | string | no | new product title; omit to leave unchanged |
| description | string | no | new product description; omit to leave unchanged |
| link | string | no | product page URL on the merchant's store |
| imageLink | string | no | main product image URL (https) |
| additionalImageLinks | array | no | extra image URLs, sent as the whole list |
| availability | string | no | in_stock | out_of_stock | preorder | backorder |
| condition | string | no | new | refurbished | used |
| price | object | no | new price: {value in major units or amountMicros, currencyCode} |
| salePrice | object | no | new sale price, same shape as price |
| brand | string | no | brand name |
| gtin | string | no | GTIN barcode (UPC, EAN or similar) |
| mpn | string | no | manufacturer part number |
| identifierExists | boolean | no | false for a product with no GTIN, MPN or brand, such as a service or bundle |
| googleProductCategory | string | no | Google product taxonomy category |
| productTypes | array | no | merchant-defined category labels, as a list |
Delete a data source (feed) from a Merchant Center account. CONFIRM-GATED and the heavier of the two deletes: without confirm:true nothing is deleted and it reports the feed plus HOW MANY PRODUCTS would be destroyed with it, counted live from Google. This is NOT recoverable the way a product delete is — the products would have to be re-inserted into a new feed, and feedLabel/contentLanguage are im
| Parameter | Type | Required | Notes |
|---|---|---|---|
| merchantCenterId | string | yes | from list_merchant_accounts |
| dataSource | string | yes | the feed to delete, from list_merchant_data_sources |
| confirm | boolean | no | must be true to actually delete; without it nothing is deleted and the product count that would go with it is reported |
Read the connected Google Ads account(s). Call with NO customerId to list the accessible accounts (customerId + name + currency) — do this first to pick a target. Call WITH customerId to list that account’s campaigns (id, name, status, daily budget, channel) plus performance metrics (impressions, clicks, CTR, avg CPC, cost, conversions). Date window: datePreset (LAST_7_DAYS | LAST_30_DAYS | TODAY
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | 10-digit account id (dashes ok) — omit to list accessible accounts |
| status | string | no | filter campaigns by status |
| datePreset | string | no | metrics window preset (default LAST_30_DAYS) |
| since | string | no | start date YYYY-MM-DD (with until) |
| until | string | no | end date YYYY-MM-DD |
| metrics | boolean | no | include performance metrics (default true) |
| loginCustomerId | string | no | manager (MCC) id — only if reaching a client account through a manager |
| limit | number | no | max campaigns (1–500, default 100) |
Use this when the user asks how their Google Ads campaigns performed. Run a GAQL (Google Ads Query Language) report for detailed performance breakdowns — ad groups, ads, keywords, search terms, demographics, geo. Pass customerId + a GAQL query (SELECT … FROM <resource> WHERE segments.date DURING LAST_30_DAYS). Allowed FROM resources: campaign, ad_group, ad_group_ad, keyword_view, campaign_budget,
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
| query | string | yes | GAQL, e.g. "SELECT ad_group.name, metrics.clicks, metrics.cost_micros FROM ad_group WHERE segments.date DURING LAST_7_DAYS" |
| loginCustomerId | string | no | manager id if operating through an MCC |
WHAT CHANGED ON THE ACCOUNT, AND WHEN — the answer to "performance fell off a cliff on Tuesday, what happened?", and the Google twin of reddit_ads_history. source:"change_event" (default) is FIELD-LEVEL over the last 30 days: the change time, who made it, from which client (web UI, API, scripts, bulk upload, automated rule), whether it was a CREATE / UPDATE / REMOVE, and exactly which fields moved
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | 10-digit account id — omit to use the brand’s selected default account |
| source | string | no | default change_event (30 days, field-level). change_status is 90 days and is the only one that sees Google Ads Editor. |
| since | string | no | YYYY-MM-DD, default 14 days ago |
| until | string | no | YYYY-MM-DD, default today |
| limit | number | no | rows, max 10000 — Google’s own ceiling, and the clamp is reported |
| detail | boolean | no | change_event only — include the old/new resource snapshots |
| loginCustomerId | string | no | manager (MCC) account id when acting through a manager account; omit otherwise |
Build a campaign on a connected Google Ads account. ALWAYS created PAUSED — it spends NOTHING until you enable it with set_google_ads_status(confirm:true). Google's object graph is campaign -> ad group -> ad, so a campaign ON ITS OWN CANNOT SERVE AN IMPRESSION: pass adGroup{name, ad{headlines,descriptions,finalUrls}, keywords[]} and this builds budget + campaign + location/language targeting + ad
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | 10-digit account id (from list_google_ads_campaigns) — omit to use the brand’s selected default account |
| name | string | yes | campaign name |
| dailyBudgetUsd | number | no | daily budget in the ACCOUNT’s currency (historical name — not necessarily USD; 1–100000) — creates a budget inline; required unless budgetResourceName is given |
| budgetResourceName | string | no | reuse an existing budget instead of creating one |
| channelType | string | no | default SEARCH |
| searchPartners | boolean | no | SEARCH only — also serve on Google search partners (default false) |
| bidding | object | no | how the campaign bids — default MANUAL_CPC |
| locations | array | no | location NAMES to target, e.g. ["United States"] or ["Toronto","Vancouver"]. WITHOUT this the campaign runs WORLDWIDE — the most expensive default in Google Ads |
| excludedLocations | array | no | location names to EXCLUDE |
| languages | array | no | ISO language codes, e.g. ["en","fr"] |
| startDate | string | no | YYYY-MM-DD |
| endDate | string | no | YYYY-MM-DD |
| adGroup | object | no | build the serving tree in the same atomic call — WITHOUT this you get a campaign shell that can never show an ad |
| finalUrlSuffix | string | no | UTMs appended to every click, e.g. "utm_source=google&utm_medium=cpc". Omit for our default (utm_source/medium/campaign + {campaignid}/{creative}); "" for none |
| trackingUrlTemplate | string | no | click-tracking redirect, e.g. "{lpurl}?src=x" — only if you use a tracker |
| urlCustomParameters | object | no | up to 8 custom parameters, used in URLs as {_key} |
| containsEuPoliticalAds | boolean | no | EU Political Advertising Regulation declaration. Google REQUIRES one on every campaign. Default false (a normal commercial ad) — set true ONLY for genuine EU political advertising |
| dryRun | boolean | no | validate the WHOLE tree against Google without creating anything. Nothing is written and no budget is consumed |
| loginCustomerId | string | no | manager id if operating through an MCC |
Add an ad group to an EXISTING Google Ads campaign — the level between a campaign and its ads. Google requires it: a campaign with no ad group cannot serve. Optionally build its ad and keywords in the same ATOMIC call. The ad-group type is taken from the campaign's channel automatically. Created PAUSED and read back from Google before you are told it exists. If the parent campaign is already LIVE
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | omit to use the brand’s selected default account |
| campaignId | string | yes | the campaign this ad group belongs to |
| name | string | yes | ad group name |
| cpcBidUsd | number | no | max CPC for this ad group — omit to inherit the campaign bidding |
| status | string | no | default PAUSED |
| ad | object | no | build the ad in the same atomic call |
| keywords | array | no | a SEARCH ad group with no keywords never shows |
| confirm | boolean | no | set true ONLY after the user explicitly approved starting spend — required when the parent campaign is already LIVE (ENABLED) and this object would serve immediately |
| dryRun | boolean | no | true = validate with Google and create nothing |
| loginCustomerId | string | no | manager (MCC) account id when acting through a manager account; omit otherwise |
Create the actual AD inside a Google Ads ad group — this is the object that carries the creative; a campaign or ad group alone shows nothing. On a SEARCH campaign it builds a RESPONSIVE SEARCH AD: 3–15 headlines (≤30 chars), 2–4 descriptions (≤90 chars), at least one finalUrl, optional path1/path2. On a DISPLAY campaign it builds a RESPONSIVE DISPLAY AD: headlines, longHeadline, descriptions, busi
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | omit to use the brand’s selected default account |
| adGroupId | string | yes | the ad group this ad lives in |
| finalUrls | array | no | the landing page(s) — at least one is required |
| headlines | array | no | SEARCH: 3–15 headlines, each ≤30 characters. DISPLAY: 1–5. Pass a plain string, or {text, pin} to PIN one to a fixed slot (a brand name or legal line). |
| descriptions | array | no | SEARCH: 2–4 descriptions, each ≤90 characters. DISPLAY: 1–5. Pass a plain string, or {text, pin} to pin it. |
| path1 | string | no | SEARCH only — display-URL path segment, ≤15 chars |
| path2 | string | no | SEARCH only — second display-URL path segment, ≤15 chars |
| longHeadline | string | no | DISPLAY only — ≤90 characters |
| businessName | string | no | DISPLAY only — ≤25 characters |
| marketingImages | array | no | DISPLAY only — landscape 1.91:1 asset resource names from upload_google_ads_asset |
| squareMarketingImages | array | no | DISPLAY only — square 1:1 asset resource names |
| logoImages | array | no | DISPLAY only — logo asset resource names |
| status | string | no | default PAUSED |
| confirm | boolean | no | set true ONLY after the user explicitly approved starting spend — required when the parent campaign is already LIVE (ENABLED) and this object would serve immediately |
| dryRun | boolean | no | true = validate with Google and create nothing |
| loginCustomerId | string | no | manager (MCC) account id when acting through a manager account; omit otherwise |
Add keywords — and NEGATIVE keywords — to a Google Ads ad group. A Search ad group with no keywords never shows. Each keyword takes text (≤80 chars, ≤10 words) and matchType EXACT | PHRASE | BROAD (default PHRASE). Set negative:true to BLOCK a term instead of targeting it, which is the cheapest way to stop wasted spend. Read back from Google before you are told they exist. If the parent campaign a
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | omit to use the brand’s selected default account |
| adGroupId | string | yes | the ad group to add them to |
| keywords | array | yes | the keywords to add |
| confirm | boolean | no | set true ONLY after the user explicitly approved starting spend — required when the parent campaign is already LIVE (ENABLED) and this object would serve immediately |
| dryRun | boolean | no | true = validate with Google and create nothing |
| loginCustomerId | string | no | manager (MCC) account id when acting through a manager account; omit otherwise |
ADD locations and languages to an existing Google Ads campaign. THIS ADDS; IT DOES NOT REPLACE — Google campaign criteria are a list, this call only ever creates entries, and there is no remove operation here. So a campaign already targeting the United States that you "change to Canada" ends up targeting BOTH and still spending in the US; the read-back names every pre-existing location and languag
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | omit to use the brand’s selected default account |
| campaignId | string | yes | the campaign to target |
| locations | array | no | location NAMES to target |
| excludedLocations | array | no | location NAMES to exclude |
| languages | array | no | ISO language codes, e.g. ["en","es"] |
| countryCode | string | no | 2-letter hint to disambiguate a city name, e.g. CA for "London" |
| confirm | boolean | no | REQUIRED true to change a LIVE (ENABLED) campaign |
| dryRun | boolean | no | true = validate with Google and change nothing |
| loginCustomerId | string | no | manager (MCC) account id when acting through a manager account; omit otherwise |
Change how an existing Google Ads campaign bids: MANUAL_CPC (optionally enhanced), MAXIMIZE_CLICKS (needs maxCpcUsd on an existing campaign — Google requires the CPC ceiling on that change), MAXIMIZE_CONVERSIONS, MAXIMIZE_CONVERSION_VALUE, TARGET_CPA (needs targetCpaUsd) or TARGET_ROAS (needs targetRoas, e.g. 4 = $4 revenue per $1 spent). TARGET_CPA and TARGET_ROAS are applied as Google's own v25
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | omit to use the brand’s selected default account |
| campaignId | string | yes | the campaign to change |
| strategy | string | yes | the bidding strategy |
| targetCpaUsd | number | no | REQUIRED for TARGET_CPA |
| targetRoas | number | no | REQUIRED for TARGET_ROAS — e.g. 4 = $4 revenue per $1 spent |
| maxCpcUsd | number | no | MAXIMIZE_CLICKS — the max CPC ceiling; REQUIRED when switching an existing campaign to it |
| enhancedCpc | boolean | no | DO NOT SET true — Google retired Enhanced CPC for new campaigns and answers OPERATION_NOT_PERMITTED_FOR_CONTEXT (measured live 2026-08-05); Hermoso refuses it up front with the reason. Use MAXIMIZE_CO |
| confirm | boolean | no | REQUIRED true to change a LIVE (ENABLED) campaign |
| dryRun | boolean | no | true = validate with Google and change nothing |
| loginCustomerId | string | no | manager (MCC) account id when acting through a manager account; omit otherwise |
Look up Google Ads location targets by name — turns "Toronto" / "California" / "United Kingdom" into the geo target ids Google needs, with each one's type (COUNTRY, STATE, CITY, POSTAL_CODE…) and reach. Use it when a location name is ambiguous, or to show the user exactly which place you are about to target. Read-only and free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| query | string | yes | one location name, or several comma-separated (up to 25) |
| countryCode | string | no | 2-letter hint, e.g. CA to disambiguate "London" |
| loginCustomerId | string | no | manager (MCC) account id when acting through a manager account; omit otherwise |
Create a new daily budget, or change an existing budget’s daily amount (pass budgetResourceName). Raising the budget on a LIVE (ENABLED) campaign increases real spend immediately — you MUST show the user the new daily amount, get an explicit yes, then call with confirm:true. Creating a budget or lowering one on a paused campaign is safe.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
| dailyBudgetUsd | number | yes | daily budget in the ACCOUNT’s currency (historical name — not necessarily USD; 1–100000) |
| budgetResourceName | string | no | existing budget to UPDATE — omit to CREATE a new budget |
| name | string | no | name for a newly created budget |
| confirm | boolean | no | REQUIRED true to raise the budget of a LIVE campaign |
| loginCustomerId | string | no | manager id if operating through an MCC |
Turn a campaign, AD GROUP or AD ON (ENABLED), OFF (PAUSED) or REMOVED. Pass level:"campaign" + campaignId, level:"adGroup" + adGroupId, or level:"ad" + BOTH adGroupId and adId (Google keys an ad by adGroupId~adId). ENABLING STARTS REAL AD SPEND — you MUST first show the user the campaign name + its daily budget, get an explicit yes, then call with status:"ENABLED" and confirm:true. Pausing is alwa
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
| level | string | no | what to change — default campaign |
| campaignId | string | no | campaign id (level:"campaign") |
| adGroupId | string | no | ad group id (level:"adGroup", or with adId for level:"ad") |
| adId | string | no | ad id (level:"ad" — pass adGroupId too) |
| campaignResourceName | string | no | full resource name, e.g. customers/{cid}/campaigns/{id} |
| status | string | yes | ENABLED = start spending; PAUSED = stop; REMOVED = permanent |
| confirm | boolean | no | REQUIRED true to ENABLE (real spend) or to REMOVE (permanent) |
| confirmName | string | no | when archiving or removing: its EXACT name, as the unconfirmed call names it. Required when it has children or is live |
| confirmChildren | number | no | when archiving or removing: the exact number of children the unconfirmed call reported. Required when it has any |
| loginCustomerId | string | no | manager id if operating through an MCC |
Google Ads WATERFALL AND NETWORK CAPS: which network a budget spends on first and how much the others may take. action 'read' (default), 'set' (settings) or 'clear', on the ACCOUNT DEFAULT or, with budgetId, on one campaign budget, which then wins. Every channel a campaign type supports must be in a tier and caps only go on tier2; Google says which if not. A budget's read says whether it can use t
| Parameter | Type | Required | Notes |
|---|---|---|---|
| action | string | no | default 'read' |
| customerId | string | no | 10-digit account id (dashes ok); omit to use the brand’s selected default account |
| budgetId | string | no | numeric campaign budget id; omit for the account default |
| settings | array | no | for set: one entry per campaign type |
| confirm | boolean | no | REQUIRED true for set and clear, after the user approved |
| dryRun | boolean | no | validate with Google, write nothing |
| loginCustomerId | string | no | manager id if operating through an MCC |
Ask Google whether a Business Profile listing can run LOCAL SERVICES ads on this Google Ads account: eligible or not, why not, and which lead types and categories it qualifies for. Read-only.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| listingId | string | yes | numeric Business Profile location id |
| customerId | string | no | 10-digit account id (dashes ok); omit to use the brand’s selected default account |
| loginCustomerId | string | no | manager id if operating through an MCC |
Change WHERE an existing Google Ads campaign serves: Google Search, Search partners (target_search_network) and the Display Network (target_content_network), each true/false. The classic use is turning Search partners OFF on a Search campaign, or Display off. Current settings are read first and a no-op says so. On a LIVE (ENABLED) campaign this moves real spend on the next auction — show the user
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
| campaignId | string | yes | the campaign to change |
| googleSearch | boolean | no | serve on Google Search (target_google_search) |
| searchPartners | boolean | no | serve on Google search partner sites (target_search_network) |
| display | boolean | no | serve on the Google Display Network (target_content_network) |
| confirm | boolean | no | REQUIRED true to change a LIVE (ENABLED) campaign |
| dryRun | boolean | no | validate with Google, write nothing |
| loginCustomerId | string | no | manager id if operating through an MCC |
WHEN a Google Ads campaign runs and how much more or less it bids by device, location and hour. Pass only campaignId to read the current schedule and adjustments. schedule:[{day: MONDAY…SUNDAY | WEEKDAYS | WEEKENDS | EVERYDAY, start:"09:00", end:"17:00", bidModifier?}] REPLACES the schedule (scheduleMode:"add" appends; clearSchedule:true runs it all day again); account time zone, quarter hours, en
| Parameter | Type | Required | Notes |
|---|---|---|---|
| campaignId | string | yes | the campaign |
| adGroupId | string | no | for adGroupDevices |
| schedule | array | no | ad schedule slots; replaces the current schedule unless scheduleMode is add |
| scheduleMode | string | no | replace (default) or add to the current schedule |
| clearSchedule | boolean | no | remove the schedule so it runs all day |
| devices | object | no | campaign-level device multipliers |
| adGroupDevices | object | no | ad-group device multipliers (needs adGroupId) |
| locations | array | no | bid adjustments for locations the campaign already targets |
| customerId | string | no | omit to use the selected default account |
| confirm | boolean | no | true only after the user approved a change to a LIVE campaign or a delete |
| dryRun | boolean | no | validate with Google, change nothing |
| loginCustomerId | string | no | manager id if operating through an MCC |
Shared NEGATIVE KEYWORD LISTS: one list of blocked searches reused across Google Ads campaigns, or applied to the whole account. action: list (with sharedSetId shows its keywords) | create (name, keywords, campaignIds or accountLevel:true) | add (keywords) | remove_keywords (keywords or criterionIds) | attach / detach (campaignIds) | delete. keywords are "text" or {text, matchType: BROAD|PHRASE|EX
| Parameter | Type | Required | Notes |
|---|---|---|---|
| action | string | no | default list |
| sharedSetId | string | no | the list (from action list) |
| name | string | no | create |
| keywords | array | no | keywords to block, as text strings or {text, matchType BROAD, PHRASE or EXACT} objects |
| criterionIds | array | no | remove_keywords by id |
| campaignIds | array | no | campaign ids to attach to or detach from the shared list |
| accountLevel | boolean | no | create: apply to every campaign on the account |
| customerId | string | no | omit to use the selected default account |
| confirm | boolean | no | true only after the user approved a change to a LIVE campaign or a delete |
| dryRun | boolean | no | validate with Google, change nothing |
| loginCustomerId | string | no | manager id if operating through an MCC |
Google Ads LABELS for organising campaigns, ad groups and ads ("Made by Hermoso", "Q4 test"). action: list | create (name, color #RRGGBB, description, and targets to label at once) | update (name, color, description) | apply / remove (campaignIds, adGroupIds, ads:[{adGroupId, adId}]) | delete (confirm:true). Labels never change delivery or spend. Report by label with google_ads_report over campaig
| Parameter | Type | Required | Notes |
|---|---|---|---|
| action | string | no | default list |
| labelId | string | no | the label (from action list) |
| name | string | no | label name, for create or update |
| color | string | no | #RRGGBB |
| description | string | no | up to 200 characters |
| campaignIds | array | no | campaign ids to apply or remove the label on |
| adGroupIds | array | no | ad group ids to apply or remove the label on |
| ads | array | no | ads to label, as a list of {adGroupId, adId} objects |
| customerId | string | no | omit to use the selected default account |
| confirm | boolean | no | true only after the user approved a change to a LIVE campaign or a delete |
| dryRun | boolean | no | validate with Google, change nothing |
| loginCustomerId | string | no | manager id if operating through an MCC |
PORTFOLIO bid strategies: one shared Google Ads strategy and target driving several campaigns. action: list | create (name, strategy, its targets, optional campaignIds to attach) | update (targets or name; the type cannot change) | attach (campaignIds) | delete (only once no campaign uses it; confirm:true). Amounts are in the account currency. Changing targets of, or attaching, a LIVE campaign cha
| Parameter | Type | Required | Notes |
|---|---|---|---|
| action | string | no | default list |
| biddingStrategyId | string | no | the strategy (from action list) |
| name | string | no | strategy name, for create or update |
| strategy | string | no | create |
| targetCpaUsd | number | no | cost per conversion |
| targetRoas | number | no | 4 = 4x revenue per unit spent |
| maxCpcUsd | number | no | max CPC ceiling (required for TARGET_IMPRESSION_SHARE) |
| minCpcUsd | number | no | min CPC floor |
| impressionSharePercent | number | no | 1-100 |
| impressionShareLocation | string | no | where to aim impression share: ANYWHERE_ON_PAGE, TOP_OF_PAGE or ABSOLUTE_TOP_OF_PAGE |
| campaignIds | array | no | campaign ids to attach to the strategy |
| allowNoConversionTracking | boolean | no | skip the conversion-tracking check |
| customerId | string | no | omit to use the selected default account |
| confirm | boolean | no | true only after the user approved a change to a LIVE campaign or a delete |
| dryRun | boolean | no | validate with Google, change nothing |
| loginCustomerId | string | no | manager id if operating through an MCC |
Which conversion goals Google Ads smart bidding optimises toward, the usual cause of "it optimises for the wrong thing". With no goals it reads: the account goals, or with campaignId that campaign's goals and whether it follows the account. goals:[{category (PURCHASE, SIGNUP, SUBMIT_LEAD_FORM, PAGE_VIEW...), origin (default WEBSITE), biddable}] sets them account-wide, or for one campaign with camp
| Parameter | Type | Required | Notes |
|---|---|---|---|
| campaignId | string | no | omit for the account goals |
| goals | array | no | conversion goals to set, as a list of {category, origin, biddable}; omit to read the current goals |
| useAccountGoals | boolean | no | put the campaign back on the account goals |
| customerId | string | no | omit to use the selected default account |
| confirm | boolean | no | true only after the user approved a change to a LIVE campaign or a delete |
| dryRun | boolean | no | validate with Google, change nothing |
| loginCustomerId | string | no | manager id if operating through an MCC |
PERMANENTLY remove a Google Ads object. Google has no HTTP delete — removal is a remove operation that puts the object in the terminal REMOVED state, which cannot be undone or re-enabled, so treat it as a delete. Levels: "campaign" + campaignId · "adGroup" + adGroupId · "ad" + adGroupId AND adId · "keyword" + adGroupId AND keywordId · "conversionAction" + conversionActionId · "campaignAsset"/"ad
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
| level | string | no | what to remove — default campaign |
| campaignId | string | no | campaign id (level:"campaign") |
| adGroupId | string | no | ad group id (level:"adGroup"; REQUIRED as the parent for "ad" and "keyword") |
| adId | string | no | ad id (level:"ad" — pass adGroupId too) |
| keywordId | string | no | keyword criterion id (level:"keyword" — pass adGroupId too) |
| conversionActionId | string | no | conversion action id (level:"conversionAction") |
| resourceName | string | no | full resource name — REQUIRED for campaignAsset / adGroupAsset, accepted for any level |
| confirm | boolean | no | REQUIRED true — REMOVED is permanent |
| confirmName | string | no | the object’s EXACT name, required when it has children / is live / has spent |
| confirmChildren | number | no | the exact number of children reported by the unconfirmed call, required when it has any |
| loginCustomerId | string | no | manager id if operating through an MCC |
Add a creative to a Google Ads account’s ASSET LIBRARY so it can be used in ads. It does NOT have to be a Hermoso render — the user’s own creative is the normal case; the URL just has to be Hermoso-HOSTED because we fetch the bytes, so run any file of theirs through upload_file and pass the URL it returns. For an IMAGE, pass imageUrl (≤5MB); an arbitrary external/CDN URL is refused. For VIDEO, Goo
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
| imageUrl | string | no | a Hermoso-hosted image URL for an IMAGE asset (≤5MB) — a Hermoso render, or the user’s OWN creative put through upload_file first. An arbitrary external/CDN URL is refused. |
| youtubeVideoId | string | no | A YouTube video id for a VIDEO asset |
| playableUrl | string | no | PLAYABLE: link to an HTML5 zip (any public https URL or upload_file). Checked against Google's rules, then Google's own free validation, then created as a MEDIA_BUNDLE asset |
| appAdId | string | no | also add the playable to this App ad (App campaigns for installs only) |
| assetResourceName | string | no | with appAdId: add a playable already in the asset library instead of uploading one |
| name | string | no | asset name |
| loginCustomerId | string | no | manager id if operating through an MCC |
Create a CONVERSION ACTION — the thing that tells Google what counts as a result on this account. This is a PREREQUISITE, not a nicety: MAXIMIZE_CONVERSIONS, MAXIMIZE_CONVERSION_VALUE, TARGET_CPA, TARGET_ROAS and every Performance Max campaign are undeliverable without one, because Google has nothing to optimise toward. type WEBPAGE (a purchase / lead / signup on the site — the normal choice), UPL
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
| name | string | yes | what the user calls this result, e.g. "Purchase", "Demo request" |
| type | string | no | default WEBPAGE — a conversion that happens on the website |
| category | string | no | what kind of result this is, default DEFAULT. Google Ads v25’s IN_APP_AD_REVENUE is not offered: Google refuses it on every type this tool creates, and it comes only from a linked Firebase app (FIREBA |
| status | string | no | default ENABLED — anything else records nothing |
| countingType | string | no | ONE_PER_CLICK for leads, MANY_PER_CLICK for sales — defaults by category |
| defaultValueUsd | number | no | what one conversion is worth — required in practice for TARGET_ROAS |
| defaultCurrencyCode | string | no | 3-letter ISO code, e.g. USD |
| alwaysUseDefaultValue | boolean | no | ignore any value sent with the conversion and always use the default |
| clickThroughLookbackDays | number | no | 1–90 days |
| viewThroughLookbackDays | number | no | 1–30 days |
| includeInConversionsMetric | boolean | no | default true — false makes smart bidding IGNORE it |
| primaryForGoal | boolean | no | default true — whether this action is biddable for its category |
| dryRun | boolean | no | validate against Google and create NOTHING |
| loginCustomerId | string | no | manager id if operating through an MCC |
List the conversion actions on a Google Ads account and say plainly whether smart bidding can work there. Call this BEFORE proposing MAXIMIZE_CONVERSIONS / MAXIMIZE_CONVERSION_VALUE / TARGET_CPA / TARGET_ROAS or any Performance Max campaign: an account with no ENABLED conversion action that counts toward "conversions" cannot optimise on any of them, and the campaign would spend its budget without
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
| includeRemoved | boolean | no | also list REMOVED conversion actions |
| loginCustomerId | string | no | manager id if operating through an MCC |
Add ASSETS (extensions) to a Google Ads campaign, ad group or the whole account (level:"customer") — and ATTACH them, which is what makes them show. Pass assetType plus assets[]: SITELINK {linkText ≤25, finalUrl, description1/2}; CALLOUT {calloutText ≤25}; STRUCTURED_SNIPPET {header, values[] ≥3}; CALL {phoneNumber, countryCode}; PROMOTION {promotionTarget ≤20, percentOff OR moneyAmountOff, finalU
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
| assetType | string | yes | what kind of asset to create and attach |
| level | string | no | where to attach it — follows the id you pass; customer = every campaign on the account |
| campaignId | string | no | campaign id (level:"campaign") |
| adGroupId | string | no | ad group id (level:"adGroup") |
| assets | array | no | the assets to CREATE and attach |
| assetResourceNames | array | no | attach assets that ALREADY exist instead of creating new ones |
| status | string | no | the LINK status — default ENABLED |
| confirm | boolean | no | set true ONLY after the user approved changing what a LIVE campaign shows |
| dryRun | boolean | no | validate against Google and create NOTHING |
| loginCustomerId | string | no | manager id if operating through an MCC |
Google Ads’ own advice for the account: the optimization score (account and per campaign), every open recommendation with what applying it would change (budget $X -> $Y, a keyword, a bid target, a Google-written ad) and Google’s weekly impact estimate, each tagged spend (changes what the account buys) or serves (changes what an ad shows), plus which types Google is auto-applying by itself. An empt
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | 10-digit account id, omit for the brand’s default account |
| campaignId | string | no | only recommendations for this campaign |
| types | array | no | recommendation types, e.g. CAMPAIGN_BUDGET, KEYWORD; an unknown one is refused by name |
| includeDismissed | boolean | no | also show dismissed ones |
| loginCustomerId | string | no | manager id if operating through an MCC |
Act on Google Ads recommendations by id. action:"apply" makes the change live immediately with no undo. Any spend-type recommendation (budget, bidding, keywords, network or audience expansion, new campaigns) or one touching a LIVE campaign is refused until the user says yes: call once without confirm, show the returned list, then call again with confirm:true and the confirmCount it named. The coun
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | 10-digit account id, omit for the brand’s default account |
| action | string | yes | apply (live now; spend types need confirm), dismiss (remove from list), or auto_apply (set auto-apply per type) |
| recommendationIds | array | no | apply/dismiss: ids from google_ads_recommendations, max 100; there is no "apply everything" |
| autoApply | object | no | auto_apply: {RECOMMENDATION_TYPE: true|false} |
| confirm | boolean | no | the user saw the list and said yes |
| confirmCount | number | no | apply: echo the count the unconfirmed call reported |
| dryRun | boolean | no | validate and change nothing |
| loginCustomerId | string | no | manager id if operating through an MCC |
A shareable Google preview link (viewable with no Google Ads login, good for client approval) for a Performance Max asset group, or for a YouTube video ad. Google cannot preview Search or Display ads (RSA/RDA), and that is refused by name for free. Links expire and cannot be embedded in an iframe. Read-only, 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | 10-digit account id, omit for the brand’s default account |
| assetGroupIds | array | no | Performance Max asset group ids |
| adId | string | no | a YouTube video ad id |
| adGroupId | string | no | its ad group id |
| loginCustomerId | string | no | manager id if operating through an MCC |
WHY Google rejected or limited something: every DISAPPROVED or APPROVED_LIMITED ad, asset (headlines, images, sitelinks, Performance Max assets) and keyword on the account or one campaign, with Google’s policy topic, its effect, the evidence (offending text, a broken landing page and its HTTP code, restricted countries) and appeal state. includeApproved lists everything. Read-only, 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | 10-digit account id, omit for the brand’s default account |
| campaignId | string | no | only this campaign |
| includeApproved | boolean | no | list approved and pending items too |
| loginCustomerId | string | no | manager id if operating through an MCC |
The assets on a Google Ads campaign, ad group or the whole account (level:"customer"): action "list" (every attached asset with its review status and any policy problem), "update" (edit a sitelink/callout/snippet/call/promotion/price/lead-form asset — pass assetId, assetType and asset{} in add_google_ads_assets' shape), "pause", "enable" or "unlink" (pass assetId + fieldType from list). Google nev
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | 10-digit account id; omit to use the brand's selected default account |
| action | string | no | default list |
| level | string | no | campaign, adGroup or customer; follows the id you pass |
| campaignId | string | no | campaign id, when level is campaign |
| adGroupId | string | no | ad group id, when level is adGroup |
| assetId | string | no | asset id, from the list action |
| fieldType | string | no | the link field type from list, e.g. CALL, SITELINK |
| assetType | string | no | update — the asset kind |
| asset | object | no | update — the new content |
| confirm | boolean | no | true only after the user agreed to change a LIVE campaign's assets |
| dryRun | boolean | no | true = validate with Google and change nothing |
| loginCustomerId | string | no | manager (MCC) account id when acting through a manager account; omit otherwise |
Set the FINAL URL SUFFIX (the UTMs added to every click), tracking template and custom parameters on a Google Ads campaign, ad group or ad; a lower level overrides a higher one. "" clears a field; utmDefault:true applies our standard UTMs (utm_source=google, utm_medium, utm_campaign=<name>, utm_id={campaignid}, utm_content={creative}). New campaigns already get those unless you say otherwise. Read
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | 10-digit account id; omit to use the brand's selected default account |
| campaignId | string | no | campaign to set tracking on; use with level campaign |
| adGroupId | string | no | ad group to set tracking on; use with level adGroup |
| adId | string | no | ad to set tracking on; use with level ad |
| level | string | no | follows the id you pass |
| finalUrlSuffix | string | no | UTM string appended to every click, e.g. utm_source=google&utm_medium=cpc; empty string clears it |
| trackingUrlTemplate | string | no | click-tracking redirect template, e.g. {lpurl}?src=x; empty string clears it |
| urlCustomParameters | object | no | up to 8 custom URL parameters as a key/value object, used in URLs as {_key} |
| utmDefault | boolean | no | true = apply the standard UTMs (utm_source, utm_medium, utm_campaign, utm_id, utm_content) |
| confirm | boolean | no | needed to change the tracking redirect on a LIVE campaign |
| dryRun | boolean | no | true = validate with Google and change nothing |
| loginCustomerId | string | no | manager (MCC) account id when acting through a manager account; omit otherwise |
Audiences for Google Ads. action "search" finds affinity + in-market interests, remarketing lists, custom segments and saved audiences (query by name). "create_remarketing_list": people who visited pages matching urlRules [{operator CONTAINS|EQUALS|STARTS_WITH|ENDS_WITH, value}] in the last membershipDays (needs the Google tag on the site). "create_custom_segment": people searching keywords[] or i
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | Google Ads account id, 10 digits (dashes ok); omit for the brand's selected default account |
| action | string | no | default search |
| query | string | no | search text matched against audience and interest names (action search) |
| name | string | no | audience or list name, unique on the account (create actions) |
| description | string | no | optional description (create actions) |
| urlRules | array | no | visitor rules for create_remarketing_list: [{operator, value}]; visitors to pages matching any rule |
| excludeUrlRules | array | no | same shape as urlRules; visitors to matching pages are left out of the list |
| membershipDays | number | no | 1–540, default 30 |
| type | string | no | custom segment type: AUTO (Google decides, default) or SEARCH (people who searched the keywords) |
| keywords | array | no | search phrases for a custom segment, 10 words or fewer each (create_custom_segment) |
| urls | array | no | full http(s) URLs of sites like yours for a custom segment (create_custom_segment) |
| apps | array | no | app identifiers for a custom segment (create_custom_segment) |
| userLists | array | no | user list ids to include in an audience (create_audience), from action search |
| interests | array | no | interest ids (affinity or in-market) to include in an audience, from action search |
| customSegments | array | no | custom segment ids to include in an audience, from action search |
| ages | array | no | age ranges to include: 18-24, 25-34, 35-44, 45-54, 55-64, 65+ or unknown |
| genders | array | no | genders to include: male, female or unknown |
| kind | string | no | remove |
| id | string | no | remove |
| confirm | boolean | no | true to actually remove the list or segment (action remove only) |
Who a Google Ads campaign reaches. adGroupId (Search/Display): userLists, interests (affinity/in-market ids), customSegments (Display only), mode "observation" (report, do not narrow — default on Search) or "targeting" (narrow to them); Demand Gen ad groups take audiences[] ids. assetGroupId (Performance Max): audiences[] as signals and searchThemes[]. campaignId: exclusions only. exclude{userList
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | Google Ads account id, 10 digits (dashes ok); omit for the brand's default account |
| adGroupId | string | no | ad group to target (Search, Display or Demand Gen); one of adGroupId, assetGroupId, campaignId |
| assetGroupId | string | no | Performance Max asset group id; audiences become signals there |
| campaignId | string | no | campaign id; exclusions only |
| userLists | array | no | user list ids to target on the ad group |
| interests | array | no | interest ids (affinity or in-market) to target on the ad group |
| customSegments | array | no | custom segment ids to target; Display ad groups only, Search refuses them |
| audiences | array | no | audience ids from manage_google_ads_audience (create_audience) for Demand Gen or Performance Max |
| searchThemes | array | no | search theme phrases for a Performance Max asset group, up to 50, 10 words or fewer each |
| mode | string | no | observation (report only, default on Search) or targeting (narrow delivery to these audiences) |
| exclude | object | no | audiences to exclude from the ad group or campaign: {userLists, interests, customSegments} ids |
| excludeAges | array | no | age ranges to exclude: 18-24, 25-34, 35-44, 45-54, 55-64, 65+ or unknown |
| excludeGenders | array | no | genders to exclude: male, female or unknown |
| remove | array | no | criterion resource names to remove; a call with no other changes lists the current ones |
| confirm | boolean | no | true to approve a change that alters who a live ad group reaches |
| dryRun | boolean | no | true = validate with Google and change nothing |
| loginCustomerId | string | no | manager (MCC) account id, when the account is managed through one |
Upload a Hermoso video (render id, Library id, attachment://N or Hermoso link) straight into Google Ads — no YouTube channel needed: it goes UNLISTED to the Google-managed channel of the ad account (or to your own brand channel with channelId, where PUBLIC is allowed). Waits for YouTube to process it, then returns the video asset ready for create_google_ads_demand_gen_campaign or Performance Max.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | Google Ads account id, 10 digits (dashes ok); omit for the brand's default account |
| action | string | no | default upload |
| video | string | no | Hermoso render id, Library id, attachment://N or Hermoso video link |
| title | string | no | required, ≤100 chars |
| description | string | no | YouTube description for the uploaded video (optional) |
| channelId | string | no | your own YouTube channel (UC…), else Google's |
| privacy | string | no | UNLISTED (default, the only option on Google's channel) or PUBLIC (own channel only, with channelId) |
| aiGenerated | boolean | no | Google Ads generative-content disclosure. OMIT IT and Hermoso decides from provenance: a Hermoso render is declared, a video that came through upload_file (the user’s own footage) is NOT. true/false o |
| uploadId | string | no | upload id from the upload reply or action list; required for status and remove |
| waitSeconds | number | no | seconds to wait for YouTube to finish processing, max 240; default 90 on upload, 0 on status |
| confirm | boolean | no | true to remove the upload (action remove); every ad using the video stops showing it |
| dryRun | boolean | no | true = build and return the request without uploading any bytes |
| loginCustomerId | string | no | manager (MCC) account id, when the account is managed through one |
Build a Google DEMAND GEN campaign — video ads on YouTube Shorts, in-feed and in-stream (plus Discover, Gmail, Display, Maps if you pick them) — as ONE atomic, PAUSED build: budget, campaign, ad group with channel controls, locations/languages, optional audiences, and a video ad. ad needs videoAssets (from upload_google_ads_video) or youtubeVideoIds, a square logo (logoImage or logoAssets), finalU
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | Google Ads account id, 10 digits (dashes ok); omit for the brand's default account |
| name | string | yes | campaign name |
| dailyBudgetUsd | number | no | daily budget in the ACCOUNT’s currency (historical name, not necessarily USD), 1 to 100000; required unless budgetResourceName is given |
| budgetResourceName | string | no | existing campaign budget resource name to reuse instead of creating a new budget |
| bidding | object | no | bidding object {strategy, targetCpaUsd, targetRoas, maxCpcUsd}; strategy defaults to MAXIMIZE_CONVERSIONS |
| channels | array | no | omit for all of Google's channels |
| locations | array | no | place names to target, resolved by Google, e.g. United States |
| excludedLocations | array | no | place names to exclude, resolved by Google |
| languages | array | no | ISO language codes to target, e.g. ["en"] |
| countryCode | string | no | 2-letter country code that disambiguates a place name, e.g. CA for Toronto |
| audiences | array | no | audience ids from manage_google_ads_audience |
| ad | object | yes | video ad: {videoAssets or youtubeVideoIds, logo, finalUrl, businessName, headlines, longHeadlines, descriptions} |
| finalUrlSuffix | string | no | UTM suffix appended to clicks; omit for Hermoso's default, empty string for none |
| trackingUrlTemplate | string | no | click-tracking redirect template, e.g. {lpurl}?src=x; only if you use a tracker |
| urlCustomParameters | object | no | up to 8 custom URL parameters, used in URLs as {_key} |
| startDate | string | no | campaign start date, YYYY-MM-DD |
| endDate | string | no | campaign end date, YYYY-MM-DD |
| containsEuPoliticalAds | boolean | no | true only for genuine EU political advertising |
| dryRun | boolean | no | true = validate with Google and create nothing |
| loginCustomerId | string | no | manager (MCC) account id, when the account is managed through one |
Build a PERFORMANCE MAX campaign — Google’s cross-surface campaign type (Search, YouTube, Display, Discover, Gmail, Maps). ALWAYS created PAUSED; it spends NOTHING until you enable it with set_google_ads_status(confirm:true). PMax has NO manual bidding and NO keywords: it bids only on conversions, so the account MUST already have a conversion action — check with list_google_ads_conversion_actions,
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
| name | string | yes | campaign name |
| dailyBudgetUsd | number | no | daily budget in the ACCOUNT’s currency (historical name — not necessarily USD; 1–100000) — or pass budgetResourceName |
| budgetResourceName | string | no | an existing budget to reuse |
| bidding | object | no | bidding {strategy, targetCpaUsd, targetRoas}; no manual bidding, strategy defaults to MAXIMIZE_CONVERSIONS |
| locations | array | no | place NAMES ("United States", "Toronto") — resolved for you |
| excludedLocations | array | no | places to block |
| languages | array | no | ISO codes, e.g. ["en"] |
| countryCode | string | no | 2-letter hint to disambiguate a city name |
| startDate | string | no | YYYY-MM-DD |
| endDate | string | no | YYYY-MM-DD |
| finalUrlSuffix | string | no | UTMs appended to every click, e.g. "utm_source=google&utm_medium=cpc". Omit for our default (utm_source/medium/campaign + {campaignid}/{creative}); "" for none |
| trackingUrlTemplate | string | no | click-tracking redirect, e.g. "{lpurl}?src=x" — only if you use a tracker |
| urlCustomParameters | object | no | up to 8 custom parameters, used in URLs as {_key} |
| containsEuPoliticalAds | boolean | no | true ONLY for genuine EU political advertising |
| merchantCenterId | string | no | makes this a RETAIL (Shopping-feed) Performance Max — the Merchant Center account whose products it advertises, from list_merchant_accounts. It advertises the WHOLE feed; partitioning by brand/categor |
| feedLabel | string | no | optional, retail only: narrow to ONE Merchant Center feed by its feed label. Omit to use products from all feeds |
| brandGuidelinesEnabled | boolean | no | default TRUE, matching Google’s own default since v21: businessName + logos are linked to the CAMPAIGN. Pass false only for the pre-v21 layout, where they sit on the asset group instead |
| assetGroup | object | yes | the creative — Google requires every field above before a PMax campaign can serve |
| dryRun | boolean | no | validate the whole tree against Google and create NOTHING |
Google’s own KEYWORD PLANNER: real keyword ideas with average monthly search volume, competition level and top-of-page bid estimates, so keyword choices are measured instead of guessed. Seed it with keywords[] (terms you already have), url (one landing page to mine) or site (a whole domain — the fastest way to size a competitor). Narrow by locations (place NAMES, resolved for you) and language. Re
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
| keywords | array | no | up to 20 seed terms |
| url | string | no | one page to mine for ideas |
| site | string | no | a whole domain to mine, e.g. example.com |
| locations | array | no | place NAMES, e.g. ["United States"] |
| language | string | no | ISO code, e.g. "en" |
| countryCode | string | no | 2-letter hint to disambiguate a city name |
| network | string | no | default GOOGLE_SEARCH |
| limit | number | no | how many ideas to return (1–200, default 50) |
| loginCustomerId | string | no | manager id if operating through an MCC |
The GA4 properties SHARED WITH THIS PROFILE, with their numeric property ids, display names and the Analytics account each sits under. CALL THIS FIRST — every other Analytics tool needs a PROPERTY ID, which is NUMERIC (e.g. 123456789) and is NOT the "G-XXXXXXX" Measurement ID people usually know from their tracking snippet; the API accepts the numeric id and nothing else. Resolve the property your
No parameter schema on file.
WHAT HAPPENED ON THE SITE — the GA4 Data API report, and the tool that answers "where is our traffic coming from", "which campaign converted", "which landing page is working". Pass metrics by name (activeUsers, sessions, screenPageViews, conversions, totalRevenue, engagementRate, bounceRate, averageSessionDuration …) and dimensions to break them down by (sessionDefaultChannelGroup, sessionSource /
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | the NUMERIC GA4 property id from list_analytics_properties (e.g. "123456789") — never the G-XXXXXXX Measurement ID, and it must be one SHARED with this profile |
| metrics | array | no | GA4 metric names — default ["activeUsers","sessions","screenPageViews"] |
| dimensions | array | no | GA4 dimension names to break the metrics down by — omit for a single total row |
| startDate | string | no | YYYY-MM-DD or a GA4 relative date like "28daysAgo" (default 28daysAgo) |
| endDate | string | no | YYYY-MM-DD or "today" (default today) |
| limit | number | no | rows to return, 1–250000 (default 50). 250,000 is GOOGLE's per-request maximum, not ours — asking for more is silently capped there. |
| offset | number | no | skip this many rows — how you page past limit. Use the nextOffset the previous call returns. |
| orderByMetric | string | no | sort by this metric — must be one of the metrics requested |
| orderDesc | boolean | no | default true (largest first) when orderByMetric is set |
| dimensionFilter | object | no | NARROW THE REPORT — a GA4 FilterExpression, exactly one of andGroup | orGroup | notExpression | filter. Without it a report is the WHOLE property. Example: {"filter":{"fieldName":"sessionSource","stri |
| metricFilter | object | no | Filter the AGGREGATED rows, GA4's having-clause — same FilterExpression shape. Example: {"filter":{"fieldName":"sessions","numericFilter":{"operation":"GREATER_THAN","value":{"int64Value":"30"}}}}. DI |
| compare | string | no | also read the period before startDate–endDate (or the same dates a year earlier) and return change |
| dateRanges | array | no | up to 4 periods instead of startDate/endDate; rows gain a dateRange column; with two, change = the FIRST relative to the SECOND |
| totals | boolean | no | add TOTAL rows |
| aggregations | array | no | extra summary rows over the result: TOTAL, MINIMUM, MAXIMUM or COUNT |
| comparisons | array | no | segments side by side: [{name, dimensionFilter}] or [{comparison:"comparisons/123"}]; adds a comparison column |
| orderBy | array | no | [{dimension:"date"}] or [{metric:"sessions", desc:true}] |
| keepEmptyRows | boolean | no | true = keep rows whose metrics are all zero |
| currencyCode | string | no | ISO 4217, e.g. EUR — default the property currency |
WHO IS ON THE SITE RIGHT NOW — GA4's realtime report, covering roughly the last 30 minutes. Use it to see a launch, a post or a campaign landing in real time. IT IS A DIFFERENT REPORT WITH A DIFFERENT METRIC SET, NOT a shortcut for analytics_report: realtime accepts activeUsers, screenPageViews, conversions and eventCount, and REJECTS sessions outright — asking for sessions here returns an error
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | the NUMERIC GA4 property id from list_analytics_properties — it must be one SHARED with this profile, and any other is refused by name |
| metrics | array | no | realtime metric names — default ["activeUsers"]. NOT the same set as analytics_report; sessions is not a realtime metric |
| dimensions | array | no | realtime dimension names, e.g. ["country"] or ["unifiedScreenName"] |
| limit | number | no | rows, 1–1000 (default 50) |
What a GA4 property already MEASURES — its key events (what GA4 counts as a conversion) and its custom dimensions, with each dimension's parameter name and scope. Two reasons to call it: to learn a property's own custom dimension names before using them in analytics_report, and to CHECK BEFORE CREATING — a custom dimension can never be deleted, only archived, and a property is capped at 50 event-s
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | the NUMERIC GA4 property id from list_analytics_properties — it must be one SHARED with this profile, and any other is refused by name |
Mark an event GA4 ALREADY COLLECTS as a KEY EVENT — what GA4 calls a conversion since the 2024 rename (the resource is keyEvents; the old conversionEvents spelling is deprecated). This is what makes an event countable as a result in reports and importable into Google Ads as a conversion. THE EVENT NAME MUST MATCH AN EVENT THE SITE ALREADY SENDS ("purchase", "generate_lead", "sign_up", or a custom
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | the NUMERIC GA4 property id from list_analytics_properties — it must be one SHARED with this profile, and any other is refused by name |
| eventName | string | yes | an event GA4 already collects, e.g. "purchase" / "generate_lead" / "sign_up" |
| countingMethod | string | no | default ONCE_PER_EVENT; ONCE_PER_SESSION counts at most once per session (GA4 publishes no per-user method) |
Register an event parameter the site ALREADY SENDS as a custom dimension, so reports can break down by it (plan tier, content category, logged-in state…). TWO THINGS TO TELL THE USER BEFORE CALLING: a GA4 custom dimension CANNOT BE DELETED — only archived — and a property is capped at 50 EVENT-scoped dimensions, so a duplicate or a typo permanently burns one of them. Call list_analytics_definition
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | the NUMERIC GA4 property id from list_analytics_properties — it must be one SHARED with this profile, and any other is refused by name |
| parameterName | string | yes | the event parameter GA4 already collects, e.g. "customer_tier" — NOT the report label |
| displayName | string | no | the label shown in GA4 reports — defaults to parameterName |
| scope | string | no | default EVENT. The 50-dimension cap applies to EVENT scope |
| description | string | no | optional description, ≤150 characters |
THE VOCABULARY OF ONE PROPERTY — every dimension and metric analytics_report will accept on it, INCLUDING that property's own custom dimensions, each with its api name and its human label. Use this instead of guessing an api name: GA4 publishes hundreds and they are not memorable (sessions broken down by landing page is landingPage, revenue is totalRevenue, the channel grouping is `sessionDefa
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | the NUMERIC GA4 property id from list_analytics_properties — it must be one SHARED with this profile, and any other is refused by name |
| search | string | no | substring filter over api name AND human label, e.g. "revenue", "campaign", "landing" — strongly recommended, since the unfiltered vocabulary is several hundred entries |
THE ONLY WAY TO RETIRE A CUSTOM DIMENSION, and it is ONE-WAY. GA4 publishes no delete and no un-archive for custom dimensions anywhere in its API — archiving is permanent through every programmatic surface — so this is how a typo'd or duplicate dimension is cleared, and it also frees the slot it was holding against the 50-event-scoped cap. Reports lose the ability to break down by it. CALLED WITHO
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | the NUMERIC GA4 property id from list_analytics_properties |
| parameterName | string | yes | the event parameter of the dimension to archive, e.g. "customer_tier" — from list_analytics_definitions, NOT the report label |
| confirm | boolean | no | must be true to actually archive. Without it nothing changes and the dimension is described back to you |
REMOVE A KEY EVENT — the reverse of create_analytics_key_event, and note the ASYMMETRY with custom dimensions: a key event really can be DELETED, where a custom dimension can only be archived. Nothing is destroyed — GA4 keeps collecting the underlying event and keeps all of its history, this only stops it counting as a conversion, and it can be marked again at any time. Use it for an event marked
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | the NUMERIC GA4 property id from list_analytics_properties |
| eventName | string | yes | the key event to stop counting, e.g. "sign_up" — from list_analytics_definitions |
| confirm | boolean | no | must be true to actually delete. Without it nothing changes and the key event is described back to you |
Register an event parameter the site ALREADY SENDS as a GA4 custom METRIC, so reports can SUM or AVERAGE it. This is the other half of the pair: a custom DIMENSION makes a value something you can group BY, a custom METRIC makes it something you can TOTAL — 'order_value' as a dimension gives you a list of prices, as a metric it gives you revenue. TELL THE USER TWO THINGS BEFORE CALLING: a GA4 custo
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | NUMERIC GA4 property id from list_analytics_properties — never the G-XXXXXXX Measurement ID |
| parameterName | string | yes | the event parameter GA4 already collects, e.g. 'order_value' — NOT the report label. Letters, digits and underscores, starting with a letter, max 40 characters |
| displayName | string | no | the label shown in reports; defaults to parameterName, max 82 characters |
| measurementUnit | string | yes | REQUIRED — Google marks it Required and there is no safe default |
| restrictedMetricType | array | no | REQUIRED when measurementUnit is CURRENCY, and refused for every other unit |
| scope | string | no | EVENT is the only scope GA4 accepts for a metric, and it is immutable |
| description | string | no | ≤150 characters |
THE ONLY WAY TO RETIRE A GA4 CUSTOM METRIC, and it is ONE-WAY: GA4 publishes no delete and no un-archive for custom metrics anywhere in its API, exactly as for dimensions, so archiving is permanent through every programmatic surface. It frees the slot against the 50-per-property cap, and reports lose the ability to sum or average it. WITHOUT confirm IT ARCHIVES NOTHING and instead describes the
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | NUMERIC GA4 property id |
| parameterName | string | yes | the event parameter of the metric to archive, e.g. 'order_value' |
| confirm | boolean | no | must be true to actually archive — without it nothing is archived and the metric is described back to you |
FREE PRE-FLIGHT FOR analytics_report — ask Google whether these dimensions and metrics can appear in ONE report together, BEFORE running the report that would fail. Use it whenever a report combines fields you have not combined before, and especially when one of them is a custom dimension: GA4 has real pairs that are each perfectly valid alone and cannot be queried together, and analytics_report
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | NUMERIC GA4 property id |
| metrics | array | no | the same metrics you would send to analytics_report (defaults to the same three it defaults to) |
| dimensions | array | no | the same dimensions you would send to analytics_report |
| dimensionFilter | object | no | the same GA4 FilterExpression you would send to analytics_report |
| metricFilter | object | no | the same GA4 FilterExpression you would send to analytics_report |
| compatibilityFilter | string | no | narrow the answer — COMPATIBLE returns only the fields that CAN be added |
Which Google Ads accounts a GA4 property is linked to, and whether each link has PERSONALIZED ADVERTISING on — the flag that decides whether GA4 audiences and remarketing events reach Google Ads at all. RUN THIS BEFORE two claims: that a GA4 audience can be remarketed to, and that a Google Ads campaign's missing conversion data is a Google Ads problem. For a GA4-first advertiser the conversions co
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | the NUMERIC GA4 property id from list_analytics_properties — never the G-XXXXXXX Measurement ID |
LINK a GA4 property to a Google Ads account. This is the prerequisite for two things users constantly ask for and cannot otherwise have: importing GA4 key events as Google Ads CONVERSIONS — which is what gets a smart-bidding campaign past the conversion-tracking refusal for a GA4-first advertiser — and using a GA4 AUDIENCE for remarketing. RE-LINKING AN ACCOUNT THAT IS ALREADY LINKED IS NOT AN ERR
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | the NUMERIC GA4 property id from list_analytics_properties |
| customerId | string | yes | the 10-digit Google Ads customer id shown top-right in Google Ads; dashes optional |
| adsPersonalizationEnabled | boolean | no | defaults to TRUE. Only set false for measurement-only linking — it disables remarketing |
REMOVE the link between a GA4 property and a Google Ads account. The link itself is trivially re-creatable, which is why this is not a one-way-door gate — but what breaks downstream is not: every campaign bidding on a conversion imported from this property loses that signal, and every remarketing list built from a GA4 audience stops refreshing. WITHOUT confirm IT CHANGES NOTHING and reports what
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | the NUMERIC GA4 property id |
| customerId | string | yes | the Google Ads customer id, from list_analytics_google_ads_links |
| confirm | boolean | no | must be true to actually unlink — without it nothing changes and the link is described back to you |
The GA4 audiences (remarketing lists) on a property, WITH whether they can actually be used in Google Ads — it reads the Google Ads link state alongside them, because an audience on an unlinked property is a silent dead end that reports no error anywhere. ARCHIVED audiences are not listed, so absence here is not proof one never existed. GA4's PREDEFINED audiences ('All Users', 'Purchasers') ARE re
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | the NUMERIC GA4 property id |
BUILD A GA4 AUDIENCE — a remarketing list defined by what people did on the site, which flows into Google Ads across the property's Google Ads link. TELL THE USER TWO THINGS BEFORE CALLING: GA4 marks the DEFINITION, the MEMBERSHIP DURATION and every filter SCOPE immutable, so a wrong audience is archived and rebuilt rather than edited; and it is NOT retroactive — it starts collecting members from
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | the NUMERIC GA4 property id |
| displayName | string | yes | the audience name shown in GA4 and in Google Ads |
| description | string | yes | REQUIRED by Google on the Audience resource — one line saying who is in it |
| membershipDurationDays | number | yes | REQUIRED and IMMUTABLE, 1-540. 30 is the usual remarketing window; 540 is GA4's maximum |
| filterClauses | array | yes | GA4 AudienceFilterClause list — each {clauseType:INCLUDE|EXCLUDE, simpleFilter|sequenceFilter}. See the worked example in the description |
| exclusionDurationMode | string | no | IMMUTABLE, and it applies to EVERY exclude clause on the audience |
| eventTrigger | object | no | optional {eventName, logCondition} event GA4 logs when a user joins the audience |
The GA4 data streams on a property, with the measurement ID (G-...) each one carries and how many Measurement Protocol secrets it already has. THIS IS WHERE A GTAG OR GTM INSTALL GETS ITS ID, and it is the first call before sending any server-side event. A web stream has a measurementId; an app stream has none and uses its firebaseAppId instead, so do not report an app stream as broken for having
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | NUMERIC GA4 property id, from list_analytics_properties |
THE GTAG SNIPPET TO PASTE, plus what that web stream is actually set to collect. list_analytics_data_streams gives the measurement ID and stops one step short of what someone installing GA4 needs, which is the finished <script> block — Google returns it verbatim and marks it immutable. The same call answers the two questions that come next: whether ENHANCED MEASUREMENT is collecting scrolls, outbo
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | NUMERIC GA4 property id, from list_analytics_properties |
| dataStream | string | no | stream id, measurement id or display name. Optional when the property has exactly one stream |
THE SERVER-SIDE EVENT CHANNEL FOR GA4, the Google twin of the conversions APIs Hermoso already ships for Reddit, Snapchat and OpenAI Ads. A Measurement Protocol secret is the api_secret half of https://www.google-analytics.com/mp/collect; the other half is the stream's measurementId for a web stream or its firebaseAppId for an app stream, and this tool reports whichever applies. action:"list" show
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | NUMERIC GA4 property id |
| action | string | no | defaults to 'list' |
| dataStream | string | no | stream id, measurement id or display name. Optional when the property has exactly one stream; required when it has several, and an ambiguous name is refused rather than first-matched |
| displayName | string | no | REQUIRED for action:'create' — what will be sending with this secret, e.g. "Shopify webhook". It is the only label a secret ever gets. May also be used to name one for delete |
| secretId | string | no | for action:'delete', from action:'list' — preferred over displayName because it is unambiguous |
| confirm | boolean | no | must be true to actually delete |
ARCHIVE a GA4 audience, and it is ONE-WAY: GA4 publishes no delete and no un-archive anywhere in its API. Any Google Ads remarketing list sourced from it stops refreshing, and because the definition is IMMUTABLE the audience cannot be recovered by editing — rebuilding means a NEW audience that starts collecting members from scratch, so months of accumulated membership are gone. WITHOUT confirm I
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | the NUMERIC GA4 property id |
| audienceId | string | no | from list_analytics_audiences — preferred, because it is unambiguous |
| displayName | string | no | the audience name, if the id is not to hand. Refused when it matches more than one |
| confirm | boolean | no | must be true to actually archive |
HOW GA4 DECIDES WHICH BUCKET A CLICK LANDS IN, which is the number an ad studio is judged on and the answer to "why is my campaign showing as Unassigned". action:'list' reads every group with its channels in the order GA4 tests them, including Google's own Default channel group — read that first, because a campaign landing in the wrong channel is a rule that does not match rather than missing data
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | NUMERIC GA4 property id, from list_analytics_properties |
| action | string | no | defaults to 'list' |
| displayName | string | no | the group name shown in GA4, max 80 characters. Required to create; may also name an existing group for update/delete |
| description | string | no | max 256 characters |
| channels | array | no | ordered list of {name, conditions:[…]}. Required to create. On update it REPLACES every channel, so send the ones you want to keep |
| primary | boolean | no | make this the group reports are bucketed by. It unsets the previous primary group, changing what every report in the property shows |
| channelGroupId | string | no | for update and delete — from action:'list', and unambiguous where a name is not |
| confirm | boolean | no | must be true to actually delete |
THE DERIVED NUMBER A MARKETER ACTUALLY REPORTS — cost per purchase, revenue per session, a margin — built from metrics GA4 already collects and then available to analytics_report like any other metric. action:'list' reads them, 'create' adds one, 'update' changes the formula, name, unit or description, 'delete' removes one (confirm-gated). THE ID IS CHOSEN ONCE AND IS PERMANENT: calculatedMetricId
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | NUMERIC GA4 property id, from list_analytics_properties |
| action | string | no | defaults to 'list' |
| calculatedMetricId | string | no | the API name, e.g. "cost_per_purchase". Required to create and PERMANENT; for update/delete it names the metric |
| displayName | string | no | the name shown in GA4, max 82 characters. Required to create |
| formula | string | no | e.g. "advertiserAdCost / conversions". Required to create |
| metricUnit | string | no | STANDARD | CURRENCY | FEET | MILES | METERS | KILOMETERS | MILLISECONDS | SECONDS | MINUTES | HOURS. Required to create |
| description | string | no | max 4096 characters |
| confirm | boolean | no | must be true to actually delete |
MARK WHAT YOU DID ON THE CUSTOMER'S OWN GA4 CHARTS — "ad launched", "post went out", "price change" — so the traffic it moved is visible where they already look. action:'list' reads them (Google's own system annotations included, which cannot be edited or deleted), 'create' adds one on a single date or a startDate–endDate range, 'update' changes one, 'delete' removes one (confirm-gated). color: PU
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | NUMERIC GA4 property id, from list_analytics_properties |
| action | string | no | defaults to 'list' |
| annotationId | string | no | for update/delete — from action:'list' |
| title | string | no | the label on the chart. Required to create |
| description | string | no | longer note shown with the annotation (create or update) |
| color | string | no | chart colour; default BLUE |
| date | string | no | YYYY-MM-DD — one day |
| startDate | string | no | YYYY-MM-DD — a range, with endDate |
| endDate | string | no | last day of a range, YYYY-MM-DD; use with startDate instead of date |
| confirm | boolean | no | must be true to actually delete |
HOW LONG GA4 KEEPS EVENT- AND USER-LEVEL DATA. The default is 2 months, which silently caps every exploration and funnel at the last two months; standard reports are unaffected. action:'get' reads it; 'set' changes eventDataRetention / userDataRetention (2 or 14 months on a standard property; 26, 38, 50 for event data on Analytics 360 only) and resetUserDataOnNewActivity. RAISING applies to data a
| Parameter | Type | Required | Notes |
|---|---|---|---|
| property | string | yes | NUMERIC GA4 property id, from list_analytics_properties |
| action | string | no | defaults to 'get' |
| eventDataRetention | string | no | months: 2 or 14 (26/38/50 Analytics 360 only), or the enum e.g. FOURTEEN_MONTHS |
| userDataRetention | string | no | months: 2 or 14 |
| resetUserDataOnNewActivity | boolean | no | true = a user's data retention window restarts each time that user shows new activity |
| confirm | boolean | no | required when the change SHORTENS retention, which deletes data |
The Google Tag Manager containers SHARED WITH THIS PROFILE, with the exact containerPath every other Tag Manager tool needs. CALL THIS FIRST. A container is addressed by PATH, "accounts/<id>/containers/<id>", never by the GTM-XXXXXX public id a human reads off the snippet: no Tag Manager method accepts the public id, so passing one is refused. Each row carries the public id beside the path so you
No parameter schema on file.
WHAT IS ACTUALLY FIRING ON THE SITE. Every tag in the container's workspace with its type, whether it is paused, which triggers fire it by name, and the one field that matters most: whether each tag is LIVE on the website or only STAGED in a draft. Also returns the container's triggers so you can reuse one instead of creating a duplicate. USE THIS TO ANSWER "is conversion tracking actually working
| Parameter | Type | Required | Notes |
|---|---|---|---|
| containerPath | string | yes | the "accounts/<id>/containers/<id>" path from list_tag_manager_containers, never the GTM-XXXXXX public id, and it must be one SHARED with this profile |
| gtmWorkspace | string | no | Tag Manager workspace id or name. Defaults to the Default Workspace, and the answer always states which one it read. |
| workspace | string | no | older name for gtmWorkspace (MCP only) |
INSTALL A TAG in the container's draft workspace: a Google Ads conversion tag, a GA4 event, or a third-party pixel. IT DOES NOT GO LIVE. Everything written here lands in a WORKSPACE, which is a draft, and it does not run on the website until a human opens Tag Manager and publishes the container. Staging NEVER publishes: making a tag live is a separate, explicitly confirmed call (publish_tag_manage
| Parameter | Type | Required | Notes |
|---|---|---|---|
| containerPath | string | yes | the "accounts/<id>/containers/<id>" path from list_tag_manager_containers |
| gtmWorkspace | string | no | Tag Manager workspace id or name. Defaults to the Default Workspace. |
| workspace | string | no | older name for gtmWorkspace (MCP only) |
| name | string | yes | what a human will look for in the Tag Manager UI |
| type | string | yes | the Tag Manager tag type string. Copy it off a tag of the same kind already in the container (list_tag_manager_tags) rather than guessing: Google publishes no machine-readable list of these. |
| firingTriggerId | array | yes | REQUIRED, at least one real trigger id from list_tag_manager_tags. A tag with no trigger is installed and never runs. |
| parameter | array | no | Tag Manager parameters, each { type, key, value }. type defaults to "template", which is Tag Manager's string type. |
| blockingTriggerId | array | no | trigger ids that stop this tag firing when they match |
| tagFiringOption | string | no | unlimited, oncePerEvent or oncePerLoad |
| paused | boolean | no | true = create the tag paused, so it does not fire |
| notes | string | no | internal note stored on the tag; not shown on the website |
CHANGE AN EXISTING TAG in the draft workspace: rename it, fix a wrong conversion id or label, move it onto a different trigger, or pause it. Like create_tag_manager_tag this lands in a DRAFT and does not reach the website until a human publishes the container. Hermoso reads the tag first and merges your change on top of it, because Tag Manager's update replaces the whole tag and sending only the c
| Parameter | Type | Required | Notes |
|---|---|---|---|
| containerPath | string | yes | container path accounts/<id>/containers/<id>, from list_tag_manager_containers |
| gtmWorkspace | string | no | Tag Manager workspace id or name; defaults to the Default Workspace |
| workspace | string | no | older name for gtmWorkspace (MCP only) |
| tagId | string | yes | the tagId from list_tag_manager_tags |
| name | string | no | new tag name |
| firingTriggerId | array | no | replaces the firing trigger ids, from list_tag_manager_tags |
| parameter | array | no | replaces the tag parameters: [{type, key, value}] |
| blockingTriggerId | array | no | replaces the blocking trigger ids |
| tagFiringOption | string | no | unlimited, oncePerEvent or oncePerLoad |
| paused | boolean | no | pause the tag rather than removing its trigger, which is how you stop one running |
| notes | string | no | replaces the internal note on the tag |
CREATE A TRIGGER, the rule that decides when a tag fires, in the container's draft workspace. Use it when list_tag_manager_tags shows no trigger matching what a conversion needs, for example a purchase confirmation page or a dataLayer purchase event. A trigger on its own fires nothing: it has to be named in a tag's firingTriggerId, so create the trigger, then create or update the tag that uses it.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| containerPath | string | yes | container path accounts/<id>/containers/<id>, from list_tag_manager_containers |
| gtmWorkspace | string | no | Tag Manager workspace id or name; defaults to the Default Workspace |
| workspace | string | no | older name for gtmWorkspace (MCP only) |
| name | string | yes | trigger name, as it appears in Tag Manager |
| type | string | yes | one of Tag Manager's own trigger types, e.g. pageview, domReady, windowLoaded, customEvent, formSubmission, linkClick, click, historyChange, scrollDepth, elementVisibility, timer. An unknown one is re |
| filter | array | no | Tag Manager Condition objects, each { type, parameter: [...] } |
| customEventFilter | array | no | REQUIRED for a customEvent trigger, or it would fire on every dataLayer event in the container |
| notes | string | no | internal note stored on the trigger |
THE CONTAINER'S VERSION HISTORY, and the thing to read before proposing to publish or to roll one back. Every version with its id, its name, how many tags, triggers and variables it holds, whether it is archived, and which one is LIVE on the site right now. This is what makes a publish reversible: pass a row's path as publish_tag_manager_container's versionPath to put that version back on the site
| Parameter | Type | Required | Notes |
|---|---|---|---|
| containerPath | string | yes | the "accounts/<id>/containers/<id>" path from list_tag_manager_containers, never the GTM-XXXXXXX public id |
WHAT WOULD HAPPEN IF YOU PUBLISHED — free, read-only, and it publishes nothing. Returns the container, the version live on the site right now and what it holds, the exact list of staged changes (added, updated, deleted) as Tag Manager itself reports them, any merge conflicts, and the one sentence to show a user before asking them to agree. Use it to answer 'what is waiting to go live?' without goi
| Parameter | Type | Required | Notes |
|---|---|---|---|
| containerPath | string | yes | the "accounts/<id>/containers/<id>" path from list_tag_manager_containers |
| gtmWorkspace | string | no | Tag Manager workspace id or name; defaults to the Default Workspace |
| workspace | string | no | older name for gtmWorkspace (MCP only) |
| versionPath | string | no | optional — preview re-publishing an EXISTING version instead of the workspace |
PUBLISH THE CONTAINER — the ONE call in Hermoso that changes what runs on the customer's live website for every visitor, immediately. Everything else in Tag Manager is staged; this is not. TWO STEPS UNDER THE HOOD, because Tag Manager publishes a VERSION and not a workspace: Hermoso turns the draft workspace into a version and then publishes that version. CALLING IT WITHOUT confirm PUBLISHES NOTHI
| Parameter | Type | Required | Notes |
|---|---|---|---|
| containerPath | string | yes | the "accounts/<id>/containers/<id>" path from list_tag_manager_containers |
| gtmWorkspace | string | no | Tag Manager workspace id or name; defaults to the Default Workspace |
| workspace | string | no | older name for gtmWorkspace (MCP only) |
| versionPath | string | no | ROLLBACK / RE-PUBLISH: publish this EXISTING version instead of the workspace. From list_tag_manager_versions. Must be a version inside the same container. |
| versionName | string | no | name for the version being created; defaults to a timestamped Hermoso name |
| versionNotes | string | no | notes stored on the version, visible in Tag Manager |
| confirm | boolean | no | REQUIRED to publish. Without it nothing is published and you get the blast-radius report instead. |
| confirmPublicId | string | no | the container's GTM-XXXXXXX public id, REQUIRED when the container already has a live version. Read it from the preview; it cannot be guessed. |
The Google Search Console properties SHARED WITH THIS PROFILE, each with the exact property string every other Search Console tool takes and the connected account's permission level on it. CALL THIS FIRST. A property is EITHER "sc-domain:example.com" (a Domain property, covering every scheme and subdomain) OR the full URL-prefix form "https://example.com/" including scheme and trailing slash — Goo
No parameter schema on file.
THE REAL SEARCH TERMS — Google Search Console's performance report, and the most useful research tool here for writing copy. dimensions:["query"] returns the literal words people typed to reach this brand, with impressions, clicks, CTR and average position; dimensions:["page","query"] answers "which landing page already wins this demand", i.e. Where paid traffic should point. Every other dimen
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the exact property string from list_search_console_sites — "sc-domain:example.com" or "https://example.com/", never a bare domain, and it must be one SHARED with this profile |
| startDate | string | yes | YYYY-MM-DD, required, Pacific time, inclusive |
| endDate | string | yes | YYYY-MM-DD, required, Pacific time, inclusive |
| dimensions | array | no | group by any of query, page, country, device, searchAppearance, date, hour. Omit for one total row. An unknown name is refused by name rather than dropped. |
| type | string | no | web (default), image, video, news, discover or googleNews |
| dimensionFilterGroups | array | no | narrow the report, e.g. [{"filters":[{"dimension":"query","operator":"contains","expression":"buy"}]}]. dimension is one of query/page/country/device/searchAppearance — date and hour are NOT filterabl |
| aggregationType | string | no | auto (default), byPage, byProperty or byNewsShowcasePanel |
| dataState | string | no | final (default, lags 2-3 days), all (includes fresh partial data that WILL change), or hourlyAll |
| rowLimit | number | no | 1-25000, default 1000 |
| startRow | number | no | zero-based, for paging past rowLimit |
THE LIVE INDEX STATUS OF ONE PAGE, straight from Google: the index verdict, coverage state, whether robots.txt allows it, which URL Google picked as canonical versus the one the page itself declares, when it was last crawled and as which crawler, plus rich-result and mobile-usability verdicts. This is the tool for "why is this page getting no traffic" — a page Google cannot crawl, or whose canonic
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the exact property string from list_search_console_sites, and it must be one SHARED with this profile |
| inspectionUrl | string | yes | the full URL to inspect, e.g. "https://example.com/pricing" — it must be under the property |
| languageCode | string | no | BCP-47 code for translated issue messages, e.g. "de-CH". Default en-US. |
The sitemaps registered on a Search Console property: when each was last submitted and last downloaded by Google, whether Google has processed it yet, its error and warning counts, and per content type how many URLs it SUBMITTED against how many are INDEXED. That last pair is the fastest read on whether a site has an indexing problem rather than a ranking one. Pass feedpath for one sitemap inste
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the exact property string from list_search_console_sites |
| feedpath | string | no | the FULL url of one sitemap, e.g. "https://example.com/sitemap.xml" |
| sitemapIndex | string | no | the FULL url of a sitemap INDEX, to list the sitemaps it contains |
Tell Google about a sitemap. feedpath is the sitemap's COMPLETE URL ("https://example.com/sitemap.xml"), never a path — Google addresses a sitemap by its full url. Re-submitting one that is already registered is harmless and simply re-notifies Google. THE ANSWER IS READ BACK FROM GOOGLE, not echoed from the request: it reports whether the sitemap is now actually registered, whether Google has pr
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the exact property string from list_search_console_sites |
| feedpath | string | yes | the FULL sitemap URL, e.g. "https://example.com/sitemap.xml" |
Remove a sitemap from a property. THIS DOES NOT REMOVE THOSE PAGES FROM GOOGLE'S INDEX — it stops Google using that file to discover new ones, and it can be submitted again at any time, so it is reversible. CONFIRM-GATED: called without confirm it removes nothing and reports what the sitemap actually IS, read back from Google (its error and warning counts and how many URLs it covers), so you can c
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the exact property string from list_search_console_sites |
| feedpath | string | yes | the FULL sitemap URL to remove |
| confirm | boolean | no | true to actually remove it. Without this nothing is removed and the sitemap is described back to you. |
Add a property to the connected Google account. TWO THINGS THAT MUST REACH THE USER, and the answer states both: (1) ADDING IS NOT VERIFYING — the property arrives with the account as an unverified user and EVERY read on it is refused until ownership is proven with a DNS record, an HTML file or a tag, which no API can do and which the user completes in Search Console itself; (2) the new property i
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | "sc-domain:example.com" for a Domain property (covers every scheme and subdomain), or the full URL-prefix form "https://example.com/". These are different properties — pick deliberately. |
Remove a property from the connected Google account. THIS LOSES HISTORY: the account loses access to that property's stored Search Console performance data, and re-adding it later means verifying ownership from scratch — Google does NOT restore the 16-month window on re-add. Nothing about the site itself, its pages or its ranking changes. CONFIRM-GATED: the unconfirmed call removes nothing and des
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the exact property string from list_search_console_sites |
| confirm | boolean | no | true to actually remove it |
The Bing Webmaster Tools sites SHARED WITH THIS PROFILE, each exactly as Bing holds it (e.g. "https://example.com"). CALL THIS FIRST — every other Bing tool takes that exact string. THIS IS NOT EVERY SITE THE CONNECTION CAN REACH: Microsoft issues one key per USER covering every site that Bing account verified, so the user ticks which belong to THIS profile and any other one is refused by name. An
No parameter schema on file.
Bing clicks and impressions over time — for the whole site (mode "site", the default), for one page (mode "page", needs url), or for every page under one directory (mode "directory", needs url). An unknown mode is refused by name. Read-only, 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the exact site string from list_bing_webmaster_sites, and it must be one SHARED with this profile |
| mode | string | no | site (default) | page | directory |
| url | string | no | required for page and directory modes — the full URL |
| page | number | no | zero-based results page, directory mode |
The Bing twin of search_console_performance, split by what you want pinned. Modes: "queries" (top search terms for the site, the default), "pages" (top pages), "pageQueries" (which queries brought traffic to one page), "queryPages" (which pages one query landed on), "queryPageDetail" (one query on one page, day by day). MICROSOFT UPDATES THIS DATA WEEKLY by their own statement, so it lags Sear
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the exact site string from list_bing_webmaster_sites |
| mode | string | no | queries (default) | pages | pageQueries | queryPages | queryPageDetail |
| query | string | no | required for queryPages and queryPageDetail |
| page | string | no | the full page URL — required for pageQueries and queryPageDetail |
REAL SEARCH DEMAND FOR ANY TERM, free. Modes: "keyword" (impressions for exactly this term over a date range), "related" (the same for terms Bing considers related — the one to use for expanding a seed into a list), "history" (the term's historical series). This is the closest thing in Hermoso to a free keyword planner: google_ads_keyword_ideas needs Google Ads Basic access, and this needs only th
| Parameter | Type | Required | Notes |
|---|---|---|---|
| q | string | yes | the keyword to look up, e.g. "running shoes" |
| mode | string | no | keyword (default) | related | history |
| country | string | yes | REQUIRED, e.g. "us" — there is no default and one will not be guessed |
| language | string | yes | REQUIRED, e.g. "en-US" |
| startDate | string | no | YYYY-MM-DD — required for keyword and related modes |
| endDate | string | no | YYYY-MM-DD — required for keyword and related modes |
Whether Bing can actually reach the site's pages: crawl statistics (mode "stats"), the URLs with problems (mode "issues", the default), one page's index detail (mode "page", needs url), a directory's (mode "directory"), or the crawl settings themselves (mode "settings"). Issue names are DECODED from Bing's bitmask into readable ones — Code301, Code302, Code4xx, Code5xx, BlockedByRobotsTxt, Conta
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the exact site string from list_bing_webmaster_sites |
| mode | string | no | issues (default) | stats | page | directory | settings |
| url | string | no | required for page and directory modes |
| page | number | no | zero-based results page, directory mode |
Inbound links according to Bing: counts per page (mode "counts", the default), the inbound links pointing at one page (mode "page", needs url), or the pages the site owner has declared as connected (mode "connected"). Read-only, 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the exact site string from list_bing_webmaster_sites |
| mode | string | no | counts (default) | page | connected |
| url | string | no | required for page mode — the page whose inbound links you want |
| page | number | no | zero-based results page |
How many URLs this site may still submit to Bing today and this month, plus the separate content-submission budget. BING SETS THIS PER SITE AND IT VARIES — Microsoft's own documented example allows 5 a day and 24 a month — so it is always read live and never assumed. Check it before promising anyone a bulk submission, because Bing rejects an over-quota batch as a WHOLE rather than taking what fits
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the exact site string from list_bing_webmaster_sites |
Submit URLs to Bing for indexing — the fastest way to get a new or changed page crawled by Bing, and therefore into Copilot and ChatGPT web search. THE SITE'S REMAINING DAILY QUOTA IS READ FIRST and a batch that would exceed it is refused with the real number rather than half-submitted, because Bing rejects an over-quota batch as a whole and a partial failure leaves you unable to say which URLs ma
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the exact site string from list_bing_webmaster_sites |
| urls | array | yes | full URLs on THIS site — a URL on another host is refused before anything is submitted |
The sitemaps registered for this site — Bing calls them FEEDS — with the status, URL count, file size and last-crawled date Bing holds for each. This is the read to make BEFORE submitting or removing one, because both of those are addressed by the sitemap's full URL exactly as Bing stores it. Pass feedUrl to expand a sitemap INDEX into its child sitemaps (Microsoft's GetFeedDetails); an empty an
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the exact site string from list_bing_webmaster_sites |
| feedUrl | string | no | optional — the full URL of a sitemap INDEX, to list its children |
Register a sitemap with Bing so it discovers new and changed pages by itself — the durable twin of submit_bing_webmaster_urls, which spends a small per-site daily quota every time. Takes the sitemap's FULL URL, not a path. Bing accepts Sitemap, RSS 2.0, Atom 0.3, Atom 1.0 and plain text files (Microsoft's own list). A sitemap hosted on ANOTHER host is not refused — Bing allows that when the other
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the exact site string from list_bing_webmaster_sites |
| feedUrl | string | yes | the FULL sitemap URL, e.g. "https://example.com/sitemap.xml" |
Remove a sitemap from Bing. GATED ON BLAST RADIUS, not just on intent: called WITHOUT confirm it removes nothing and reports what Bing says that sitemap actually carries — its URL count, status and type — and a sitemap carrying any URLs then ALSO needs confirmUrlCount set to that number. confirm:true proves you meant to remove something; the echoed count proves you aimed at the sitemap you ins
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the exact site string from list_bing_webmaster_sites |
| feedUrl | string | yes | the FULL sitemap URL to remove, exactly as list_bing_webmaster_sitemaps shows it |
| confirm | boolean | no | must be true; without it nothing is removed and you get the radius instead |
| confirmUrlCount | number | no | the URL count Bing reports for that sitemap — required when it carries any |
The URLs fetched on demand as Bingbot for this site, or one of them in detail (pass url) including what Bingbot actually received. This is how you see a page through the CRAWLER's eyes rather than a browser's — a robots block, a redirect chain or a JS-only page all look fine in a browser and wrong here. An empty list means nothing has been fetched on demand yet, which is a real answer and not a
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the exact site string from list_bing_webmaster_sites |
| url | string | no | optional — one full URL, for the stored detail of that fetch |
Ask Bingbot to fetch one URL now, so you can then read back what the crawler actually received with list_bing_webmaster_fetched_urls. THE FETCH IS QUEUED, NOT PERFORMED WHILE YOU WAIT — so "it has not appeared in the fetched list yet" is the normal FIRST answer, and it is reported as "requested, not yet done" rather than as a failure or as success. A URL that is not on this site is refused before
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the exact site string from list_bing_webmaster_sites |
| url | string | yes | the full URL on THIS site for Bingbot to fetch |
Give Bing a page's content DIRECTLY instead of waiting for it to crawl — for a page Bingbot renders badly or cannot reach. YOU PASS THE HTML AND NOTHING ELSE: Microsoft's httpMessage parameter is a base64 raw HTTP response whose status line and every header must end CRLF with exactly two CRLFs before the body, and an LF-only message is accepted with a 200 while indexing nothing useful — a silent w
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the exact site string from list_bing_webmaster_sites |
| url | string | yes | the full URL on this site that this content belongs to |
| html | string | yes | the page content Bing should index for that URL |
| status | number | no | HTTP status for the message, default 200 |
| contentType | string | no | default text/html |
| headers | object | no | extra response headers. Content-Length is always computed and never taken from you. |
| structuredData | string | no | optional JSON-LD, for non-HTML content types |
| dynamicServing | string | no | none (default) | pc-laptop | mobile | amp | tablet | non-visual-browser |
Add a site to the connected Bing Webmaster account. Important: ADDING IS NOT VERIFYING, and saying so is most of this tool's value: the site arrives UNVERIFIED and every read on it is refused until an ownership proof is placed on the site itself — an XML file at the root, a meta tag in the home page <head>, or a CNAME DNS record — which no API can do. Place one, then call verify_bing_webmaster_sit
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the site with its scheme, e.g. "https://example.com" |
Ask Bing to CHECK the ownership proof for a site already on the account. IT DOES NOT PLACE THE PROOF — nothing can over an API. The user puts an XML file at the site root, a meta tag in the home page <head>, or a CNAME DNS record, and Bing Webmaster Tools shows the exact filename and value for each. A negative answer therefore means "the proof is not in place yet", never that Hermoso or the connec
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the site to verify, exactly as it was added |
Remove a site from the connected Bing Webmaster account — the heaviest thing in this connector. GATED ON BLAST RADIUS: called WITHOUT confirm it removes nothing and reports whether Bing holds the site as VERIFIED and how many sitemaps are registered on it; a site that is verified or has any sitemaps then ALSO needs confirmSiteUrl set to that exact site string, which is a fact you only have aft
| Parameter | Type | Required | Notes |
|---|---|---|---|
| siteUrl | string | yes | the exact site string from list_bing_webmaster_sites |
| confirm | boolean | no | must be true; without it nothing is removed and you get the radius instead |
| confirmSiteUrl | string | no | the exact site string again — required unless the site is unverified with no sitemaps |
Ping the IndexNow search engines that pages were added, updated or deleted — Bing, Yandex, Naver, Seznam.cz and Yep, all reached by ONE submission. Important: GOOGLE DOES NOT PARTICIPATE IN INDEXNOW and never has, so say that plainly rather than letting anyone expect a Google effect; for Google use submit_search_console_sitemap and search_console_url_inspection. NO CONNECTION IS NEEDED, because th
| Parameter | Type | Required | Notes |
|---|---|---|---|
| host | string | yes | the bare hostname that owns these URLs, e.g. "example.com" |
| key | string | yes | the IndexNow key — 8-128 letters, numbers and dashes — ALREADY hosted as <key>.txt on that site |
| urls | array | yes | the full URLs that were added, updated or deleted, all on host |
| keyLocation | string | no | the full URL of the key file if it is NOT at the site root. Moving it NARROWS the key to that directory only. |
The PostHog projects the connected personal API key can see, and which ONE this profile is pointed at. Only the ACTIVE project is readable. That is deliberate: PostHog's API otherwise falls back to "the last project you visited in the UI", which would make every answer depend on the user's browsing history, so Hermoso pins a project at connect time instead of relying on their implicit default. To
No parameter schema on file.
ASK POSTHOG A QUESTION IN HogQL — their SQL dialect over the events, persons and sessions tables. Use it for per-user funnels, retention and custom breakdowns, e.g. SELECT properties.$current_url, count() FROM events WHERE event = '$pageview' AND timestamp > now() - INTERVAL 7 DAY GROUP BY 1 ORDER BY 2 DESC. IT IS NOT AN EXPORTER AND MUST NEVER BE LOOPED — PostHog's own policy, verbatim: "
| Parameter | Type | Required | Notes |
|---|---|---|---|
| query | string | yes | a HogQL (ClickHouse-flavoured SQL) query — one bounded question, never a page of an export |
| limit | number | no | row cap, 1–1000 (default 1000). A LIMIT already in the query is honoured and clamped to the same ceiling |
Read back an insight the user already BUILT in their PostHog UI — funnels, retention curves, trends and paths. Call with NO id to LIST the saved insights with their names and ids; call with insightId for that insight's definition and computed result. THIS IS THE RIGHT WAY TO ANSWER A FUNNEL OR RETENTION QUESTION when the report already exists, and the reason is a documentation fact rather than a p
| Parameter | Type | Required | Notes |
|---|---|---|---|
| insightId | string | no | omit to list the saved insights and their ids |
| refresh | boolean | no | default true (recompute if stale); false = return the cached result even if stale |
SESSION REPLAYS. Lists recent recordings with who they belong to, how long they ran, how many clicks / keypresses / CONSOLE ERRORS each had, the URL they started on, and a link that opens the replay in PostHog. USE THE SIGNALS RATHER THAN JUST LISTING THEM: a session with console errors and a high keypress count on a checkout page is a bug report with a video attached, and that is the insight wort
| Parameter | Type | Required | Notes |
|---|---|---|---|
| limit | number | no | 1–100, default 20 |
Look up PEOPLE in PostHog by distinct id, email or a free-text search, with their properties. Use it to resolve a specific customer, then use their distinct id inside posthog_query to see exactly what that one human did. Read-only, 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| distinctId | string | no | PostHog distinct id |
| string | no | exact email match | |
| search | string | no | free-text search across person properties |
| limit | number | no | 1–100, default 20 |
READ BACK A SAVED MIXPANEL REPORT by its bookmark id — and this is the PREFERRED Mixpanel lane, not a fallback. Mixpanel has put BOTH its Segmentation and its Funnels query APIs in maintenance mode and recommends in their place that the report is built in the Mixpanel UI and read programmatically through Insights, which is exactly what this does. THE BOOKMARK ID MUST COME FROM THE USER: Mixpanel p
| Parameter | Type | Required | Notes |
|---|---|---|---|
| bookmarkId | string | yes | the saved report's bookmark id, from its URL in Mixpanel |
MIXPANEL RETENTION — how many of the people who did a first thing came back and did another, cohorted by day / week / month. This endpoint is FULLY SUPPORTED, unlike segmentation and funnels, which Mixpanel has put in maintenance mode — so it is the one typed Mixpanel report to reach for first. retentionType "birth" cohorts people by their FIRST occurrence of bornEvent (new-user retention, and Mix
| Parameter | Type | Required | Notes |
|---|---|---|---|
| fromDate | string | no | YYYY-MM-DD (default 30 days ago) |
| toDate | string | no | YYYY-MM-DD (default today) |
| retentionType | string | no | "birth" = cohort by first occurrence of bornEvent; "compounded" = anyone active |
| bornEvent | string | no | the event that puts someone into the cohort — REQUIRED by Mixpanel when retentionType is "birth", which is the default |
| event | string | no | the returning event; omitted means any event |
| unit | string | no | the interval unit (default day) |
| interval | number | no | the WIDTH of each bucket, in DAYS. Mixpanel cannot take this together with "unit" — it answers HTTP 500 to the pair, so pass one or the other (default: interval 1 day) |
| intervalCount | number | no | HOW MANY buckets to return (default 1). A "0th" bucket is always included for events inside the first interval |
| bornWhere | string | no | a Mixpanel segmentation expression filtering who ENTERS the cohort (the born event) |
| where | string | no | a Mixpanel segmentation expression filtering the RETURNING event |
| on | string | no | a property expression to segment the returning event on — this is what breaks a retention curve down by e.g. plan or platform |
| limit | number | no | top N segmentation values; does nothing unless "on" is set |
| unboundedRetention | boolean | no | accumulate right-to-left, so day N means "retained on day N or any day after" |
One Mixpanel event over time, optionally broken down by a property (on) and filtered (where). Warning: MIXPANEL HAS PUT THIS ENDPOINT IN MAINTENANCE MODE — their words: "We recommend discontinuing new use of this endpoint. To break down and filter event data, build an Insights report in-app and query it programmatically with the Insights Query API." It still answers today, which is why it is o
| Parameter | Type | Required | Notes |
|---|---|---|---|
| event | string | yes | a SINGLE event name — not an array |
| fromDate | string | no | YYYY-MM-DD (default 30 days ago) |
| toDate | string | no | YYYY-MM-DD (default today) |
| on | string | no | a segmentation expression to break down by, e.g. properties["$browser"] |
| where | string | no | a segmentation expression to filter by |
| unit | string | no | NOTE: Mixpanel offers no "week" on segmentation, unlike retention |
| interval | number | no | the number of days each bucket covers — Mixpanel offers this in lieu of "unit" when "type" is not "general" |
| type | string | no | count type: general (event count, default), unique (distinct users) or average |
| limit | number | no | top N property values; Mixpanel defaults to 60, max 10000, and it does nothing unless "on" is set |
A saved Mixpanel funnel. Call with NO id to LIST the saved funnels and their ids; call with funnelId to read its conversion data. Warning: MIXPANEL HAS PUT THE FUNNELS QUERY API IN MAINTENANCE MODE — their words: "We recommend discontinuing new use of this endpoint. To get funnel data, build a Funnels report in-app and query it programmatically with the Insights Query API." So prefer mixpanel_insi
| Parameter | Type | Required | Notes |
|---|---|---|---|
| funnelId | string | no | omit to list the saved funnels and their ids |
| fromDate | string | no | YYYY-MM-DD (default 30 days ago) |
| toDate | string | no | YYYY-MM-DD (default today) |
| length | number | no | the conversion window, counted in lengthUnits — the TOTAL may not exceed 90 days. Omit to use the funnel own saved value |
| lengthUnit | string | no | the unit "length" is counted in (Mixpanel offers no week or month here). Omit to use the funnel own saved value |
| unit | string | no | the bucket the results are grouped into |
| interval | number | no | the number of days each bucket covers — an alternative to "unit" (default 1) |
| on | string | no | a property expression to break the funnel down by |
| where | string | no | a segmentation expression to filter by |
| limit | number | no | top N property values; Mixpanel defaults to 255, max 10000, and it does nothing unless "on" is set |
The EVENT VOCABULARY of the Mixpanel project — the most common event names over THE LAST 31 DAYS, so a query names something the project actually records rather than a guess. Start here before any other Mixpanel call. Pass event to list THAT event's top property names instead; the two are different questions with different answers, and a failure to read properties is reported AS a failure and ne
| Parameter | Type | Required | Notes |
|---|---|---|---|
| event | string | no | pass an event name to list ITS properties instead of listing events |
| window | string | no | "vocabulary" (default) = the most common events over the last 31 days, names only. "today" = today only, with counts and the change from yesterday — a much narrower question |
| type | string | no | count type for the event amounts: general (default), unique or average |
| limit | number | no | Mixpanel own defaults are 255 for the vocabulary, 100 for today and 10 for properties |
AMPLITUDE EVENT SEGMENTATION — an event over time, with filters and group-bys. metric is uniques / totals / pct_dau / average / histogram / sums / value_avg / formula (and "formula" additionally REQUIRES formula, or Amplitude refuses it). Dates take YYYY-MM-DD or YYYYMMDD. AMPLITUDE'S RATE LIMIT IS COST-BASED, not per-request: cost = days × conditions × query type, 1000 per 5-minute window, so o
| Parameter | Type | Required | Notes |
|---|---|---|---|
| events | array | no | one or two event specs: a plain name, or {eventType, filters:[{subprop_type,subprop_key,subprop_op,subprop_value}], groupBy:[…]} |
| event | string | no | shorthand for a single event name |
| metric | string | no | default uniques |
| formula | string | no | required when metric is "formula" |
| start | string | no | YYYYMMDD or YYYY-MM-DD (default 30 days ago) |
| end | string | no | YYYYMMDD or YYYY-MM-DD (default today) |
| interval | number | no | -300000 realtime, -3600000 hourly, 1 daily, 7 weekly, 30 monthly |
| groupBy | string | no | user property or event property to break the series down by, e.g. gp:utm_campaign or country |
| groupBy2 | string | no | a SECOND group-by (Amplitude's g2), e.g. groupBy gp:utm_campaign + groupBy2 country |
| segments | array | no | FILTER to only these users (Amplitude's segment s): [{prop:'gp:utm_campaign', op:'is', values:['summer']}]. op is one of is, is not, contains, does not contain, less, less or equal, greater, greater |
| limit | number | no | ≤1000 |
AMPLITUDE FUNNELS: conversion and drop-off across ordered steps. Pass events as an ORDERED array of at least two steps. mode is ordered / unordered / sequential. conversionWindowSeconds is how long a user has to complete the funnel and DEFAULTS TO 2,592,000 SECONDS (30 DAYS) — ALWAYS state which window a conversion rate was measured over, because a 30-day window makes a funnel look dramatically
| Parameter | Type | Required | Notes |
|---|---|---|---|
| events | array | yes | ordered steps — plain event names, or {eventType, filters:[…]} objects |
| mode | string | no | default ordered |
| conversionWindowSeconds | number | no | Amplitude default is 2592000 (30 days) — say which window you used |
| start | string | no | YYYYMMDD or YYYY-MM-DD (default 30 days ago) |
| end | string | no | YYYYMMDD or YYYY-MM-DD (default today) |
| groupBy | string | no | at most ONE — a second is refused |
| users | string | no | 'new' = first-time users only (the acquisition funnel); Amplitude's default is active |
| segments | array | no | FILTER to only these users (Amplitude's segment s): [{prop:'gp:utm_campaign', op:'is', values:['summer']}]. op is one of is, is not, contains, does not contain, less, less or equal, greater, greater |
AMPLITUDE RETENTION — of the people who did startEvent, how many came back and did returnEvent. The two magic values are "_new" (first-time users) and "_active" (any active user), which is what makes "new-user retention" a single call. retentionMode is n-day / bracket / rolling, and "bracket" additionally REQUIRES brackets — it is refused without them rather than silently switched to another mod
| Parameter | Type | Required | Notes |
|---|---|---|---|
| startEvent | string | no | "_new" (default) or an event name |
| returnEvent | string | no | "_active" (default) or an event name |
| start | string | no | YYYYMMDD or YYYY-MM-DD (default 30 days ago) |
| end | string | no | YYYYMMDD or YYYY-MM-DD (default today) |
| retentionMode | string | no | n-day, bracket (needs brackets) or rolling |
| brackets | array | no | required when retentionMode is "bracket" |
| interval | number | no | 1 daily, 7 weekly, 30 monthly |
| groupBy | string | no | ONE property to group by |
| segments | array | no | FILTER to only these users (Amplitude's segment s): [{prop:'gp:utm_campaign', op:'is', values:['summer']}]. op is one of is, is not, contains, does not contain, less, less or equal, greater, greater |
Amplitude ACTIVE or NEW user counts over a date range — the top-line "is the product growing" number, and the cheapest Amplitude call to run first after connecting because it proves the credential with no setup. metric is "active" or "new"; interval 1 daily, 7 weekly, 30 monthly. An empty series on a new project is the CORRECT answer and means the connection works and there is no data yet — never
| Parameter | Type | Required | Notes |
|---|---|---|---|
| start | string | no | YYYYMMDD or YYYY-MM-DD (default 30 days ago) |
| end | string | no | YYYYMMDD or YYYY-MM-DD (default today) |
| metric | string | no | default active |
| interval | number | no | 1 daily, 7 weekly, 30 monthly |
| groupBy | string | no | ONE property to group by |
| segments | array | no | FILTER to only these users (Amplitude's segment s): [{prop:'gp:utm_campaign', op:'is', values:['summer']}]. op is one of is, is not, contains, does not contain, less, less or equal, greater, greater |
ONE PERSON'S ACTIVITY STREAM in Amplitude. Pass a user id, device id or user-id PREFIX and it SEARCHES; pass a numeric Amplitude ID and it returns that user's recent events with their properties. Two steps, because Amplitude splits them across two endpoints: search resolves a human-typed identifier to an Amplitude ID, and only that id reads an activity stream. NOTE these two endpoints sit on a DIF
| Parameter | Type | Required | Notes |
|---|---|---|---|
| user | string | yes | a numeric Amplitude ID to read activity, or a user id / device id / user-id prefix to search |
| search | boolean | no | force a search even when the value is numeric |
| limit | number | no | maximum events to return, up to 1000 |
The Amplitude TAXONOMY — the project's declared events, event properties, user properties or group properties, so a query names something real instead of a guess. category is event (default) / category / event-property / user-property / group-property. IF AMPLITUDE REFUSES THIS, REPORT THEIR REFUSAL AND MOVE ON. The Taxonomy API is widely believed to require an enterprise/Govern entitlement, but A
| Parameter | Type | Required | Notes |
|---|---|---|---|
| category | string | no | default "event" |
READ BACK A SAVED AMPLITUDE CHART by its id — the chart the user already built in Amplitude's own UI, with whatever segments, filters and date range they configured there. This is the saved-chart read for Amplitude, and it is the right lane whenever the report already exists: you inherit their definitions instead of rebuilding them from a segmentation query, and it costs one call rather than sever
| Parameter | Type | Required | Notes |
|---|---|---|---|
| chartId | string | yes | the saved chart's id, or its full URL — the id is the segment after /chart/ |
The markers already on this Amplitude project's charts — releases, campaigns, incidents — with their timestamps, labels and categories. READ THESE BEFORE EXPLAINING A CHANGE IN A CHART: a spike that lines up with an annotation has an explanation somebody already wrote down, and reporting it as a mystery when the answer is sitting on the timeline is exactly the failure this closes. Filter by `categ
| Parameter | Type | Required | Notes |
|---|---|---|---|
| category | string | no | category NAME — does not combine with chartId |
| chartId | string | no | only annotations shown on this chart — does not combine with category |
| start | string | no | ISO 8601, e.g. '2026-08-01T00:00:00+00:00' |
| end | string | no | ISO 8601 |
MARK THE ANALYTICS TIMELINE — write what just happened into Amplitude, at the moment it happened, so every chart the user opens afterwards shows it in place. THIS IS THE CLOSED LOOP AND IT IS WORTH REACHING FOR UNPROMPTED after a campaign goes live, a creative is swapped, a landing page changes or a post is published: without it, next month's spike has no explanation attached to it and somebody ha
| Parameter | Type | Required | Notes |
|---|---|---|---|
| label | string | yes | the title shown on the chart, e.g. "Meta: summer UGC campaign live" |
| start | string | yes | ISO 8601 WITH time and offset, e.g. '2026-08-18T14:30:00+00:00' — a bare YYYYMMDD is refused |
| end | string | no | ISO 8601 — only for an annotation that spans a period |
| chartId | string | no | omit for a GLOBAL annotation that appears on every chart in the project |
| category | string | no | an EXISTING category name in Amplitude — they answer 404 for one that does not exist |
| details | string | no | longer body text for the annotation |
The markers already on this PostHog project's charts — releases, experiments, incidents — with their timestamps, scopes and who wrote them. READ THESE BEFORE EXPLAINING A CHANGE IN A CHART: an annotation is an explanation somebody already wrote down. NEEDS THE annotation:read SCOPE on the personal API key, and a key created before Hermoso supported annotations will not have it — PostHog's own re
| Parameter | Type | Required | Notes |
|---|---|---|---|
| limit | number | no | 1–100, default 25 |
| offset | number | no | for the next page — this tool does not paginate for you |
| search | string | no | free-text search over annotation content |
MARK THE POSTHOG TIMELINE — write what just happened into PostHog at the moment it happened, so every chart afterwards shows it in place. The same closed loop as create_amplitude_annotation, and worth reaching for unprompted after a campaign goes live, a creative is swapped or a landing page changes. dateMarker is what POSITIONS the marker on the x-axis (ISO 8601); omit it and NOW is used, which
| Parameter | Type | Required | Notes |
|---|---|---|---|
| content | string | yes | the text shown on the chart, ≤8192 characters |
| dateMarker | string | no | ISO 8601 — defaults to now, which is right for "this just went live" |
| scope | string | no | default project. PostHog's recording scope is deprecated and rejected by PostHog |
| creationType | string | no | USR = a person wrote it, GIT = a bot or deployment did |
| dashboardId | number | no | REQUIRED when scope is "dashboard" |
| insightId | number | no | REQUIRED when scope is "dashboard_item" — the numeric id of the insight |
The markers already on this Mixpanel project's charts (launches, campaigns, incidents) with their date, text, tags and who wrote them. READ THESE BEFORE EXPLAINING A CHANGE IN A CHART: an annotation is an explanation somebody already wrote down. fromDate / toDate (YYYY-MM-DD, both inclusive) narrow the window. Read-only, 0 credits. Needs Mixpanel connected.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| fromDate | string | no | YYYY-MM-DD, inclusive |
| toDate | string | no | YYYY-MM-DD, inclusive |
MARK THE MIXPANEL TIMELINE: write what just happened into Mixpanel at the moment it happened, so every chart afterwards shows it in place. The same closed loop as create_posthog_annotation and create_amplitude_annotation, worth reaching for unprompted after a campaign goes live, a creative is swapped or a landing page changes. date is "YYYY-MM-DD HH:mm:ss" (Mixpanel's own format, charted in the
| Parameter | Type | Required | Notes |
|---|---|---|---|
| description | string | yes | the text shown on the chart, e.g. "Meta: summer UGC campaign went live" |
| date | string | no | "YYYY-MM-DD HH:mm:ss", "YYYY-MM-DD" or ISO with offset; default now |
| tags | array | no | annotation-tag ids |
Change the text or tags of a Mixpanel annotation (annotationId from list_mixpanel_annotations). Mixpanel cannot move an annotation to a new date; to fix a wrong date, delete it and create a new one. The edited row is read back. Needs the ANALYST role on the service account. 0 credits. Needs Mixpanel connected.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| annotationId | number | yes | from list_mixpanel_annotations |
| description | string | no | new annotation text; empty text is refused |
| tags | array | no | annotation-tag ids |
Permanently delete a Mixpanel annotation from every chart in the project. WITHOUT confirm:true nothing is deleted and you get the exact annotation that would go, to show the user; call again with confirm:true only after they say yes. The delete is verified by reading it back. Needs the ANALYST role on the service account. 0 credits. Needs Mixpanel connected.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| annotationId | number | yes | from list_mixpanel_annotations |
| confirm | boolean | no | true only after the user approved deleting this exact annotation |
PostHog SURVEYS. Without surveyId: the project's surveys (name, type, running / complete / draft / archived, dates, questions); filter by status or search. With surveyId: that survey's questions (with their ids, for posthog_survey_responses) plus its stats: shown / dismissed / sent counts, response and dismissal rates, and per-question distributions and rating averages. NEEDS THE OPTIONAL survey:r
| Parameter | Type | Required | Notes |
|---|---|---|---|
| surveyId | string | no | one survey: its questions and stats |
| status | string | no | only surveys in this state |
| search | string | no | match survey names containing this text |
| archived | boolean | no | PostHog archived filter: true or false; omit to leave it unfiltered |
| limit | number | no | 1-100, default 25 |
| offset | number | no | skip this many surveys; use the nextOffset from the previous reply |
| dateFrom | string | no | stats window start, ISO |
| dateTo | string | no | stats window end, ISO |
THE ANSWERS people gave a PostHog survey, with the QUESTION TEXT resolved beside each answer: who (distinct_id), when, country, device and the page they were on. The customers' own words: the best raw input for ad hooks, angles and objections. questionId narrows to one question; with scoreLte / scoreGte on a rating question it pulls e.g. NPS detractors (scoreLte 6) or promoters (scoreGte 9). since
| Parameter | Type | Required | Notes |
|---|---|---|---|
| surveyId | string | yes | from list_posthog_surveys |
| questionId | string | no | one question id from the survey (see list_posthog_surveys with surveyId) |
| scoreLte | number | no | needs questionId; e.g. 6 for NPS detractors |
| scoreGte | number | no | needs questionId; e.g. 9 for NPS promoters |
| since | string | no | ISO timestamp |
| until | string | no | ISO timestamp |
| excludeArchived | boolean | no | true = leave out responses to archived questions |
| limit | number | no | 1-500, default 100 |
| offset | number | no | skip this many responses; use the nextOffset from the previous reply |
THE SAVED COHORTS in the Mixpanel project — each with its id, name, description and CURRENT MEMBER COUNT. Two uses, and both matter for advertising: the counts alone answer "how big is our converted audience", and the id is what mixpanel_profiles needs to read the PEOPLE in one. A cohort hidden in Mixpanel's UI is still listed and flagged rather than dropped, because hidden is a display choice and
No parameter schema on file.
THE PEOPLE in Mixpanel, with their profile properties. Pass cohortId (from list_mixpanel_cohorts) for "who is in the Converted cohort and what do we know about them", or where for an ad-hoc filter over profile properties, or distinctId for one person. PASS outputProperties ALMOST ALWAYS: it cuts the payload to the properties you actually need, and Mixpanel says it "can drastically reduce t
| Parameter | Type | Required | Notes |
|---|---|---|---|
| cohortId | string | no | a numeric cohort id from list_mixpanel_cohorts |
| where | string | no | a Mixpanel segmentation expression over profile properties, e.g. properties["$city"] == "London" |
| distinctId | string | no | one person |
| distinctIds | array | no | several people by distinct id |
| outputProperties | array | no | only return these properties — strongly recommended, it is much faster and much smaller |
| dataGroupId | string | no | group key — for GROUP profiles rather than user profiles |
| page | number | no | pages start at ZERO; any page above 0 REQUIRES sessionId |
| sessionId | string | no | from the previous page — required for page > 0 |
| includeAllUsers | boolean | no | only applies alongside cohortId; false = only distinct ids that actually have a profile |
The brand's Vercel projects (its websites): name, id, framework, production URL and state, the latest deployment (URL, state, target, when), the linked git repo, and whether Web Analytics and Speed Insights are on. search filters by project name. Read-only, free. Needs Vercel connected (Settings > Connectors > Vercel, or connect_connector with an access token).
| Parameter | Type | Required | Notes |
|---|---|---|---|
| search | string | no | part of a project name |
| limit | number | no | 1-50, default 20 |
| cursor | string | no | nextCursor from a previous call |
Deployments on the brand's Vercel account, newest first: URL, state (READY, BUILDING, QUEUED, ERROR, CANCELED...), target (production or preview), when it was created and became ready, the commit message and Vercel's inspector link. Filter by projectId, target or state, or pass deploymentId (an id or a deployment URL) to read one, e.g. to see whether a page you just deployed is READY. Read-only, f
| Parameter | Type | Required | Notes |
|---|---|---|---|
| projectId | string | no | project id or name |
| deploymentId | string | no | read ONE deployment by id or URL |
| target | string | no | filter by environment: production, preview or staging |
| state | string | no | filter to deployments in this state |
| limit | number | no | 1-50, default 20 |
| until | string | no | the nextCursor a previous page returned — the next (older) page |
The domains on the brand's Vercel account (verified or not, when they expire), or with projectId the domains assigned to one project (and any redirect or git branch on them). Read-only, free. Needs Vercel connected (Settings > Connectors > Vercel, or connect_connector with an access token).
| Parameter | Type | Required | Notes |
|---|---|---|---|
| projectId | string | no | project id or name, for that project's domains |
| limit | number | no | 1-50, default 20 |
Vercel WEB ANALYTICS for one project: page views and unique visitors over a window (default the last 30 days, production traffic), and with by a breakdown by requestPath (which pages), referrerHostname (where visitors came from), country, deviceType, osName, browserName, route, utmSource / utmMedium / utmCampaign / utmContent / utmTerm (which campaigns and ads brought them), or a time step (day,
| Parameter | Type | Required | Notes |
|---|---|---|---|
| projectId | string | yes | project id or name (list_vercel_projects) |
| by | string | no | one or TWO comma-separated dimensions (at most one time step): requestPath, referrerHostname, country, deviceType, osName, browserName, route, utmSource, utmMedium, utmCampaign, utmContent, utmTerm, e |
| days | number | no | 1-366, default 30 |
| since | string | no | YYYY-MM-DD (instead of days) |
| until | string | no | YYYY-MM-DD (default now) |
| limit | number | no | rows in the breakdown, 1-100, default 10 |
| filter | string | no | Vercel's OData filter, e.g. utmSource eq 'facebook' |
Vercel CUSTOM EVENTS, the CONVERSIONS on a Vercel-hosted site (signups, purchases, any track('Name') from @vercel/analytics): the count and unique visitors over a window, and with by a breakdown by eventName, eventData/<property> (e.g. eventData/plan), utmSource / utmMedium / utmCampaign / utmContent / utmTerm (which ad or campaign drove the conversion), requestPath, country and the rest, or a t
| Parameter | Type | Required | Notes |
|---|---|---|---|
| projectId | string | yes | project id or name (list_vercel_projects) |
| eventName | string | no | only this event, e.g. 'Signup' |
| by | string | no | one or TWO comma-separated dimensions: eventName, eventData, eventData/<property>, utmSource, utmMedium, utmCampaign, utmContent, utmTerm, requestPath, referrerHostname, country, deviceType, osName, b |
| days | number | no | 1-366, default 30 |
| since | string | no | YYYY-MM-DD (instead of days) |
| until | string | no | YYYY-MM-DD (default now) |
| limit | number | no | rows in the breakdown, 1-100, default 10 |
| filter | string | no | Vercel's OData filter, e.g. utmSource eq 'facebook' |
Publish a STATIC landing page to the brand's Vercel account from HTML (it becomes index.html) or a few files [{file, data, encoding:'utf-8'|'base64'}], up to 50 files and 4 MB. project names the Vercel project (created if it does not exist; it becomes part of the URL). target 'preview' (default) makes a new preview URL and leaves the live site alone; 'production' REPLACES what the project's domain
| Parameter | Type | Required | Notes |
|---|---|---|---|
| project | string | yes | Vercel project name, e.g. spring-sale |
| html | string | no | the full HTML of the page |
| files | array | no | instead of (or as well as) html: relative paths with their content |
| target | string | no | default preview |
| confirm | boolean | no | true only after the user approved this exact deploy |
PERMANENTLY delete one Vercel deployment: its URL stops working and Vercel has no undo. Without confirm:true nothing is deleted and you get the deployment it would delete (URL, target, state) to show the user; a production deployment the domains still serve takes the site down. Call again with confirm:true only after the user says yes to that exact deployment. Free. Needs Vercel connected (Setting
| Parameter | Type | Required | Notes |
|---|---|---|---|
| deploymentId | string | yes | from list_vercel_deployments |
| confirm | boolean | no | true only after the user approved deleting this exact deployment |
Revenue from the brand's OWN Stripe account: gross, refunds, net and succeeded-charge count per day/week/month, new customers in the window, and active subscriptions + MRR where the key can read them. This is the money side of the loop — read an ad, a hook or a launch against real revenue instead of clicks. Default window is the last 30 days; a window with no charges answers a real zero, not a fai
| Parameter | Type | Required | Notes |
|---|---|---|---|
| since | string | no | YYYY-MM-DD (default 30 days ago) |
| until | string | no | YYYY-MM-DD (default today) |
| granularity | string | no | default day |
| currency | string | no | ISO code to isolate one currency, e.g. USD; omit to sum every currency as-is |
Recent customers on the brand's OWN Stripe account — email, name, created, currency, delinquent — newest first, or one customer by exact email. Read-only, free. Needs Stripe connected.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| string | no | exact email match | |
| limit | number | no | 1–100, default 25 |
| after | string | no | pagination cursor: the last id of the previous page |
Recent charges on the brand's OWN Stripe account — amount, currency, status, refunds, customer email, description — newest first, optionally for one customer. Read-only, free. Needs Stripe connected.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | no | a Stripe customer id (cus_…) |
| limit | number | no | 1–100, default 25 |
| after | string | no | pagination cursor: the last id of the previous page |
Subscriptions on the brand's OWN Stripe account with each item's price in MAJOR units (19.00 means $19.00), interval and quantity, and the MRR each contributes. With NO status it returns everything that counts toward MRR — active + trialing + past_due — and says so; pass status all, canceled, unpaid, incomplete, incomplete_expired or paused for the rest, or filter by customer or price. Read-only,
| Parameter | Type | Required | Notes |
|---|---|---|---|
| status | string | no | filter by status (all, active, trialing, past_due, canceled and others); omit for MRR statuses |
| customerId | string | no | Stripe customer id, cus_... |
| priceId | string | no | Stripe price id, price_...; only subscriptions on this price |
| limit | number | no | rows to return, 1 to 100; default 25 |
| after | string | no | pagination cursor: the last subscription id from the previous page |
Invoices on the brand's Stripe account, newest first: number, status (draft, open, paid, uncollectible, void), amount due and paid, customer email, dates and the hosted invoice link; filter by status or customer. Read-only, free. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Parameter | Type | Required | Notes |
|---|---|---|---|
| status | string | no | filter to draft, open, paid, uncollectible or void |
| customerId | string | no | Stripe customer id, cus_... |
| limit | number | no | rows to return, 1 to 100; default 25 |
| after | string | no | pagination cursor: the last invoice id from the previous page |
Products on the brand's Stripe account with their active prices (amount, currency, one-time or recurring interval), which is what create_stripe_payment_link and create_stripe_subscription take. active:false lists archived products too. Read-only, free. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Parameter | Type | Required | Notes |
|---|---|---|---|
| active | boolean | no | false also lists archived products; omit or true for active products only |
| limit | number | no | rows to return, 1 to 100; default 25 |
| after | string | no | pagination cursor: the last product id from the previous page |
The brand's Stripe balance (available and pending, per currency) and the most recent payouts with status and arrival date. Read-only, free. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Parameter | Type | Required | Notes |
|---|---|---|---|
| limit | number | no | number of recent payouts to show, 1 to 100; default 25 |
Refunds on the brand's Stripe account, newest first: amount, status, reason and the charge refunded; filter by charge. Read-only, free. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Parameter | Type | Required | Notes |
|---|---|---|---|
| chargeId | string | no | only refunds of this charge, ch_... |
| limit | number | no | rows to return, 1 to 100; default 25 |
| after | string | no | pagination cursor: the last refund id from the previous page |
Coupons on the brand's Stripe account: percent or amount off, duration, validity and redemptions. Read-only, free. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Parameter | Type | Required | Notes |
|---|---|---|---|
| limit | number | no | rows to return, 1 to 100; default 25 |
| after | string | no | pagination cursor: the last coupon id from the previous page |
Create a product on the brand's Stripe account together with its default price: name, optional description, amount in MAJOR units — 19 or 19.00 both mean $19.00, and a zero-decimal currency like JPY is whole — a 3-letter currency, and an optional recurring interval (day, week, month, year) with intervalCount. Returns both ids, read back. On a LIVE key it shows what it is about to create and need
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | product name |
| description | string | no | product description shown to customers in Stripe |
| amount | number | yes | MAJOR units — 19 or 19.00 is $19.00, never 1900 |
| currency | string | yes | 3-letter ISO currency code, e.g. usd |
| interval | string | no | recurring interval: day, week, month or year; omit for a one-time price |
| intervalCount | number | no | intervals between charges, e.g. 3 with month for quarterly |
| nickname | string | no | internal label for the default price; not shown to customers |
| metadata | object | no | key/value metadata stored on the product |
| confirm | boolean | no | true on a live key, after the user approves the preview |
Add a price to an existing Stripe product: amount in MAJOR units (19 or 19.00 is $19.00, never 1900), currency, optional recurring interval and intervalCount, optional nickname. Read back. LIVE key: confirm:true after showing the preview. Idempotent on its arguments. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Parameter | Type | Required | Notes |
|---|---|---|---|
| productId | string | yes | product id, prod_..., from create_stripe_product or list_stripe_products |
| amount | number | yes | MAJOR units — 19 or 19.00 is $19.00, never 1900 |
| currency | string | yes | 3-letter ISO currency code, e.g. usd |
| interval | string | no | recurring interval: day, week, month or year; omit for a one-time price |
| intervalCount | number | no | intervals between charges, e.g. 3 with month for quarterly |
| nickname | string | no | internal label for the price; not shown to customers |
| confirm | boolean | no | true on a live key, after the user approves the preview |
Create a shareable Stripe Payment Link: for ONE price pass priceId (price_…) with an optional quantity; for several pass lineItems: [{priceId, quantity}]. Optionally redirect to afterCompletionUrl when paid. allowPromotionCodes:true shows the promotion-code box at checkout — needed for any 'use code X' ad. referenceId and utm return a taggedUrl: put THAT URL in the ad, and get_stripe_payment_link_
| Parameter | Type | Required | Notes |
|---|---|---|---|
| priceId | string | no | one price — the shorthand for a single line item |
| quantity | number | no | quantity of priceId, e.g. 2 |
| lineItems | array | no | several prices at once; use instead of priceId |
| afterCompletionUrl | string | no | URL the buyer is sent to after paying |
| allowPromotionCodes | boolean | no | show the promotion-code box at checkout |
| referenceId | string | no | letters, digits, dashes, underscores (up to 200) — tags the returned taggedUrl for one ad; Stripe stores it on each purchase |
| utm | object | no | UTM values on the taggedUrl (letters, digits, dashes, underscores, up to 150 each); Stripe passes them to the after-payment redirect only |
| metadata | object | no | key/value metadata stored on the payment link |
| confirm | boolean | no | true on a live key, after the user approves the preview |
Create a Stripe coupon: exactly one of percentOff (1–100) or amountOff (major units) + currency; duration once (default), repeating (with durationInMonths) or forever; optional name, id and maxRedemptions. A coupon alone has NO code a customer can type: pass code (letters, digits, dashes) to also mint the customer-typable promotion code in the same call, with optional codeMaxRedemptions, expires
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | no | display name for the coupon, for your records |
| id | string | no | custom coupon id; Stripe generates one if omitted |
| percentOff | number | no | percent discount, 1 to 100; use this or amountOff |
| amountOff | number | no | fixed discount in major units, e.g. 5 = 5.00; needs currency |
| currency | string | no | 3-letter currency code; required with amountOff |
| duration | string | no | once (default), repeating or forever |
| durationInMonths | number | no | months the discount applies, with duration repeating |
| maxRedemptions | number | no | uses of the COUPON across every code |
| code | string | no | the customer-typable promotion code, e.g. SUMMER20 — letters, digits and dashes only; refused (never altered) otherwise |
| codeMaxRedemptions | number | no | uses of this CODE |
| expiresAt | string | no | YYYY-MM-DD the code stops working |
| firstTimeOnly | boolean | no | the code works only on a customer's first purchase |
| minimumAmount | number | no | minimum order in MAJOR units for the code |
| minimumAmountCurrency | string | no | currency for the code's minimumAmount, e.g. usd |
| confirm | boolean | no | true on a live key, after the user approves the preview |
Create a Stripe customer by email (optional name, phone, description, metadata). If a customer with that email already exists it is returned instead and nothing is created — pass allowDuplicate:true to create a second one on purpose. LIVE key: confirm:true after showing the preview. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Parameter | Type | Required | Notes |
|---|---|---|---|
| string | yes | customer email; the key Stripe matches duplicates on | |
| name | string | no | customer name |
| phone | string | no | customer phone number |
| description | string | no | internal note on the customer record |
| metadata | object | no | key/value metadata stored on the customer |
| allowDuplicate | boolean | no | true = create a second customer even if this email already has one |
| confirm | boolean | no | true on a live key, after the user approves the preview |
Subscribe a Stripe customer (cus_…) to a price (price_…), optional quantity and trialDays. THIS CHARGES THE CUSTOMER'S SAVED PAYMENT METHOD when there is no trial, so it ALWAYS needs confirm:true (test keys included) after you show the user exactly what will be created. Read back; an INCOMPLETE status means no chargeable card is on file. Needs Stripe connected (Settings > Connectors > Stripe, or c
| Parameter | Type | Required | Notes |
|---|---|---|---|
| customerId | string | yes | customer id, cus_... |
| priceId | string | yes | recurring price id, price_... |
| quantity | number | no | quantity of the price, e.g. number of seats |
| trialDays | number | no | free trial length in days before the first charge |
| metadata | object | no | key/value metadata stored on the subscription |
| confirm | boolean | no | true to charge the saved card; always required, test keys included |
Cancel a Stripe subscription: at the end of the current period by default (the customer keeps access until then), or immediately:true to end it now. ALWAYS needs confirm:true, test keys included. The read-back distinguishes the two — a period-end cancel reports the date it will end and the status it keeps until then, an immediate one reports status canceled. Needs Stripe connected (Settings > Conn
| Parameter | Type | Required | Notes |
|---|---|---|---|
| subscriptionId | string | yes | subscription id, sub_... |
| immediately | boolean | no | true = end now; omitted or false = end at the period end |
| confirm | boolean | no | true after the user approves this cancellation |
Refund a Stripe charge (ch_… or a pi_… payment intent): the full refundable amount, or a partial amount in major units; optional reason duplicate / fraudulent / requested_by_customer. ALWAYS needs confirm:true, test keys included. Read back. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Parameter | Type | Required | Notes |
|---|---|---|---|
| chargeId | string | yes | charge id, ch_..., or payment intent id, pi_... |
| amount | number | no | partial refund in major units; omit to refund the full refundable amount |
| reason | string | no | duplicate, fraudulent or requested_by_customer |
| confirm | boolean | no | true after the user approves this refund |
Promotion codes on the brand's Stripe account — the codes customers TYPE at checkout — with the coupon each applies, active or not, times redeemed / max, expiry, first-purchase-only and minimum-order rules. Filter by active, code or couponId. Read-only, free. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Parameter | Type | Required | Notes |
|---|---|---|---|
| active | boolean | no | true = active codes only; false = inactive codes only |
| code | string | no | customer-typable promotion code text to find |
| couponId | string | no | only codes that apply this coupon, from list_stripe_coupons |
| limit | number | no | rows to return, 1 to 100; default 25 |
| after | string | no | pagination cursor: the last promotion code id from the previous page |
Create a customer-typable promotion code (e.g. SUMMER20) for an existing Stripe coupon — the thing a "use code X" ad needs, since a coupon alone has no code. couponId (from list_stripe_coupons); optional code (letters, digits and dashes; Stripe generates one if omitted — a code with spaces or symbols is refused, never altered), maxRedemptions, expiresAt (YYYY-MM-DD), firstTimeOnly, minimumAmount +
| Parameter | Type | Required | Notes |
|---|---|---|---|
| couponId | string | yes | coupon id to apply, from list_stripe_coupons |
| code | string | no | customer-typable code, letters, digits and dashes; Stripe generates one if omitted |
| maxRedemptions | number | no | maximum uses of this code |
| expiresAt | string | no | last day the code works, YYYY-MM-DD |
| firstTimeOnly | boolean | no | true = valid only on a customer's first purchase |
| minimumAmount | number | no | MAJOR units |
| minimumAmountCurrency | string | no | 3-letter currency code for minimumAmount |
| customerId | string | no | restrict the code to one customer (cus_…) |
| metadata | object | no | key/value metadata stored on the code |
| confirm | boolean | no | true on a live key, after the user approves the preview |
Switch a Stripe promotion code OFF (active:false) or back on (active:true), or change its metadata. Pass promotionCodeId (promo_…) or the code itself. Switching a code off ALWAYS needs confirm:true, test keys included, because anyone holding it from an ad is then refused at checkout. Read back; a code Stripe will not reactivate (its coupon is no longer valid) is reported, not claimed. Needs Stripe
| Parameter | Type | Required | Notes |
|---|---|---|---|
| promotionCodeId | string | no | promotion code id, promo_...; give this or code |
| code | string | no | the customer-typable code text, if promotionCodeId is not given |
| active | boolean | no | false switches the code off; true switches it back on |
| metadata | object | no | key/value metadata to set on the code |
| confirm | boolean | no | true to switch a code off, as well as to confirm a live-key write |
Payment links on the brand's Stripe account: URL, active or not, whether the promotion-code box shows, what happens after payment, and their line items (price, amount). Filter active true/false. Read-only, free. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Parameter | Type | Required | Notes |
|---|---|---|---|
| active | boolean | no | true = active links only; false = inactive links only |
| limit | number | no | rows to return, 1 to 100; default 25 |
| after | string | no | pagination cursor: the last payment link id from the previous page |
Change a Stripe payment link (plink_…). active:false DEACTIVATES it — its URL then shows a "no longer available" page, in every ad that points at it — and ALWAYS needs confirm:true, test keys included; active:true reactivates it. allowPromotionCodes shows or hides the promotion-code box; afterCompletionUrl (redirect) or afterCompletionMessage (Stripe's confirmation page); inactiveMessage (up to 50
| Parameter | Type | Required | Notes |
|---|---|---|---|
| paymentLinkId | string | yes | payment link id, plink_..., from list_stripe_payment_links |
| active | boolean | no | false deactivates the link; true reactivates it |
| allowPromotionCodes | boolean | no | true shows the promotion-code box at checkout; false hides it |
| afterCompletionUrl | string | no | URL the buyer is redirected to after paying |
| afterCompletionMessage | string | no | message shown on Stripe's confirmation page after paying |
| inactiveMessage | string | no | message shown while the link is off, up to 500 characters |
| metadata | object | no | key/value metadata to set on the link |
| confirm | boolean | no | true to deactivate the link, and to confirm a live-key write |
How much money the brand's Stripe PAYMENT LINKS made — the "what did the link in this ad earn" question. Paid purchases and revenue (MAJOR units, per currency) from completed checkouts between since and until (YYYY-MM-DD, default the last 30 days), for one paymentLinkId or every link. groupBy: link (default without an id — which link earned most), reference (default with an id — per ad, from the r
| Parameter | Type | Required | Notes |
|---|---|---|---|
| paymentLinkId | string | no | one payment link, plink_...; omit for every link |
| since | string | no | start date, YYYY-MM-DD; default 30 days before until |
| until | string | no | end date, YYYY-MM-DD; default today |
| groupBy | string | no | link, reference, promotion_code or day; default link without paymentLinkId, reference with it |
Contacts from the brand's OWN HubSpot CRM, newest first — email, name, lifecycle stage, lead status, created date, original source — or a search by email / first name / last name (whole-token match, wildcards allowed). Read-only, free. Needs HubSpot connected (Settings > Connectors > HubSpot).
| Parameter | Type | Required | Notes |
|---|---|---|---|
| search | string | no | email or name to search for |
| limit | number | no | 1–100, default 25 |
| after | string | no | pagination cursor from the previous page |
Create a contact in the brand's HubSpot by email, or UPDATE the contact that already has that email — HubSpot's duplicate answer carries the existing id, so this is an UPSERT and never makes a twin. Optional firstname, lastname, phone, company, website, lifecyclestage, hs_lead_status, hubspotOwnerId (the sales rep to assign it to — an owner id from list_hubspot_owners), plus any other writable con
| Parameter | Type | Required | Notes |
|---|---|---|---|
| string | yes | the contact email — the key HubSpot de-duplicates on | |
| firstname | string | no | contact first name |
| lastname | string | no | contact last name |
| phone | string | no | contact phone number |
| company | string | no | company name on the contact |
| website | string | no | company website URL |
| lifecyclestage | string | no | HubSpot lifecycle stage internal value, e.g. lead or customer |
| hs_lead_status | string | no | HubSpot lead status internal value, e.g. NEW |
| hubspotOwnerId | string | no | HubSpot owner id (digits) from list_hubspot_owners |
| ownerId | string | no | older name for hubspotOwnerId (MCP only) |
| properties | object | no | any other writable contact properties, by internal name |
Push the leads the brand's LEAD FORMS collected into HubSpot as contacts — Meta, LinkedIn, TikTok (every published Instant Form, every lead region), X Ads, Snapchat and ChatGPT Ads (the last two: leads delivered to Hermoso by their lead webhooks). Each lead is upserted by email with lead status NEW and the platform + form + campaign written into the contact's Message property (a default HubSpot pr
| Parameter | Type | Required | Notes |
|---|---|---|---|
| since | string | no | ISO date; default 7 days ago |
| platforms | array | no | default every connected one |
| tiktokRegions | array | no | TikTok stores leads per region; default all three |
| dryRun | boolean | no | list what would be synced and write nothing |
Deals from the brand's HubSpot created in the window (default the last 90 days): count and amount by stage (labels resolved through the deal pipelines, closed-won recognised from the stage's own metadata), by original source, BY CAMPAIGN (source -> platform -> campaign from HubSpot's Original Source drill-downs, so PAID_SOCIAL splits into the actual campaigns), BY OWNER (the sales rep holding each
| Parameter | Type | Required | Notes |
|---|---|---|---|
| since | string | no | YYYY-MM-DD, default 90 days ago |
| until | string | no | YYYY-MM-DD, default today |
COMPANIES (accounts) from the brand's OWN HubSpot CRM, newest first: name, domain, website, LinkedIn company page, industry, employees, annual revenue, lifecycle stage, lead status, country/city, owner (rep name), original source + drill-downs, associated contacts and deals, total revenue — plus a de-duplicated domains list, which is what LinkedIn company targeting and account-based (ABM) audien
| Parameter | Type | Required | Notes |
|---|---|---|---|
| search | string | no | name, domain, website or phone |
| lifecycleStage | string | no | internal value, e.g. customer, opportunity, salesqualifiedlead, lead |
| industry | string | no | HubSpot's internal industry value, e.g. COMPUTER_SOFTWARE |
| hubspotOwnerId | string | no | owner id from list_hubspot_owners |
| ownerId | string | no | older name for hubspotOwnerId (MCP only) |
| createdSince | string | no | YYYY-MM-DD |
| limit | number | no | 1–100, default 25 |
| after | string | no | pagination cursor from the previous page |
The sales reps (OWNERS) in the brand's HubSpot: id, name, email, teams. An owner's id is what create_hubspot_contact's hubspotOwnerId assigns a lead to, and what list_hubspot_companies filters by. search narrows by name or email; archived:true lists DEACTIVATED owners instead. Read-only, free. Needs HubSpot connected.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| search | string | no | name or email contains |
| archived | boolean | no | true = deactivated owners only |
Whether the brand's lead-form leads are pushed into HubSpot AUTOMATICALLY: on or off, how often (everyMinutes), which platforms, when it runs next, and what the last run did — counts per platform, or why it did not run (HubSpot disconnected, a platform not readable). Read-only, free. Needs HubSpot connected.
No parameter schema on file.
Switch the AUTOMATIC HubSpot lead sync on or off for this brand: enabled, everyMinutes (a whole number from 15 to 1440; default 60 — anything outside that is refused, not clamped), optional platforms (meta, linkedin, tiktok, x, snapchat, chatgpt, reddit; default every connected one). Each run is sync_leads_to_hubspot reading from the last successful run, so a new lead reaches the CRM within minute
| Parameter | Type | Required | Notes |
|---|---|---|---|
| enabled | boolean | yes | true turns the automatic lead sync on; false turns it off |
| everyMinutes | number | no | 15–1440, default 60 |
| platforms | array | no | omit for every connected one |
| runNow | boolean | no | true = run the first sync immediately |
Read the brand’s connected Microsoft Advertising (Bing Ads) account(s). Call with NO adAccountId to list the accounts shared with this profile — do this first to pick a target. Call WITH adAccountId to list that account’s campaigns (id, name, status, daily budget, campaign type, and whether the budget is SHARED). Microsoft statuses are Active / Paused — never Google’s ENABLED — and Microsoft also
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
Performance for a Microsoft Advertising account — impressions, clicks, CTR, average CPC, spend, conversions. reportType picks WHICH report, and that is the whole Microsoft reporting surface, not just campaigns: AdGroupPerformance, AdPerformance, KeywordPerformance, SearchQueryPerformance (the actual search terms people typed), GeographicPerformance, UserLocationPerformance, AgeGenderAudience and
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| timePeriod | string | no | predefined Microsoft window, default Last30Days — must be one of the values in the description; anything else is rejected |
| since | string | no | YYYY-MM-DD custom range start (with until) |
| until | string | no | YYYY-MM-DD custom range end |
| columns | array | no | report columns — defaults to campaign performance. Each report type accepts only its OWN column set; Microsoft also refuses impression-share columns alongside BidMatchType / BudgetName / DeviceOS / Go |
| reportType | string | no | which report — default CampaignPerformanceReportRequest. An unknown name is refused with the full list. |
| aggregation | string | no | default Summary. Hourly accepts only Today/Yesterday or a custom range. |
| reportRequestId | string | no | pick up a report that came back pending — pass it back and this RESUMES that exact report instead of submitting a new one |
Microsoft Advertising’s KEYWORD PLANNER — real monthly search volume, competition, suggested bid and ad impression share, expanded from seed keywords, a landing-page URL to mine, or a category. Run it BEFORE choosing keywords for a Microsoft campaign. locationIds is REQUIRED and deliberately not defaulted: a search volume with no market attached is a number nobody can act on, and inventing a count
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| keywords | array | no | seed terms to expand from |
| url | string | no | a landing page for Microsoft to mine ideas from |
| categoryId | number | no | Microsoft keyword category id to mine ideas from |
| locationIds | array | yes | REQUIRED — Microsoft location ids (microsoft_ads_geo_search resolves names to ids, free) |
| language | string | no | default English |
| network | string | no | search network the volume is measured on: Microsoft-owned, partner-syndicated or both |
| competition | array | no | filter to Low | Medium | High |
| minSearchVolume | number | no | only ideas with at least this many monthly searches |
| maxSearchVolume | number | no | only ideas with at most this many monthly searches |
| attributes | array | no | which idea attributes to return — omit for all |
| expandIdeas | boolean | no | false = do not expand; then keywords[] is mandatory |
What a set of keywords would DELIVER on Microsoft Advertising at a given bid — estimated impressions, clicks, CTR, average CPC, average position and total cost. maxCpc is REQUIRED because a traffic estimate IS a function of the bid; estimating without one would be inventing the input. locationIds is REQUIRED for the same reason a search volume needs a market. SAY THIS WHEN REPORTING: Microsoft ret
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| keywords | array | yes | the keywords to estimate |
| maxCpc | number | yes | REQUIRED — the max CPC bid to estimate at, in the account currency |
| matchType | string | no | default Exact |
| locationIds | array | yes | REQUIRED — Microsoft location ids |
| language | string | no | language to estimate for, e.g. English |
| network | string | no | search network to estimate for; same values as microsoft_ads_keyword_ideas network |
| dailyBudget | number | no | daily budget in account currency the estimate assumes |
Where a Microsoft Advertising campaign is BUDGET-CONSTRAINED — Microsoft’s own recommended budget against the current one, the estimated WEEKLY click and impression gain from raising it, and a budget/return curve. Omit campaignId for the whole account. SAY THIS WHEN REPORTING: these are Microsoft’s FORECASTS, never measurements — a projected increase has not happened — and acting on one spends rea
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| campaignId | string | no | omit for the whole account |
Resolve country / region / city names to the Microsoft Advertising location ids that create_microsoft_ads_campaign needs. Read-only, free, 0 credits. Use it when a location ask is ambiguous ("Springfield") — this returns EVERY candidate with its id so the USER can pick, and you never guess between two places. Accepts names, ISO country codes ("CA"), or numeric location ids. Pass query as ONE ask
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| query | yes | ONE location ask as a plain string, or several as an array of strings — names, ISO country codes, or numeric Microsoft location ids. A comma belongs to a name ("Seattle, Washington, United States") an |
Build a campaign on a connected Microsoft Advertising (Bing Ads) account. ALWAYS created Paused — it spends NOTHING until you activate it with set_microsoft_ads_status(confirm:true). Microsoft’s object graph is campaign -> ad group -> responsive search ad -> keywords, so a campaign ON ITS OWN CANNOT SERVE AN IMPRESSION: pass adGroup{name, ad{headlines,descriptions,finalUrls}, keywords[]} and this
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| name | string | yes | campaign name, ≤128 characters |
| dailyBudget | number | yes | daily budget in the account’s currency |
| budgetType | string | no | default DailyBudgetStandard; Accelerated is Audience-campaign only |
| campaignType | string | no | Search (default), Shopping, DynamicSearchAds, Audience or PerformanceMax. Search (and DynamicSearchAds, which Microsoft now builds AS a Search campaign) take the full tree below; Shopping, Audience an |
| storeId | string | no | Shopping (required) or retail Performance Max (optional): the Microsoft Merchant Center store id. Checked against the account’s own stores before anything is created. |
| shoppingPriority | integer | no | Shopping only: 0 (Low, the default), 1 or 2 — decides which campaign serves when several share a store. Performance Max is always 4, set for you. |
| salesCountryCode | string | no | Shopping: two-letter country to filter the store catalog to, e.g. "US". Not with feedLabel. |
| feedLabel | string | no | Shopping: advertise every product with this feed label (≤20 characters). Not with salesCountryCode. |
| localInventoryAds | boolean | no | Shopping: enable local inventory ads (only where Microsoft has enabled the feature for the account) |
| dynamicSearchAds | object | no | Dynamic search ads: required for campaignType DynamicSearchAds, optional on Search. Microsoft retired the DynamicSearchAds TYPE, so the campaign is a Search campaign carrying this setting. |
| biddingScheme | string | no | bid strategy. Search takes any; Audience EnhancedCpc/MaxConversions; Shopping EnhancedCpc; Performance Max MaxConversions/MaxConversionValue. TargetCpa/TargetRoas are sent as MaxConversions/MaxConvers |
| maxCpc | number | no | bid cap for MaxClicks / MaxConversions / MaxConversionValue / TargetImpressionShare (not Performance Max) |
| targetImpressionShare | number | no | 1–100, with biddingScheme TargetImpressionShare |
| targetAdPosition | string | no | with TargetImpressionShare; MainLine1 = the very top |
| targetCpa | number | no | Performance Max with MaxConversions: target cost per acquisition, account currency |
| targetRoas | number | no | Performance Max with MaxConversionValue: target return on ad spend as a ratio, e.g. 4 = 400% |
| finalUrlExpansionOptOut | boolean | no | Performance Max: true = serve only the final URLs you give; default false lets Microsoft use the whole domain |
| timeZone | string | no | Microsoft time-zone enum (e.g. EasternTimeUSCanada); default the ad account’s own. Microsoft marks a campaign time zone deprecated and may keep its own — the reply states the zone it stored |
Add an ad group to an existing Microsoft Advertising campaign — optionally with its responsive search ad and keywords in the same call. Created Paused by default. If the parent campaign is already LIVE (Active), creating this ad group Active starts REAL AD SPEND on the next auction, exactly like activating it — show the user what would begin serving, get an explicit yes, then pass confirm:true. Le
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| campaignId | string | yes | the campaign this ad group belongs to |
| name | string | yes | ad group name, ≤256 characters |
| status | string | no | default Paused |
| cpcBid | number | no | default max CPC in the account currency |
| language | string | no | required if the campaign has no language set |
| ad | object | no | create the responsive search ad in the same call |
| keywords | array | no | keywords to add: [{text, matchType, bid, status}]; status defaults to Paused |
| confirm | boolean | no | REQUIRED true to create this Active under a LIVE campaign |
Create the actual AD inside a Microsoft Advertising ad group — this is the object that carries the creative; a campaign or ad group alone shows nothing. It builds a RESPONSIVE SEARCH AD: 3–15 headlines (≤30 chars), 2–4 descriptions (≤90 chars), at least one finalUrl, optional path1/path2. Expanded text ads CANNOT be created any more — Microsoft rejects them outright. Created Paused; if the parent
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| adGroupId | string | yes | the ad group this ad lives in |
| headlines | array | no | 3–15 headlines, each ≤30 characters |
| descriptions | array | no | 2–4 descriptions, each ≤90 characters |
| finalUrls | array | no | the landing page(s) — at least one is required |
| path1 | string | no | display-URL path segment, ≤15 chars, no "/" |
| path2 | string | no | second display-URL path segment (only with path1) |
| status | string | no | default Paused |
| confirm | boolean | no | REQUIRED true to create this Active in a LIVE ad group |
Add keywords to a Microsoft Advertising ad group. Match types are Exact, Phrase and Broad — Microsoft has no broad-match-modifier. Keywords are added Paused unless you set status:"Active"; an Active keyword on a live ad group makes the campaign bid on a new term immediately, so that needs confirm:true. Note that per-keyword bids are honoured but ad-group / keyword BID STRATEGIES are silently ignor
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| adGroupId | string | yes | the ad group to add them to |
| keywords | array | yes | the keywords |
| confirm | boolean | no | REQUIRED true to add an Active keyword to a LIVE ad group |
Find the LinkedIn profile ids Microsoft Advertising can target: a company (CompanyName), an Industry or a JobFunction. A company is found with Microsoft’s own company search (3+ characters); industries and job functions are matched against the profile list Microsoft publishes for them. Microsoft publishes no lookup for JobTitle or JobSeniority ids, so those cannot be searched here; an id you alrea
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| profileType | string | yes | what to search: CompanyName, Industry or JobFunction |
| query | string | yes | part of the name to find (3+ characters for a company) |
| languageLocale | string | no | en (default), fr, de, it, pt-BR, es or zh-Hant |
| limit | number | no | 1-100, default 25 |
Read the LinkedIn profile targeting on a Microsoft Advertising campaign: every company, industry, job function, seniority and job title criterion with its bid adjustment and criterion id, read from Microsoft. A profile type Microsoft would not read is named as unread, never reported as none. Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| campaignId | string | yes | Microsoft campaign id to read LinkedIn profile targeting from |
Target LinkedIn profiles on a Microsoft Advertising Search, Shopping or Dynamic Search Ads campaign: add companies, industries, job functions, seniorities or job titles, each with an optional bid adjustment (whole percent, -90 to 900), or change an existing adjustment by criterion id (list_microsoft_ads_profile_targeting shows it). Find profile ids with search_microsoft_ads_profiles. On an ACTIVE
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| campaignId | string | yes | Microsoft campaign id to change LinkedIn profile targeting on |
| add | array | no | profile criteria to add: [{profileType, profileId, bidAdjustment}] |
| update | array | no | bid adjustments to change on existing criteria: [{criterionId, bidAdjustment}] |
| confirm | boolean | no | REQUIRED true when the campaign is ACTIVE |
Change a Microsoft Advertising campaign’s DAILY BUDGET (in the account’s currency). Raising it on a LIVE (Active) campaign increases real spend immediately — you MUST show the user the new daily amount, get an explicit yes, then call with confirm:true. If the campaign is on a SHARED budget its amount is read-only here and this refuses with an explanation rather than pretending to change it. Read b
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| campaignId | string | yes | the campaign whose budget changes |
| dailyBudget | number | yes | new daily budget in the account’s currency |
| confirm | boolean | no | REQUIRED true to change the budget of a LIVE campaign |
Turn a Microsoft Advertising campaign, AD GROUP or AD on (Active) or off (Paused). Pass level:"campaign" + campaignId, level:"adGroup" + adGroupId, or level:"ad" + BOTH adGroupId and adId. ACTIVATING STARTS REAL AD SPEND — you MUST first show the user the campaign name + its daily budget, get an explicit yes, then call with status:"Active" and confirm:true. Pausing is always safe. Microsoft has on
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| level | string | no | what to change — default campaign |
| campaignId | string | no | campaign id (level:"campaign") |
| adGroupId | string | no | ad group id (level:"adGroup", or with adId for level:"ad") |
| adId | string | no | ad id (level:"ad" — pass adGroupId too) |
| status | string | yes | Active = start spending; Paused = stop |
| confirm | boolean | no | REQUIRED true to set Active (real spend) |
PERMANENTLY delete a Microsoft Advertising campaign, ad group, ad or keyword. This is a real delete — Microsoft removes the object and it stops being returned by every read, with no undelete and no documented recovery window. Pass level:"campaign" + campaignId, level:"adGroup" + adGroupId, level:"ad" + adGroupId AND adId, or level:"keyword" + adGroupId AND keywordId. CALL IT WITHOUT confirm FIRST:
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| level | string | no | what to delete — default campaign |
| campaignId | string | no | campaign id (level:"campaign"; also the parent for level:"adGroup" if you know it) |
| adGroupId | string | no | ad group id (level:"adGroup"; REQUIRED as the parent for level:"ad" and level:"keyword") |
| adId | string | no | ad id (level:"ad" — pass adGroupId too) |
| keywordId | string | no | keyword id (level:"keyword" — pass adGroupId too) |
| confirm | boolean | no | REQUIRED true — the delete is permanent |
| confirmName | string | no | the object’s EXACT name, required when it has children / is Active / has spent |
| confirmChildren | number | no | the exact number of children reported by the unconfirmed call, required when it has any |
List the brand's MICROSOFT MERCHANT CENTER stores — the product-feed side of Microsoft Advertising, the exact twin of Google Merchant Center. THIS IS WHERE THE merchantId COMES FROM: every other Microsoft Merchant tool needs it and there is no other way to discover it (Content API's own Store resource is closed-beta only, so Hermoso reads the list from Campaign Management instead, which is open to
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
Read the product offers in a Microsoft Merchant Center store — the feed a Microsoft Shopping campaign serves from. Pass productId to fetch ONE offer, or omit it to page the store (limit up to 250, then pass the returned pageToken). MICROSOFT'S PRODUCT IDS ARE COMPOSED AND CASE SENSITIVE: the id is channel:contentLanguage:targetCountry:offerId (e.g. Online:en:US:Sku123), not the bare offerId, and a
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| merchantId | string | no | Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store |
| productId | string | no | fully qualified id (channel:contentLanguage:targetCountry:offerId, e.g. Online:en:US:Sku123) to fetch ONE offer |
| limit | number | no | up to 250, default 25 |
| pageToken | string | no | the nextPageToken from a previous call |
Add or update product offers in a Microsoft Merchant Center store. THE ONE THING TO KNOW BEFORE CALLING IT: Microsoft has NO partial update — 'because an update is an insert operation, you must include all fields of the offer in the request' — so a title-only 'update' CLEARS every other field. Always send the whole offer. Ten fields are required: availability, channel, condition, contentLanguage,
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| merchantId | string | no | Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store |
| product | object | no | ONE whole offer — availability, channel, condition, contentLanguage, imageLink, link, offerId, price{value,currency}, targetCountry, title are required; brand/gtin/mpn strongly recommended |
| products | array | no | several whole offers in one batch, maximum 300 |
| catalogId | string | no | write into a specific catalog instead of the store default |
| dryRun | boolean | no | validate against Microsoft and write nothing — Microsoft returns no ids for a dry run, so none is claimed |
Delete one product offer from a Microsoft Merchant Center store. Pass productId as the FULLY QUALIFIED id (channel:contentLanguage:targetCountry:offerId), not the offerId. Call without confirm first — nothing is deleted and you get Microsoft's own better advice back: deleted products can take UP TO 12 HOURS to stop delivering, so if the goal is to stop showing it today, upsert it with availability
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| merchantId | string | no | Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store |
| productId | string | yes | the FULLY QUALIFIED id (channel:contentLanguage:targetCountry:offerId), not the offerId |
| confirm | boolean | no | REQUIRED true — call without it first to see Microsoft’s own advice about the 12-hour delivery tail |
Why a Microsoft Merchant Center store's products are NOT serving — the approved / disapproved / expiring counts plus the per-offer disapprovals and warnings with Microsoft's own issue codes. This is the tool to reach for when a Microsoft Shopping campaign is live and showing nothing. TWO CAVEATS THAT LOOK LIKE FAILURES AND ARE NOT, both repeated in the note: Microsoft returns detail rows ONLY for
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| merchantId | string | no | Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store |
| limit | number | no | up to 250, default 25 |
| pageToken | string | no | nextPageToken from a previous result, to get the next page |
List the catalogs inside a Microsoft Merchant Center store. Catalogs logically group products, and a Shopping campaign's product scope points at them. Each row says whether publishing is ENABLED — products in a catalog with publishing off do not serve at all, which is a common and invisible reason a feed appears healthy and delivers nothing. Products go into the store's default catalog unless a ca
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| merchantId | string | no | Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store |
Create, update or delete a catalog in a Microsoft Merchant Center store. Creating needs name + market + isPublishingEnabled; updating needs catalogId + name + isPublishingEnabled (market is not settable after creation). isPublishingEnabled is NEVER defaulted on — products serve only when it is true, so publishing a catalogue is always an explicit choice. Names must be unique within the store and a
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| merchantId | string | no | Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store |
| catalogId | string | no | set to UPDATE or DELETE an existing catalog; omit to create a new one |
| name | string | no | unique within the store, maximum 70 characters |
| market | string | no | REQUIRED when creating — where the products are served, e.g. en-US. Not settable afterwards |
| isPublishingEnabled | boolean | no | products serve ONLY when this is true — never defaulted on |
| deleteIt | boolean | no | true to delete the catalog named by catalogId |
| confirm | boolean | no | REQUIRED true to delete |
WHY a Microsoft Advertising ad or keyword was DISAPPROVED: Microsoft's editorial reason codes, the word or phrase it objected to (term), where it appeared (location), the countries it applies to, and any appeal status, read live from Microsoft. Use it whenever a read-back shows editorialStatus Disapproved, or the user asks why a Bing ad is not running. Pass adGroupId plus adIds OR keywordIds (Micr
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| adGroupId | string | yes | the ad group the ads or keywords sit in |
| adIds | array | no | the ads to explain |
| keywordIds | array | no | or the keywords to explain (not both) |
The NEGATIVE keywords (excluded searches) on a Microsoft Advertising campaign (campaignId) or ad group (adGroupId) with their ids and match types; with no id, the account’s SHARED negative keyword lists; with listId, the keywords in one list. Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| campaignId | string | no | campaign whose negative keywords to list |
| adGroupId | string | no | ad group whose negative keywords to list |
| listId | string | no | a shared negative keyword list |
EXCLUDE searches from a Microsoft Advertising campaign or ad group with negative keywords — Exact or Phrase only (Microsoft has no broad negative; default Phrase, which blocks every search containing the phrase), up to 20,000 per campaign or ad group. Broad-match keywords without negatives are the classic way to waste money: “stop paying for free” is this tool. It can only narrow spend, so it is n
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| campaignId | string | no | campaign to add negatives to; pass this or adGroupId |
| adGroupId | string | no | pass this OR campaignId |
| keywords | array | yes | negative keywords, each with text and matchType (Exact or Phrase) |
Remove negative keywords (by id, from list_microsoft_ads_negative_keywords) from a Microsoft Advertising campaign or ad group. On a LIVE campaign that lets those searches trigger paid ads again, so it needs confirm:true after the user agrees. Read back. Free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| campaignId | string | no | campaign to remove negatives from; pass this or adGroupId |
| adGroupId | string | no | ad group to remove negatives from; pass this or campaignId |
| negativeKeywordIds | array | yes | ids of the negative keywords to remove, from list_microsoft_ads_negative_keywords |
| confirm | boolean | no | REQUIRED true on a LIVE campaign |
A SHARED negative keyword list on Microsoft Advertising — one set of exclusions reused across up to 20 campaigns. action: create (name + keywords; campaignIds attaches it straight away), add (keywords), remove (negativeKeywordIds), attach / detach (campaignIds), delete (permanent, confirm:true). Removing, detaching or deleting on a LIVE campaign lets those searches trigger paid ads again, so it ne
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| action | string | yes | create, add, remove, attach, detach or delete a shared negative keyword list |
| listId | string | no | every action but create |
| name | string | no | create: ≤255 characters |
| keywords | array | no | keywords for create or add: [{text, matchType}] |
| negativeKeywordIds | array | no | keyword ids to remove from the list (action remove) |
| campaignIds | array | no | campaigns to attach to or detach from the list (action attach or detach) |
| confirm | boolean | no | true to delete the list, or to remove, detach or delete on a LIVE campaign |
Change a Microsoft Advertising campaign’s BID STRATEGY, read back: EnhancedCpc, MaxClicks (maxCpc cap), MaxConversions (optional targetCpa), MaxConversionValue (optional targetRoas), TargetImpressionShare (targetImpressionShare + targetAdPosition). Search takes any; Audience EnhancedCpc / MaxConversions; Shopping EnhancedCpc; Performance Max MaxConversions / MaxConversionValue. TargetCpa and Targe
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| campaignId | string | yes | Microsoft campaign id whose bid strategy changes |
| biddingScheme | string | yes | new bid strategy; TargetCpa and TargetRoas are sent as MaxConversions and MaxConversionValue |
| maxCpc | number | no | bid cap in account currency, for MaxClicks and MaxConversions |
| targetCpa | number | no | target cost per conversion in account currency, for MaxConversions |
| targetRoas | number | no | ratio, e.g. 4 = 400% |
| targetImpressionShare | number | no | 1–100 |
| targetAdPosition | string | no | FirstPage, MainLine or MainLine1 (MainLine1 = very top), with TargetImpressionShare |
| confirm | boolean | no | REQUIRED true on a LIVE campaign |
The UET tags (Microsoft’s site tracking pixel) on this Microsoft Advertising customer: id, name, status (Active / Inactive / Unverified) and each tag’s install snippet. Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| tagIds | array | no | only these UET tag ids; omit for all tags |
Create a UET tag (Microsoft’s site tracking pixel) and get its install snippet, read back. Url / Event / Duration / PagesViewedPerVisit conversion goals, remarketing lists and conversion bidding all need one. Name ≤100 characters, unique per customer. Free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| name | string | yes | tag name, up to 100 characters, unique per customer |
| description | string | no | optional note on what the tag tracks |
Create a Microsoft Advertising CONVERSION GOAL, read back. type: Url (a thank-you page: urlExpression + urlOperator), Event (a UET custom event: eventAction / eventCategory / eventLabel / eventValue), OfflineConversion (conversions uploaded from a CRM with send_microsoft_ads_offline_conversions — no tag; Microsoft asks for 2 hours before the first upload), Duration (minimumDurationSeconds) or Page
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| name | string | yes | ≤100 characters, unique per customer |
| type | string | yes | Url (thank-you page), Event (UET custom event), OfflineConversion, Duration or PagesViewedPerVisit |
| tagId | string | no | UET tag id from create_microsoft_ads_uet_tag; not needed for OfflineConversion |
| category | string | no | conversion category, e.g. Purchase or Signup; required for Url, Event and OfflineConversion |
| urlExpression | string | no | Url goals: the thank-you page URL or path to match |
| urlOperator | string | no | Url goals: how urlExpression matches the page: Equals, BeginsWith, Contains or RegularExpression |
| eventAction | string | no | Event goals: UET custom event action |
| eventCategory | string | no | Event goals: UET custom event category |
| eventLabel | string | no | Event goals: UET custom event label |
| eventValue | number | no | Event goals: numeric value attached to each event |
| minimumDurationSeconds | number | no | Duration goals: visit length in seconds that counts as a conversion |
| minimumPagesViewed | number | no | PagesViewedPerVisit goals: page count that counts as a conversion |
| revenueType | string | no | FixedValue (one value per conversion), VariableValue or NoValue |
| revenue | number | no | FixedValue: the value of one conversion; VariableValue: the default |
| currencyCode | string | no | 3-letter ISO currency code for the revenue value, e.g. USD |
| countType | string | no | All or Unique: how repeat conversions are counted |
| conversionWindowDays | number | no | 1–90 |
| scope | string | no | Account = this ad account only; Customer = shared across the customer |
The audiences (customer lists, remarketing, in-market, custom, similar, combined, product) targeted on — or EXCLUDED from — a Microsoft Advertising campaign or ad group, with each one’s bid adjustment and criterion id. Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| campaignId | string | no | campaign whose audience targeting to list; pass this or adGroupId |
| adGroupId | string | no | ad group whose audience targeting to list; pass this or campaignId |
Put an audience to work on a Microsoft Advertising campaign or ad group — e.g. the customer list from create_microsoft_ads_customer_list, to retarget it or EXCLUDE existing customers. action add (audienceId, exclude, bidAdjustment -90..900 %) or remove (criterionIds from list_microsoft_ads_audience_targeting). Microsoft allows biddable audiences at the campaign OR the ad-group level of one campaig
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| campaignId | string | no | campaign to target or exclude the audience on; pass this or adGroupId |
| adGroupId | string | no | ad group to target or exclude the audience on; pass this or campaignId |
| action | string | no | default add |
| audienceId | string | no | list_microsoft_ads_audiences |
| exclude | boolean | no | true = exclude this audience instead of targeting it |
| bidAdjustment | number | no | -90 to 900 (%); 0 = target without changing the bid |
| criterionIds | array | no | criterion ids to remove (action remove), from list_microsoft_ads_audience_targeting |
| confirm | boolean | no | true required on a LIVE campaign when targeting or removing |
Turn a Hermoso render, or the user’s own picture (any https URL, or a file via upload_file), into a Microsoft Advertising IMAGE ad: a responsive ad on the Microsoft Audience Network (an Audience campaign) or a multimedia ad on Search. One imageUrl is centre-cropped to the 1.91:1 and 1:1 pictures Microsoft requires (or pass images{landscape, square, logo}), uploaded, and the ad is built PAUSED and
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| adGroupId | string | yes | ad group to place the image ad in |
| businessName | string | yes | business name shown on the ad, up to 25 characters |
| headlines | array | yes | 1-15 headlines, up to 30 characters each |
| longHeadlines | array | yes | 1-5 long headlines, up to 90 characters each |
| descriptions | array | yes | 1-5 descriptions, up to 90 characters each |
| finalUrls | array | yes | landing page URL(s) the ad sends people to |
| imageUrl | string | no | one picture, cut to both shapes for you |
| images | object | no | pictures by shape: {landscape, square, logo} URLs, instead of imageUrl |
| logoUrl | string | no | logo picture URL |
| callToAction | string | no | Search multimedia ads only, e.g. LearnMore, ShopNow, SignUp |
| videoIds | array | no | video ids from upload_microsoft_ads_video to attach |
| videoSubType | string | no | aspect ratio of the attached video: 16x9, 5x4, 1x1, 4x5 or 9x16 |
| status | string | no | default Paused |
| confirm | boolean | no | true required to create the ad Active in a live ad group |
Upload videos (a Hermoso render, or the user’s own clip: any direct-download https file or an upload_file URL) to Microsoft Advertising, or check ones already uploaded with videoIds: Draft (still processing), Active, or Failed with Microsoft’s reason. Microsoft enables video per account and refuses clips under 6 seconds. Use the ids in create_microsoft_ads_image_ad(videoIds). Free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| urls | array | no | direct-download https URLs of videos to upload; clips under 6 seconds are refused |
| videoIds | array | no | existing video ids to check the status of, instead of uploading |
| description | string | no | description stored with the uploaded video |
List a Microsoft Advertising account's audiences — by default the CUSTOMER LISTS (Customer Match), with each row's membership duration and its current Search and Audience-network sizes. TWO MICROSOFT CAVEATS THAT LOOK LIKE FAILURES AND ARE NOT: a size is nil or empty for UP TO 48 HOURS while a list is being built, and Microsoft will not use an audience of fewer than 300 people at all — so never re
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| audienceIds | array | no | up to 100 specific ids — omit for all of the requested type |
| types | array | no | audience types, default CustomerList |
Create an EMPTY Customer Match list on a Microsoft Advertising account, ready for apply_microsoft_ads_customer_list to fill. membershipDuration is 1–390 days, or -1 for no expiration (Microsoft's default is 30). scope 'Account' (the default) makes it usable only by that ad account; scope 'Customer' makes it usable by every account under the manager — Hermoso derives the right parent id for whichev
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| name | string | yes | maximum 128 characters |
| description | string | no | maximum 1024 characters |
| membershipDuration | number | no | 1–390 days, or -1 for no expiration. Microsoft’s default is 30 |
| scope | string | no | Account (default) = this ad account only; Customer = every account under the manager |
Upload customer emails into a Microsoft Advertising Customer Match list so campaigns can target or exclude them. YOU PASS PLAIN EMAIL ADDRESSES — Hermoso normalizes and SHA-256 hashes them locally and sends ONLY the digests, so no plaintext ever leaves the server; values that arrive already hashed (64 hex characters) are passed through untouched. Microsoft's own normalization is applied exactly as
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| audienceId | string | yes | the customer list to write into |
| emails | array | no | PLAIN email addresses — normalized and SHA-256 hashed inside Hermoso, never sent as plaintext. Already-hashed 64-hex values pass through untouched. Maximum 1000 per call |
| action | string | no | Add (default) is additive across calls |
| subType | string | no | Email is the only identifier Microsoft supports here — it publishes Phone and CRM as “Not currently supported” |
| acceptTerms | boolean | no | REQUIRED true — the user must agree to Microsoft’s Customer Match terms first. Without it nothing is uploaded and nothing is hashed |
The brand’s AppLovin Ads campaigns: id, name, LIVE/PAUSED, daily budget, goal, countries, landing page. Rename or edit one with update_applovin_ads_campaign; pause or go live with set_applovin_ads_status. Read-only, free. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to get it.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| campaignIds | array | no | only these ids (up to 100) |
| page | number | no | from 1 |
| size | number | no | 1–100, default 100 |
Create an AppLovin Ads campaign, read back PAUSED: AppLovin ignores status on create, so Hermoso pauses it straight after. A WEB campaign needs name, websiteUrl, countries, dailyBudget, goalType and goalValue (CPE also eventTarget; CHK_ROAS also roasDayTarget). APP campaigns add platform, packageName, trackingMethod, clickUrl, impressionUrl (iOS: itunesId). Every field is checked before AppLovin i
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | campaign name |
| type | string | no | default: the connected account’s type |
| websiteUrl | string | no | WEB: full landing page URL with tracking parameters |
| countries | array | yes | ISO codes, e.g. ["US","CA"] |
| usRegions | array | no | US state codes, only with US |
| usMetros | array | no | US metro names such as NEW_YORK; not with usRegions |
| dailyBudget | number | no | account currency per day, all countries |
| countryBudgets | object | no | per country instead, e.g. {"US":50} |
| goalType | string | yes | WEB: CPE, CPP, CHK_ROAS |
| goalValue | number | no | target cost (CPE/CPP/CPI) or ROAS ratio (0.9 = 90%) |
| countryGoals | object | no | per-country goal values keyed by ISO code, e.g. {US: 2.5} |
| roasDayTarget | string | no | WEB DAY0/DAY7; APP DAY7/DAY28 |
| eventTarget | string | no | CPE: the event, e.g. GENERATE_LEAD |
| audienceStrategy | string | no | CPP / CHK_ROAS only |
| startDate | string | no | YYYY-MM-DD, default tomorrow (UTC) |
| endDate | string | no | end date, YYYY-MM-DD |
| catalogId | string | no | product catalog id for DPA or DOA campaigns |
| variantSetId | string | no | product variant set id within the catalog |
| catalogType | string | no | product catalog type: DPA or DOA |
| platform | string | no | app platform: IOS or ANDROID |
Rename an AppLovin campaign or change its budget, goal value, countries, US states or metros, end date or landing page, read back. On a LIVE campaign a budget, goal, country or end-date change needs confirm:true. Status is set_applovin_ads_status; goal type, bidding and start date cannot change. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; withou
| Parameter | Type | Required | Notes |
|---|---|---|---|
| campaignId | string | yes | AppLovin campaign id |
| name | string | no | new campaign name |
| dailyBudget | number | no | new daily budget in account currency, all countries |
| countryBudgets | object | no | per-country daily budgets keyed by ISO code, e.g. {US: 50} |
| goalValue | number | no | new target cost, or ROAS ratio (0.9 = 90%) |
| countryGoals | object | no | per-country goal values keyed by ISO code |
| countries | array | no | ISO country codes the campaign runs in |
| usRegions | array | no | US state codes; only when US is a target country |
| usMetros | array | no | US metro names such as NEW_YORK; not together with usRegions |
| endDate | string | no | YYYY-MM-DD |
| websiteUrl | string | no | landing page URL for a WEB campaign |
| audienceStrategy | string | no | audience strategy: UNIVERSAL, PROSPECTING or DISCOVERY |
| clickUrl | string | no | click-tracking URL for app campaigns |
| impressionUrl | string | no | impression-tracking URL for app campaigns |
| confirm | boolean | no | required for a spend change on a LIVE campaign |
LIVE or PAUSED for an AppLovin campaign (campaignId) or creative set (creativeSetId), read back. LIVE starts real ad spend: show the user the budget, get an explicit yes, then call with confirm:true. Pausing is always safe. AppLovin’s API has no delete; pause instead. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to
| Parameter | Type | Required | Notes |
|---|---|---|---|
| level | string | no | inferred from the id you pass |
| campaignId | string | no | AppLovin campaign id; pass this or creativeSetId |
| creativeSetId | string | no | AppLovin creative set id; pass this or campaignId |
| status | string | yes | LIVE starts real spend; PAUSED stops delivery |
| confirm | boolean | no | required true for LIVE |
AppLovin creative sets with their assets and review status, for one campaign (campaignId) or the whole account. Read-only, free. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to get it.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| campaignId | string | no | campaign whose creative sets to list; omit for the whole account |
| creativeSetIds | array | no | only these creative set ids |
| page | number | no | page number of the listing |
| size | number | no | rows per page |
Create an AppLovin creative set from uploaded asset ids, born PAUSED, optionally inside a campaign. It must hold EITHER an HTML playable OR a portrait image (IMG_INTER_P) plus a video; Hermoso checks that against AppLovin’s own asset types before creating it. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to get it.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | creative set name |
| assetIds | array | yes | AppLovin asset ids |
| campaignId | string | no | campaign to place the creative set in; optional |
| countries | array | no | ISO country codes for the creative set |
| languages | array | no | e.g. ENGLISH |
| url | string | no | landing page for this set only (WEB) |
| brandInfo | object | no | override the account brand on THIS creative set — how a brand A/B tests the end card |
| useAccountBrandInfo | boolean | no | true = go back to the account-level brand info |
Take an AppLovin creative set out of its campaigns (all of them, or campaignIds). AppLovin’s API has no delete or archive, so this is its only removal: the set and its assets stay on the account and can be added back. Call it WITHOUT confirm FIRST: nothing changes and you get the campaigns it is in; show the user, then call with confirm:true, plus confirmName (its exact name) and confirmChildren w
| Parameter | Type | Required | Notes |
|---|---|---|---|
| creativeSetId | string | yes | AppLovin creative set to take out of its campaigns |
| campaignIds | array | no | only these campaigns (up to 20); omit for all |
| confirm | boolean | no | true to remove, after a first call without it has shown the campaigns |
| confirmName | string | no | its exact name, when asked |
| confirmChildren | number | no | the campaign count the unconfirmed call reported |
Edit an AppLovin creative set, read back: rename it, change its countries or languages (an empty list means all), its landing page (url; "" falls back to the campaign’s), replace its assets (assetIds, checked against AppLovin’s own asset types first), or add it to more campaigns (addToCampaignIds, up to 20). Adding a LIVE set to a LIVE campaign needs confirm:true. Status is set_applovin_ads_status
| Parameter | Type | Required | Notes |
|---|---|---|---|
| creativeSetId | string | yes | AppLovin creative set id to edit |
| name | string | no | new creative set name |
| countries | array | no | ISO codes; [] = all countries |
| languages | array | no | e.g. ENGLISH; [] = all |
| url | string | no | landing page for this set; "" = the campaign’s |
| assetIds | array | no | REPLACES the set’s assets |
| addToCampaignIds | array | no | also run it in these campaigns (up to 20) |
| brandInfo | object | no | override the account brand on THIS creative set — how a brand A/B tests the end card |
| useAccountBrandInfo | boolean | no | true = go back to the account-level brand info |
| confirm | boolean | no | required to add a LIVE set to a LIVE campaign |
Copy an AppLovin creative set into another campaign, born PAUSED and read back. AppLovin only clones a set that already belongs to a campaign. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to get it.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| creativeSetId | string | yes | creative set to copy; it must already belong to a campaign |
| campaignId | string | yes | the campaign to copy it into |
Who changed what on the brand’s AppLovin campaigns, and when: creation, edits (field by field, before and after), status changes and ARCHIVING — including archived campaigns that list_applovin_ads_campaigns no longer returns. One campaign (campaignId) or the whole account; since/until must fall within the latest 30 UTC days and go together (neither = the last 7 days). Read-only, free. Needs the Ca
| Parameter | Type | Required | Notes |
|---|---|---|---|
| campaignId | string | no | ONE campaign id; omit for every campaign |
| since | string | no | YYYY-MM-DD UTC, with until |
| until | string | no | YYYY-MM-DD UTC, with since |
| page | number | no | from 1 |
| size | number | no | 1–100, default 100 |
The AppLovin account’s product catalogs and their variant sets: the catalogId and variantSetId a dynamic-ads campaign (create_applovin_ads_campaign) needs. Website accounts. Read-only, free. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to get it.
No parameter schema on file.
Assets on the AppLovin account: id, type (VID_LONG_P, VID_SHORT_P, IMG_INTER_P, IMG_BANNER, HOSTED_HTML), review status and rejection reasons. Read-only, free. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to get it.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| assetIds | array | no | only these asset ids |
| resourceType | string | no | asset kind to list: image, video or html |
| page | number | no | page number of the listing |
| size | number | no | rows per page |
Upload videos, images or an HTML playable to AppLovin from Hermoso render or Library urls or public files (mp4, mov, jpg, png, gif, html; up to 40). Returns AppLovin asset ids once processed; pass uploadId to check a pending upload again. New assets go through AppLovin review. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer say
| Parameter | Type | Required | Notes |
|---|---|---|---|
| urls | array | no | files to upload; a local file goes through upload_file first |
| uploadId | string | no | re-check an earlier upload instead |
AppLovin Ads performance: impressions, clicks, CTR, spend, sales, ROAS and checkout revenue by day, campaign, creative set or country. Works with the Reporting Key alone, which every AppLovin account has. Web reports reach back 90 days, app 45; UTC. Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| since | string | no | YYYY-MM-DD, default 7 days ago |
| until | string | no | YYYY-MM-DD, default today |
| columns | array | no | e.g. day, campaign, campaign_id_external, creative_set, country, impressions, clicks, ctr, cost, sales, roas_7d, chka_usd_7d |
| attribution | string | no | web only, default click |
| filters | object | no | column -> values |
| cohort | boolean | no | attribute to impression day |
| notZero | boolean | no | drop rows whose metrics are all zero |
| having | string | no | server-side numeric filter, e.g. "cost > 0 AND roas_7d < 1": report columns, numbers, > < >= <= = !=, AND, OR, parentheses (AppLovin warns it slows the report) |
| limit | number | no | 1–10000, default 1000 |
| offset | number | no | rows to skip, for paging past the limit |
The AppLovin pixel for the brand’s website: AppLovin’s own install snippet with the account’s Event Key filled in, where it goes (every page’s <head>, Google Tag Manager, or the AppLovin Shopify app), the events to fire for a store or a lead-gen site with copy-ready examples, dedupe ids, and how to validate it. The Event Key is public by design. Calls nothing at AppLovin. Free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| eventKey | string | no | the AppLovin Event Key; omit to use the one saved on the connection |
| siteType | string | no | default purchase |
| platform | string | no | where it is installed, default html |
Send website conversion events to AppLovin from the server (its Conversion API): purchase, generate_lead, add_to_cart and the rest, up to 100 per batch, each with the visitor’s public IP, user agent and one identifier. Email and phone are hashed with AppLovin’s own normalisation before sending; a purchase dedupes on its transaction id. Needs the Conversion API key, which AppLovin enables per accou
| Parameter | Type | Required | Notes |
|---|---|---|---|
| events | array | yes | 1–100 events; one invalid event makes AppLovin drop the whole batch, so all are checked first |
Read the brand’s connected ChatGPT Ads account — the ads that appear below ChatGPT answers. Every campaign, ad group and ad row carries servingIssues, OpenAI’s own list of what is blocking delivery (payment method, brand review, budget spent, ad in review, landing page not crawlable, country policy…) with a plain meaning each: null means OpenAI was not asked, [] means it reports no blocker, which
| Parameter | Type | Required | Notes |
|---|---|---|---|
| campaignId | string | no | list this campaign’s ad groups |
| adGroupId | string | no | list this ad group’s ads |
| limit | number | no | page size, default 100 |
| after | string | no | pagination cursor from a previous page |
Performance for ChatGPT Ads — impressions, clicks, spend, CTR, CPC, CPM, and CONVERSIONS with CPA, post-click conversion rate and attributed order sales / ROAS, in the ad account currency. OpenAI returns conversions only with granularity none or daily and with no segment or a country/device segment (never platform or product), and CPA, conversion rate and sales only with no segment at all; the rep
| Parameter | Type | Required | Notes |
|---|---|---|---|
| campaignId | string | no | scope the report to one campaign; omit for the whole ad account |
| adGroupId | string | no | scope the report to one ad group |
| adId | string | no | scope the report to one ad |
| since | string | no | YYYY-MM-DD |
| until | string | no | YYYY-MM-DD |
| granularity | string | no | default daily |
| level | string | no | roll rows up to this level |
| segment | string | no | extra group-by dimension (at most one). platform splits rows by ChatGPT app or browser (ios_app, android_app, desktop_web, ios_web, android_web, and web for rows from before 2026-09-10, which OpenAI d |
| limit | number | no | rows per page, up to 2000 |
| filters | array | no | keep only matching rows, e.g. {field:"campaign.status",operator:"IN",value:["active"]} or {field:"ad.clicks",operator:"GREATER_THAN",value:100} |
| sort | array | no | order rows, e.g. {field:"campaign.spend",direction:"desc"} |
| includeZeroImpressions | boolean | no | also list campaigns / ad groups / ads with no impressions (unsegmented reports only) |
| after | string | no | nextAfter from the previous page; limit goes up to 2000 rows |
| conversions | boolean | no | default true: add conversions, CPA, conversion rate and sales where OpenAI allows them. false leaves them out. |
| fields | array | no | extra insight fields by name, e.g. product_title, product_price, carousel_product_card_impressions; each row returns them under fields |
| attributedEvents | boolean | no | add events attributed beyond the goal |
| eventNames | array | no | only these events, e.g. ["order_created"] |
| attributionTimeBasis | string | no | default ad_event_time |
| clickWindowDays | no | default 30 | |
| viewWindowDays | no | default 1; 0 = no views |
Look up ChatGPT Ads location ids by name — countries, regions and DMAs — so a campaign can be geo-targeted. GEO AND CUSTOM AUDIENCES ARE THE ONLY LIST-BASED TARGETING THIS PLATFORM HAS: there are no interests, no lookalikes, no age or gender. Everything else is semantic, through an ad group’s context hints. Custom audiences are targeted with customAudienceIds / excludedCustomAudienceIds (see list_
| Parameter | Type | Required | Notes |
|---|---|---|---|
| query | string | yes | a place name, e.g. "Toronto" or "United Kingdom" |
| limit | number | no | maximum number of locations to return |
List the X (Twitter) ad accounts this profile can act on, with the PERMISSION LEVEL held on each so you can tell an admin grant from a read-only one before attempting a write. X grants API access PER AD ACCOUNT, not per app: the customer adds Hermoso’s X user to their ad account at business.x.com -> Account access, and it appears here. Read-only, free.
No parameter schema on file.
List campaigns on an X ad account — status, budgets, and whether X considers each servable. Omit adAccountId when only one account is reachable and it resolves itself. Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| limit | number | no | maximum number of campaigns to return |
Create a campaign on X (Twitter). ALWAYS CREATED PAUSED with no override — it spends NOTHING until set_x_ads_status(confirm:true). A CAMPAIGN ALONE CANNOT SERVE ON X: it needs a line item and a promoted post underneath it, and the read-back says so rather than letting you call it a finished ad. Requires a funding instrument (a payment method on the X ad account) — omit fundingInstrumentId to be sh
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| name | string | yes | campaign name |
| fundingInstrumentId | string | no | omit to be shown the account’s usable funding instruments |
| dailyBudget | number | no | in the ad account’s currency |
| totalBudget | number | no | lifetime budget in ad account currency |
| startTime | string | no | ISO 8601 |
| endTime | string | no | ISO 8601 end time; X deprecates campaign schedules, so set the schedule on the line item |
Pause or ACTIVATE an X ads CAMPAIGN (campaignId) or ONE LINE ITEM inside it (lineItemId) — pass exactly one. ACTIVATING STARTS REAL SPEND on the next auction, so it requires confirm:true — this is the only switch on X that arms money. Tell the user the budget and what will start spending BEFORE you pass confirm. DELIVERY ON X IS THE AND OF BOTH LEVELS: an ACTIVE line item under a PAUSED campaign s
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| campaignId | string | no | the whole campaign — pass this OR lineItemId |
| lineItemId | string | no | one ad group — pass this OR campaignId |
| status | string | yes | ACTIVE starts real spend; PAUSED stops delivery |
| confirm | boolean | no | required to set ACTIVE — real money |
Export a Microsoft Advertising account as a BULK FILE — one spreadsheet holding whatever slice of the account you ask for. This is the ONLY way to read most of a Microsoft account: roughly 185 of Microsoft’s record types are reachable here and nowhere else in Hermoso — sitelink / callout / structured-snippet / image / logo / price / promotion ad extensions, labels, shared negative keyword lists, b
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| entities | array | no | Microsoft DownloadEntity names — Campaigns, AdGroups, Ads, Keywords (the default), plus SitelinkAdExtensions, CalloutAdExtensions, Labels, Budgets, BidStrategies, AssetGroups, ConversionGoal, Experime |
| campaignIds | array | no | export only these campaigns (max 1000) instead of the whole account |
| since | string | no | ISO date — export only what CHANGED since then. Microsoft refuses a delta older than 30 days, and refuses one at all alongside QualityScoreData / BidSuggestionsData |
| fileType | string | no | default Csv |
| compression | string | no | default GZip |
| dataScope | array | no | EntityData (default), QualityScoreData, BidSuggestionsData — the last two force a FULL export |
| limit | number | no | how many rows to return inline, default 200 (the full file is still in file) |
| allowUnlistedEntities | boolean | no | send an entity name we do not recognise verbatim — for a record type Microsoft added after our copy of its list was read |
| downloadRequestId | string | no | pick up an export that came back pending. Valid 2 days, and far cheaper than exporting the account again |
| waitMs | number | no | how long to wait for Microsoft to build the file before returning pending, max 120000 |
Apply a Microsoft Advertising BULK FILE — edit hundreds of campaigns, ad groups, ads, keywords, extensions or labels in one request. Pass file as the CSV/TSV string; microsoft_ads_bulk_download returns exactly that shape in its file field. THE SPEND GATE, AND IT IS UNLIKE EVERY OTHER TOOL HERE. A bulk file carries a Status column, so one upload can turn campaigns ON — and confirm:true ALONE IS
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| file | string | yes | the bulk file itself, CSV or TSV, whose first column header must be "Type" |
| fileType | string | no | inferred from the file when omitted |
| confirm | boolean | no | the user has SEEN what this file activates and deletes and said yes. Not sufficient on its own — the two echo counts below are also required |
| confirmActivations | number | no | echo back how many objects this file ACTIVATES — the exact count the un-confirmed call reported |
| confirmDeletions | number | no | echo back how many objects this file DELETES — the exact count the un-confirmed call reported |
| pauseInstead | boolean | no | rewrite every activation to Paused so the rest of the file uploads with nothing armed. Never applied unless asked: an unrequested rewrite would PAUSE A LIVE CAMPAIGN |
| dryRun | boolean | no | report what the file would do and upload nothing at all |
| responseMode | string | no | default ErrorsAndResults, so the outcome can be read back rather than assumed |
| uploadRequestId | string | no | read the outcome of an upload that came back pending. NEVER re-upload the file — it is already queued, and a second upload applies every row twice |
| waitMs | number | no | how long to wait for Microsoft to apply the file before returning pending, max 120000 |
The conversion goals on a Microsoft Advertising account — what the account COUNTS as a conversion, and therefore what every conversion-optimised bid strategy on it is optimising toward. Flags which ones are OFFLINE goals: those are the only goals send_microsoft_ads_offline_conversions can post to, and its conversionName must match one of their names EXACTLY, including case and spacing. An account
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| goalIds | array | no | specific goal ids — omit for all of them |
| goalType | string | no | filter to one Microsoft ConversionGoalType, e.g. OfflineConversion |
Tell Microsoft Advertising about conversions that happened OFF the website — a phone sale, an in-store purchase, a lead that closed weeks after the click — so smart bidding on that account stops optimising against only what converted online. Each row needs conversionName (an existing OFFLINE conversion goal, matched by name — list_microsoft_ads_conversion_goals shows them), conversionTime (within
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| conversionName | string | no | default goal name for every row that does not carry its own |
| conversions | array | yes | up to 1000 per request — Microsoft applies each request independently |
Who ELSE is competing for the same Microsoft Advertising auctions — the other advertisers’ domains with their impression share, overlap rate, position-above rate, top-of-page rate and outranking share. microsoft_ads_report tells you what the account did; this tells you who it did it against, which is the input to a bid or a creative decision rather than a report. Scope it with entityType Account (
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| entityType | string | no | default Account |
| entityIds | array | no | REQUIRED for Campaign / AdGroup / Keyword, max 200. Microsoft only defaults to the whole account for entityType Account |
| since | string | no | YYYY-MM-DD, default 30 days back |
| until | string | no | YYYY-MM-DD, default today |
| segments | array | no | at most three — Microsoft’s own limit |
What Microsoft Advertising ITSELF suggests changing on the account: budget raises, new keywords, broad-match widenings, conflicting negative keywords to remove, and responsive search ads or extra headlines Microsoft has written. Each row carries the campaign and ad group it touches plus Microsoft’s OWN estimate of what applying it would add to clicks, impressions, conversions and COST; a budget re
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| types | array | no | narrow to some of ADD_BROAD_MATCH_KEYWORD, CAMPAIGN_BUDGET, KEYWORD, REMOVE_CONFLICTING_NEGATIVE_KEYWORD, RESPONSIVE_SEARCH_AD, RESPONSIVE_SEARCH_AD_ASSET, all six by default. An unknown name is refus |
| limit | number | no | Microsoft’s MaxCount, omit for everything |
| includeDismissed | boolean | no | also show recommendations already dismissed |
Apply Microsoft Advertising recommendations by id. THIS SPENDS REAL MONEY AND HAS NO UNDO. It raises daily budgets, adds keywords, widens keywords to broad match, DELETES negative keywords so the account buys more searches, and publishes Microsoft-written ads under the brand’s name, all live at the next auction. THE GATE IS UNLIKE THE OTHER MICROSOFT SPEND SWITCHES, AND confirm:true ALONE IS REFUS
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| recommendationIds | array | yes | ids from microsoft_ads_recommendations, max 100. There is deliberately no "apply everything" |
| confirm | boolean | no | the user has SEEN the pre-flight list and said yes. Not sufficient on its own, both echo numbers below are also required |
| confirmCount | number | no | echo back how many recommendations this applies: the exact count the un-confirmed call reported |
| confirmCostIncrease | number | no | echo back Microsoft’s own summed cost estimate for them: the exact number the un-confirmed call reported |
| dryRun | boolean | no | report what would happen and apply nothing at all |
Dismiss Microsoft Advertising recommendations by id so Microsoft stops offering them. IT CANNOT SPEND and it changes nothing about what the account runs (it takes advice off the list), so it takes no confirmation at all, which is the point: reach for this rather than applying something just to clear it. Nothing is destroyed either: Microsoft still returns a dismissed recommendation from a read, fl
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| recommendationIds | array | yes | ids from microsoft_ads_recommendations, max 100 |
Whether Microsoft is allowed to change this ad account BY ITSELF, unattended: the auto-apply opt-in status for each of the five recommendation types Microsoft supports. THIS IS THE READ THAT ANSWERS "is something changing my account when nobody is looking?", and it is worth running on any Microsoft account you inherit: an account can already be opted in without anyone at the brand having done it,
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| types | array | no | narrow to some of ResponsiveSearchAdsOpportunity, MultiMediaAdsOpportunity, RemoveConflictingNegativeKeywordOpportunity, FixConversionGoalSettingsOpportunity, CreateConversionGoalOpportunity, all five |
Switch Microsoft’s auto-apply on or off, per recommendation type. SWITCHING ONE ON IS UNLIKE EVERY OTHER WRITE IN HERMOSO: it is STANDING PERMISSION FOR MICROSOFT TO CHANGE THIS AD ACCOUNT ON ITS OWN, indefinitely, with nobody watching, publishing search and multimedia ads it wrote under the brand’s name, DELETING negative keywords so the account starts buying searches it currently blocks, or crea
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| optIns | object | yes | map of Microsoft auto-apply type name to true/false, e.g. {"RemoveConflictingNegativeKeywordOpportunity": false}. Case-sensitive keys: ResponsiveSearchAdsOpportunity, MultiMediaAdsOpportunity, RemoveC |
| confirm | boolean | no | required to turn ANY type ON, after the user has been told what Microsoft will be allowed to do unattended. Never needed to turn one off |
| confirmTypes | array | no | every type you are turning ON, named individually. That is the echo which proves you know WHICH permission you are granting, where a count could not |
Read a Microsoft Advertising ad account’s account-level settings (Microsoft’s GetAccountProperties): TrackingUrlTemplate, FinalUrlSuffix, MSCLKIDAutoTaggingEnabled, AdClickParallelTracking, IncludeViewThroughConversions, ProfileExpansionEnabled, AllowImageAutoRetrieve, BusinessAttributes, AssetAIEnhancementOptout, AIDetectedContentWatermarkPolicy and the rest Microsoft returns, each flagged settab
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| names | array | no | narrow to some account property names, e.g. ["AIDetectedContentWatermarkPolicy"]; all of them by default |
Change a Microsoft Advertising ad account’s account-level settings (Microsoft’s SetAccountProperties). They apply to EVERY campaign in the account. Settable: TrackingUrlTemplate and FinalUrlSuffix ("" clears them), MSCLKIDAutoTaggingEnabled, AdClickParallelTracking (true only, Microsoft made it mandatory), IncludeViewThroughConversions, ProfileExpansionEnabled, AllowImageAutoRetrieve, BusinessAttr
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Microsoft ad account id — omit to use the single shared account |
| accountId | string | no | older name for adAccountId (MCP only) |
| properties | object | yes | map of account property name to value, e.g. {"MSCLKIDAutoTaggingEnabled": true}, {"FinalUrlSuffix": "utm_source=bing"} or {"AIDetectedContentWatermarkPolicy": "<value read from get_microsoft_ads_accou |
| confirm | boolean | no | the user said yes. Required for ProfileExpansionEnabled=true, AllowImageAutoRetrieve=true and any AIDetectedContentWatermarkPolicy value |
Read the brand’s connected Apple Ads (Apple Search Ads) campaigns — App Store search ads, on Apple’s Platform API. Each row carries status, the system-computed displayStatus, the daily budget, the bid strategy, the countries and placements it runs in and, when a campaign cannot run, systemStatusReasons stating exactly why. Read-only, free, zero spend risk. Needs Apple Ads connected (Settings > Con
| Parameter | Type | Required | Notes |
|---|---|---|---|
| limit | number | no | page size, default 100, Apple’s max is 1000 |
| offset | number | no | offset pagination |
Ad groups on the brand’s Apple Ads account — status, the system-computed displayStatus, pricing model, the bid off the ad group’s bidStrategy, and whether Search Match (Apple’s automated keyword matching) is on. campaignId is OPTIONAL: with it, the ad groups of that one campaign; WITHOUT it, every ad group in the ad account, each row carrying its own campaignId. (Apple’s older API had no account-w
| Parameter | Type | Required | Notes |
|---|---|---|---|
| campaignId | string | no | optional — from list_apple_ads_campaigns. Omit for every ad group in the ad account. |
| limit | number | no | maximum number of ad groups to return |
| offset | number | no | ad groups to skip, for paging |
Targeting or negative keywords with their match type, status and bid. THE TWO RESOURCES SCOPE DIFFERENTLY and Apple enforces it: TARGETING keywords (the default) need campaignId OR adGroupId — campaignId alone now returns every targeting keyword in the campaign, which Apple’s older API had no way to ask for. NEGATIVE keywords (negative:true) always need one of the two as well, and Apple splits the
| Parameter | Type | Required | Notes |
|---|---|---|---|
| campaignId | string | no | the campaign — required unless you pass adGroupId |
| adGroupId | string | no | narrow to one ad group. Outranks scope on both resources. |
| negative | boolean | no | list negative keywords instead of targeting keywords |
| scope | string | no | NEGATIVE keywords only, with campaignId and no adGroupId: "campaign" (default) = the campaign-level negatives; "adgroup" = every ad-group-level negative across the campaign. |
| limit | number | no | maximum number of keywords to return |
| offset | number | no | keywords to skip, for paging |
Apple Ads performance — impressions, taps, installs, spend, TTR, CPT, CPA. level is campaign / adgroup / ad / keyword / searchterm; promotedObjectType is apps (App Store, the default) or business-brands (Ads on Apple Maps). startTime and endTime are REQUIRED, as YYYY-MM-DD. campaignId is REQUIRED for every level except campaign — it moved from the URL into a filter, but Apple still enforces it (me
| Parameter | Type | Required | Notes |
|---|---|---|---|
| level | string | no | default campaign |
| promotedObjectType | string | no | apps = App Store (default), business-brands = Ads on Apple Maps |
| campaignId | string | no | REQUIRED for every level except campaign. It moved from the URL into a filter in the Platform API, but Apple still enforces it — only a campaign-level report may be account-wide. |
| adGroupId | string | no | optional second filter — v5 had no path for this |
| startTime | string | yes | YYYY-MM-DD (required) |
| endTime | string | yes | YYYY-MM-DD (required) |
| granularity | string | no | omit entirely for a single day, or for totals only |
| groupBy | array | no | restricted per level — see the description; an unsupported value is refused by name |
| timeZone | string | no | ORTZ = the organization time zone, Apple’s default. Search-term reports accept ORTZ only. |
| grandTotals | boolean | no | add a summary row across all result rows |
| emptyMetrics | boolean | no | include entities with no delivery. App Store reports only, and never together with groupBy. |
| fields | array | no | limit the returned fields. Omit for all of them. |
| offset | number | no | row to start at (default 0). hasMore is now MEASURED against Apple’s own pagination.totalCount rather than inferred from a full page, and the default ordering is localSpend DESCENDING — so omitted row |
| limit | number | no | rows per page, up to 5000 (default 100) |
The Apple Ads ad accounts these credentials can act as, with the roles held on each, plus the organization’s currency, time zone and payment model. NAMING: Apple’s older API called these “organizations” (campaign groups) and its Platform API calls them AD ACCOUNTS — the id is the same number, and orgId and adAccountId on each row are equal. One login can cover several: an agency managing multi
No parameter schema on file.
Create an Apple Ads (Apple Search Ads) campaign promoting an iOS app on the App Store. CREATED PAUSED ALWAYS — there is no override, so nothing can spend until you separately call set_apple_ads_status(status:"ENABLED", confirm:true). REQUIRED: name (unique across the whole Apple Ads ORGANIZATION, not just this ad account — Apple 400s DUPLICATE_CAMPAIGN_NAME, and the org can contain campaigns you c
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | REQUIRED. Unique across the entire Apple Ads organization. |
| appAdamId | string | yes | REQUIRED. App Store id of the promoted app, e.g. "6752439949". |
| dailyBudget | string | yes | REQUIRED. Decimal string, e.g. "25.00". |
| countries | array | yes | REQUIRED. ISO 3166-1 alpha-2 codes, e.g. ["US","CA"]. |
| placements | array | no | Default ["APPSTORE_SEARCH_RESULTS"]. Also APPSTORE_SEARCH_TAB, APPSTORE_TODAY_TAB, APPSTORE_PRODUCT_PAGES, or MAPS_SEARCH_RESULTS / MAPS_SEARCH_HOME. One campaign cannot mix App Store and Maps placeme |
| bidStrategyType | string | no | Default MANUAL_CPT. |
| bid | string | no | MAX_CONVERSIONS only — the target CPA. Refused on MANUAL_CPT. |
| currency | string | no | Defaults to the connected ad account’s currency; an account cannot mix currencies. |
| startTime | string | no | campaign start time, ISO 8601 |
| endTime | string | no | Omit to run indefinitely. |
| budgetOrderIds | array | no | budget order id(s) from list_apple_ads_budget_orders to assign this campaign to — a budget order is a spend CEILING SHARED across every campaign assigned to it, and it works ALONGSIDE dailyBudget rath |
Create an ad group inside an Apple Ads campaign — this is where the bid and the keywords live. CREATED PAUSED ALWAYS. REQUIRED: campaignId, name. defaultBid is the most you will pay per tap (decimal string, e.g. "1.50") and applies to every keyword with no bid of its own. startTime is optional here and defaults to now: Apple documents it as optional and then REJECTS the create without it, so Hermo
| Parameter | Type | Required | Notes |
|---|---|---|---|
| campaignId | string | yes | REQUIRED. |
| name | string | yes | REQUIRED. |
| defaultBid | string | no | Max cost per tap, e.g. "1.50". |
| currency | string | no | ISO 4217 currency code of the ad group bids |
| startTime | string | no | Defaults to now. |
| endTime | string | no | Omit to inherit the campaign end date. |
| searchMatch | boolean | no | Opt into Apple’s automated keyword matching. |
| pricingModel | string | no | Default CPT; must match the campaign billing event. |
| targeting | object | no | Who and where this ad group reaches. For Apple Maps, locationGroupIds is what says which shops it promotes. |
Add targeting keywords to an Apple Ads ad group. keywords is [{text, matchType, bid}] — matchType EXACT (default), BROAD, PHRASE or CATEGORY, and bid optionally overrides the ad group default for that one term. Up to 1000 per call and 5000 per ad group. They are sent ONE AT A TIME on purpose: a single term Apple refuses cannot take the rest of the batch down, and the reply names every refusal with
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adGroupId | string | yes | REQUIRED. |
| keywords | array | yes | REQUIRED. Up to 1000. |
| matchType | string | no | Default for entries that do not set one. |
| currency | string | no | ISO 4217 currency code for the bids |
| confirm | boolean | no | Required ONLY when the ad group is already ENABLED. |
Add negative keywords to an Apple Ads campaign or ad group — search terms your ads must NOT show for. Pass exactly one of campaignId (campaign-wide) or adGroupId (that ad group only); Apple rejects a request carrying both. keywords is [{text, matchType}] with matchType BROAD (default) or EXACT. Never confirm-gated and never a spend risk, because a negative keyword only ever RESTRICTS where you sho
| Parameter | Type | Required | Notes |
|---|---|---|---|
| campaignId | string | no | Campaign-wide. Pass this OR adGroupId, never both. |
| adGroupId | string | no | One ad group. Pass this OR campaignId, never both. |
| keywords | array | yes | REQUIRED. |
| matchType | string | no | Default BROAD. |
Edit an existing Apple Ads campaign, ad group, keyword, creative or ad. Pass level and id plus ONLY the fields you want to change — Apple leaves every field you omit exactly as it is. Campaign: name, dailyBudget, startTime, endTime, countries, bidStrategyType. Ad group: name, defaultBid, startTime, endTime, searchMatch. Keyword: bid only — its text and match type are immutable, so delete and re-ad
| Parameter | Type | Required | Notes |
|---|---|---|---|
| level | string | yes | REQUIRED. |
| id | string | yes | REQUIRED. |
| name | string | no | new name for the campaign, ad group, creative, ad or location group |
| dailyBudget | string | no | Campaign only. |
| defaultBid | string | no | Ad group only. |
| bid | string | no | Keyword only. |
| countries | array | no | Campaign only. REPLACES the whole list. |
| bidStrategyType | string | no | Campaign only. |
| searchMatch | boolean | no | Ad group only. |
| targeting | object | no | Ad group only. Same shape as create_apple_ads_ad_group.targeting — locationGroupIds, radius, adminAreaIds, daypart and the rest. REPLACES the dimensions you name. |
| locationIds | array | no | Location group only (STATIC). REPLACES the whole membership. |
| rules | array | no | Location group only (DYNAMIC). REPLACES every rule and puts the group back to PENDING. |
| description | string | no | Location group only. |
| startTime | string | no | start time, ISO 8601 |
| endTime | string | no | end time, ISO 8601 |
| currency | string | no | ISO 4217 currency code |
| budgetOrderIds | array | no | budget order id(s) from list_apple_ads_budget_orders to assign this campaign to — a budget order is a spend CEILING SHARED across every campaign assigned to it, and it works ALONGSIDE dailyBudget rath |
Pause or enable an Apple Ads campaign, ad group, keyword or ad. PAUSING is immediate and needs no confirmation. ENABLING IS THE ONE SWITCH THAT ARMS REAL MONEY: it requires confirm:true, and without it nothing changes and the refusal names the object Apple actually holds under that id — read back from Apple, never echoed from your input, because aiming at the wrong campaign is invisible until mone
| Parameter | Type | Required | Notes |
|---|---|---|---|
| level | string | yes | REQUIRED. |
| id | string | yes | REQUIRED. |
| status | string | yes | REQUIRED. |
| confirm | boolean | no | REQUIRED to ENABLE — it arms real spend. Pausing needs none. |
Delete an Apple Ads campaign, ad group, keyword, negative keyword, ad, creative, asset, location group or budget order. Requires confirm:true, and optionally confirmName echoed back to prove you aimed at the right object. THIS CASCADES AND CANNOT BE UNDONE: Apple soft-deletes with no undelete, and deleting a campaign takes every ad group, keyword and ad underneath it. THE BLAST RADIUS DIFFERS BY L
| Parameter | Type | Required | Notes |
|---|---|---|---|
| level | string | yes | REQUIRED. |
| id | string | yes | REQUIRED. |
| confirm | boolean | no | REQUIRED. Nothing is deleted without it. |
| confirmName | string | no | Optional — echo the object’s exact name to prove you aimed at the right one. |
Find the App Store app to advertise and its adamId — the id every other Apple Ads tool needs (create_apple_ads_campaign’s appAdamId, a creative’s destination, an eligibility check). Call it with NO arguments to list the apps THIS Apple Ads organization owns, which is what a first-time advertiser wants: only an app you own can be promoted, so a public app you merely found by name cannot be used as
| Parameter | Type | Required | Notes |
|---|---|---|---|
| query | string | no | App name or developer to search the App Store for. |
| adamId | string | no | Fetch one app’s full details instead of searching. |
| ownedOnly | boolean | no | Default true — only apps this organization owns (the only ones that can be promoted). false searches the whole catalogue. Apple’s own default is false; Hermoso flips it because only an owned app can b |
| storeFronts | array | no | App Store country/region codes to search in, e.g. ["US","GB"]. |
| cpids | string | no | Comma-separated iTunes content provider ids to scope the search to. |
| limit | number | no | maximum number of apps to return |
| offset | number | no | apps to skip, for paging |
Ask Apple whether an app may be advertised at all, PER PLACEMENT AND PER COUNTRY — the check Apple tells you to run before building a campaign around an app. Each row is ELIGIBLE or INELIGIBLE for one combination of placement, country or region and device class, with the minimum age rating for that market. A campaign targeting a country the app is INELIGIBLE in simply will not deliver there and no
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adamId | string | yes | REQUIRED — from search_apple_ads_apps. |
| supplyPlacement | array | no | Narrow to placements, e.g. ["APPSTORE_SEARCH_RESULTS","APPSTORE_SEARCH_TAB"]. |
| countryOrRegion | array | no | ISO 3166-1 alpha-2 codes. |
| deviceClass | array | no | IPHONE and/or IPAD. |
| limit | number | no | maximum number of eligibility rows to return |
The App Store product pages this ad account can advertise — the post-tap destination a creative points at. Every app has exactly one DEFAULT product page (its standard listing, needing no setup) plus any CUSTOM PRODUCT PAGES the developer built in App Store Connect, each with its own productPageId (a UUID, not an integer) that a CUSTOM_PRODUCT_PAGE creative requires. Filter by adamId. Pass localeD
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adamId | string | no | App Store id — one app’s pages, or its DEFAULT listing’s locale details. |
| productPageId | string | no | A specific custom product page (UUID). |
| localeDetails | boolean | no | Read per-language content instead of the page list. |
| languageCode | string | no | With localeDetails — one locale, e.g. "en-US". Omit for all. |
| language | string | no | With localeDetails — a language, e.g. "en". |
| state | string | no | Filter by page state; the typical live value is PUBLISHED. |
| limit | number | no | maximum number of rows to return |
| offset | number | no | rows to skip, for paging |
The ad creatives on this Apple Ads account, each with its type, the app and product page it points at, its systemStatus (VALID / INVALID / PENDING) and its eligibility per ad placement. A creative is reusable — one can back ads in several ad groups and campaigns. Filter by creativeType, systemStatus, a name prefix or eligibility, pass id for a single one, or includeDeleted:true to see deleted reco
| Parameter | Type | Required | Notes |
|---|---|---|---|
| id | string | no | Fetch one creative. |
| creativeType | string | no | creative kind: DEFAULT_PRODUCT_PAGE, CUSTOM_PRODUCT_PAGE or LOCAL_ADS_SEARCH_CREATIVE |
| systemStatus | string | no | Apple's review state: VALID, INVALID or PENDING |
| name | string | no | Name prefix. |
| eligibility | string | no | ELIGIBLE or INELIGIBLE for ad placement |
| includeDeleted | boolean | no | true = include deleted creatives, which Apple hides by default |
| limit | number | no | maximum number of creatives to return |
| offset | number | no | creatives to skip, for paging |
Create an Apple Ads creative — what a person sees before and after tapping the ad. Pick creativeType: DEFAULT_PRODUCT_PAGE renders the app’s standard App Store listing and needs only adamId; CUSTOM_PRODUCT_PAGE renders a custom page built in App Store Connect and needs adamId AND productPageId (from list_apple_ads_product_pages); LOCAL_ADS_SEARCH_CREATIVE is Apple Maps and needs mapsBrandId (from
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | REQUIRED. |
| creativeType | string | no | Default DEFAULT_PRODUCT_PAGE. |
| adamId | string | no | REQUIRED for App Store creatives — the App Store id. |
| productPageId | string | no | REQUIRED for CUSTOM_PRODUCT_PAGE; refused on DEFAULT_PRODUCT_PAGE, which is the app’s standard listing. |
| mapsBrandId | string | no | Apple Maps only — the brand this creative belongs to. |
| brandId | string | no | older name for mapsBrandId (MCP only) |
| creativeSubtype | string | no | Apple Maps only. Default BUSINESS_LOGO. |
| assetIds | array | no | Apple Maps only — ids from upload_apple_ads_asset. |
| localizedText | object | no | Apple Maps only — promo copy per locale, e.g. {"en-US":"Visit us today"}. |
| defaultLocale | string | no | Apple Maps only, e.g. "en-US". |
The ads on this Apple Ads account — the serving units that put a creative into an ad group. Each row carries the advertiser status, the rolled-up displayStatus, Apple’s own systemStatus and, when an ad is not serving, systemStatusReasons saying exactly why: AD_APPROVAL_PENDING while Apple reviews it, CREATIVE_PENDING, CREATIVE_LOCALE_INCOMPATIBLE, PRODUCT_PAGE_HIDDEN, PAUSED_BY_USER and more. READ
| Parameter | Type | Required | Notes |
|---|---|---|---|
| id | string | no | Fetch one ad. |
| adGroupId | string | no | only ads in this ad group |
| campaignId | string | no | only ads in this campaign |
| creativeId | string | no | only ads that use this creative |
| status | string | no | advertiser's own setting: ENABLED or PAUSED |
| includeDeleted | boolean | no | true = include deleted ads |
| limit | number | no | maximum number of ads to return |
| offset | number | no | ads to skip, for paging |
Create an Apple Ads ad — the object that shows one creative in one ad group. REQUIRED: adGroupId, creativeId (from create_apple_ads_creative) and name. CREATED PAUSED ALWAYS, with no override, so it cannot spend until set_apple_ads_status(level:"ad", status:"ENABLED", confirm:true) and its parents are enabled too. THE PLACEMENT RULE, which Apple documents nowhere and which is checked FOR FREE befo
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adGroupId | string | yes | REQUIRED. |
| creativeId | string | yes | REQUIRED — from create_apple_ads_creative. |
| name | string | yes | REQUIRED. |
Upload an image into the Apple Ads asset library for an APPLE MAPS brand creative. APPLE MAPS ONLY, and that is Apple’s rule rather than ours: it refuses uploads for App Store apps outright, because an App Store ad is rendered from the app’s product page in App Store Connect and not from an image anyone uploads. That refusal is stated up front instead of being sent and rejected, so if you want a d
| Parameter | Type | Required | Notes |
|---|---|---|---|
| imageUrl | string | yes | REQUIRED — a PNG, JPG or HEIC image Hermoso can fetch. |
| mapsBrandId | string | no | REQUIRED: the Apple Maps brand this asset belongs to. |
| brandId | string | no | older name for mapsBrandId (MCP only) |
| filename | string | no | Decides the format Apple is told; defaults to the name in the URL. |
| promotedObjectType | string | no | Only BUSINESS_BRAND is accepted. |
Images in the Apple Ads asset library with format, orientation, dimensions and eligibility — use it to check whether an uploaded asset has finished processing before referencing it in a creative. Filter by mapsBrandId (Apple’s promotedObjectId), promotedObjectType or assetType, or pass id for one. CROPS AND OTHER VARIANTS NEVER APPEAR IN A LIST: Apple omits them from query results, so a variant ha
| Parameter | Type | Required | Notes |
|---|---|---|---|
| id | string | no | Fetch one asset — the only way to see a crop or variant. |
| mapsBrandId | string | no | Scope to one brand or app (promotedObjectId). |
| brandId | string | no | older name for mapsBrandId (MCP only) |
| promotedObjectType | string | no | APPSTORE_APP or BUSINESS_BRAND: which kind of promoted object the assets belong to |
| assetType | string | no | asset type; IMAGE is the only one listed |
| limit | number | no | maximum number of assets to return |
| offset | number | no | assets to skip, for paging |
The Apple Maps brands on this Apple Ads account. A BRAND IS THE ROOT OF EVERY ADS-ON-APPLE-MAPS CAMPAIGN and its id is what three other calls need: it is the promotedObjectId of a BUSINESS_BRAND campaign, the mapsBrandId upload_apple_ads_asset uploads against, and the mapsBrandId a LOCAL_ADS_SEARCH_CREATIVE carries. A brand must read eligibility ELIGIBLE before Apple will let a campaign use it. YO
| Parameter | Type | Required | Notes |
|---|---|---|---|
| mapsBrandId | string | no | Fetch one brand by id. |
| brandId | string | no | older name for mapsBrandId (MCP only) |
| name | string | no | brand name to filter by |
| countryOrRegion | string | no | Two-letter code, e.g. "US". |
| eligibilityStatus | string | no | Usually ELIGIBLE — only an ELIGIBLE brand can be used in a campaign. |
| limit | number | no | maximum number of brands to return |
| offset | number | no | brands to skip, for paging |
The physical business locations behind an Apple Maps brand — the shops, restaurants or service centres an Ads-on-Apple-Maps campaign promotes. Filter by mapsBrandId (do: without it Apple returns every location across every brand, which can be a very large page), plus status, eligibility or address. LOCATIONS ARE READ-ONLY THROUGH THIS API BY APPLE’S DESIGN: Apple Business Connect creates and maint
| Parameter | Type | Required | Notes |
|---|---|---|---|
| id | string | no | Fetch one location. |
| mapsBrandId | string | no | Strongly recommended — scopes to one brand. |
| brandId | string | no | older name for mapsBrandId (MCP only) |
| status | string | no | Usually OPEN — only OPEN locations are targetable. |
| eligibilityStatus | string | no | Apple eligibility state; only ELIGIBLE locations can be targeted |
| countryOrRegion | string | no | Two-letter code. |
| adminArea | string | no | Full state or province name, e.g. "California". |
| locality | string | no | City name. |
| postalCode | string | no | postal code to filter by |
| name | string | no | location name to filter by |
| limit | number | no | maximum number of locations to return |
| offset | number | no | locations to skip, for paging |
The location groups on this Apple Ads account — named sets of a brand’s shops that an ad group targets. Each row carries its type (STATIC or DYNAMIC), Apple’s systemStatus and groupTotal, the number of locations currently in it. READ groupTotal BEFORE TARGETING A GROUP: a DYNAMIC group sits at PENDING while Apple evaluates its rules, and a group that evaluated to ZERO is Apple’s one silent failure
| Parameter | Type | Required | Notes |
|---|---|---|---|
| id | string | no | Fetch one group. |
| mapsBrandId | string | no | Apple Maps brand id from list_apple_ads_brands |
| brandId | string | no | older name for mapsBrandId (MCP only) |
| name | string | no | Substring match. |
| groupType | string | no | STATIC (fixed shop list) or DYNAMIC (rule-based) |
| includeDeleted | boolean | no | Deleted groups are hidden by default. |
| eligibilityStatus | string | no | filter by Apple eligibility state of the group |
| limit | number | no | maximum number of location groups to return |
| offset | number | no | groups to skip, for paging |
Create a location group — the set of a brand’s shops an Apple Maps ad group promotes. TWO KINDS. STATIC takes an explicit locationIds list (from list_apple_ads_locations) and is VALID immediately. DYNAMIC takes rules and maintains itself as the brand opens and closes shops, but starts PENDING while Apple evaluates them — wait for systemStatus VALID before targeting it. A RULE THAT MATCHES NOTHING
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | REQUIRED. |
| mapsBrandId | string | no | REQUIRED: from list_apple_ads_brands. Immutable afterwards. |
| brandId | string | no | older name for mapsBrandId (MCP only) |
| groupType | string | no | Default STATIC. Immutable afterwards. |
| locationIds | array | no | REQUIRED for STATIC — ids from list_apple_ads_locations. |
| rules | array | no | REQUIRED for DYNAMIC. value is a string for EQUALS/NOT_EQUALS and an array for IN/NOT_IN. |
| description | string | no | description of the location group |
Apple’s OWN search data for an app you advertise — the fastest way to build a real Apple Search Ads keyword list instead of guessing. THE THREE KINDS DO NOT SHARE A FILTER VOCABULARY and Apple 400s any field that is not an attribute of the one you query, so each takes different parameters. kind:"keyword" (the default) returns suggested keyword strings and accepts terms + countriesOrRegions — i
| Parameter | Type | Required | Notes |
|---|---|---|---|
| kind | string | no | Default keyword. Each kind takes DIFFERENT filters — see the description. |
| promotedObjectId | string | yes | REQUIRED — the app’s adamId, or an Apple Maps brandId. |
| promotedObjectType | string | no | Default APPSTORE_APP. |
| terms | array | no | kind:"keyword" ONLY. Seed search terms to expand from, e.g. ["productivity","task management"]. Refused on phrase/category. |
| phrases | array | no | kind:"phrase" ONLY, and REQUIRED there on the SEARCH route — the phrases to look up, e.g. ["best productivity app"]. |
| categories | array | no | kind:"category" ONLY, and REQUIRED there on the SEARCH route — the App Store categories to look up, e.g. ["Utilities"]. |
| countriesOrRegions | array | no | ISO 3166-1 alpha-2, e.g. ["US","GB"]. |
| queryType | string | no | phrase/category ONLY (refused on keyword). Default SEARCH. SUGGESTION is measured BROKEN at Apple — ~34s then a server error. |
| limit | number | no | maximum number of suggestions to return |
| offset | number | no | suggestions to skip, for paging |
What Apple thinks an install should cost you. Returns the suggested target CPA for an app — the MAXIMUM tap-install CPI Apple observed across that app’s eligible markets over the last 28 days, together with the market that produced it. Only markets with at least 10 installs in that window qualify, so a brand-new or low-volume app legitimately has no suggestion yet and that is an answer, not a fail
| Parameter | Type | Required | Notes |
|---|---|---|---|
| promotedObjectId | string | yes | REQUIRED — the app’s adamId. |
| promotedObjectType | string | no | Default APPSTORE_APP. |
| countriesOrRegions | array | no | ISO 3166-1 alpha-2 markets to consider, e.g. ["US","GB","CA"]. |
Apple’s own optimisation engine for this account: campaigns it thinks are budget-capped, and campaigns whose target CPA it thinks should move. type:"daily_budget" returns DAILYCAP recommendations, each carrying the current and suggested budget plus the historical installs/taps/spend behind it AND Apple’s projection if you raise it; type:"target_cpa" returns TCPA recommendations, which Apple produc
| Parameter | Type | Required | Notes |
|---|---|---|---|
| type | string | no | Default daily_budget. |
| promotedObjectId | string | yes | REQUIRED — the app’s adamId, or an Apple Maps brandId. |
| promotedObjectType | string | no | Default APPSTORE_APP. |
| state | string | no | AVAILABLE (default), APPLIED, DISMISSED, DELETE, or "ALL" to skip the filter. |
| limit | number | no | maximum number of recommendations to return |
| offset | number | no | recommendations to skip, for paging |
THIS CHANGES WHAT A LIVE CAMPAIGN MAY SPEND, and it is the only tool in the Apple Ads suggestion/recommendation family that touches money. Applying a daily-budget recommendation raises that campaign’s daily budget to Apple’s suggested amount; applying a target-CPA recommendation moves the CPA its auto-bidder chases. REQUIRES confirm:true, and the refusal you get without it is built from the recomm
| Parameter | Type | Required | Notes |
|---|---|---|---|
| type | string | no | Default daily_budget. |
| id | string | yes | REQUIRED — the recommendation id from list_apple_ads_recommendations. |
| promotedObjectId | string | yes | REQUIRED — the same app or brand id the recommendation was queried for. |
| promotedObjectType | string | no | Default APPSTORE_APP. |
| amount | string | no | Override Apple’s suggestion with your own decimal amount, e.g. "500.00". Omit to apply what Apple suggested. |
| currency | string | no | ISO 4217; defaults to the currency of the recommendation Apple quoted (then the ad account’s). A different currency from the recommendation’s is refused — an Apple Ads account cannot mix currencies. |
| confirm | boolean | no | REQUIRED true. Without it nothing changes and the refusal states the campaign and the exact budget or CPA change. |
Decline an Apple Ads recommendation. NOTHING ABOUT THE CAMPAIGN CHANGES — no budget moves, no CPA moves, no status changes — it records that you reviewed the recommendation and said no, and Apple stops offering that particular one (it may generate a new one later). No confirmation is needed and that is deliberate rather than an oversight: dismissing cannot spend money, and gating the safe directio
| Parameter | Type | Required | Notes |
|---|---|---|---|
| type | string | no | Default daily_budget. |
| id | string | yes | REQUIRED — the recommendation id. |
| promotedObjectId | string | yes | REQUIRED — the same app or brand id the recommendation was queried for. |
| promotedObjectType | string | no | Default APPSTORE_APP. |
COMPETITIVE INTELLIGENCE: what fraction of the available impressions your app actually captures for each search term, by market and day or week. This is the report that finds where you are being out-bid or out-budgeted — a LOW share on a term with high search popularity means budget, bid or eligibility is capping your reach on a term people are really searching, which is the most actionable row Ap
| Parameter | Type | Required | Notes |
|---|---|---|---|
| promotedObjectId | string | yes | REQUIRED — the app’s adamId. Apple answers 400 without it. |
| countryOrRegion | array | no | ISO 3166-1 alpha-2, e.g. ["US"]. |
| start | string | yes | REQUIRED YYYY-MM-DD. Must be a SUNDAY when granularity is WEEKLY_SUN_SAT. |
| end | string | yes | REQUIRED YYYY-MM-DD. |
| granularity | string | no | Default DAILY (max 30-day window). WEEKLY_SUN_SAT allows at most 4 weeks. |
| reportType | string | no | Default FIRST_SLOT. |
| limit | number | no | maximum number of rows to return |
| offset | number | no | rows to skip, for paging |
The most-searched App Store terms in a genre and market — MARKET-WIDE DEMAND, not your own performance, so it works for finding terms to target before you have spent anything. Cross-read it against apple_ads_impression_share: a term ranking high here where your impression share is low is exactly where budget or bid is leaving installs on the table. genre is a FREE-TEXT App Store genre name (e.g. "
| Parameter | Type | Required | Notes |
|---|---|---|---|
| countryOrRegion | array | no | ISO 3166-1 alpha-2, e.g. ["US","GB","DE"]. |
| genre | array | no | App Store genre names, free text, e.g. ["PRODUCTIVITY_UTILITIES"]. |
| start | string | yes | REQUIRED YYYY-MM-DD. Must be a SUNDAY when granularity is WEEKLY_SUN_SAT. |
| end | string | yes | REQUIRED YYYY-MM-DD. |
| granularity | string | no | Default WEEKLY_SUN_SAT. DAILY does NOT exist on this report. |
| limit | number | no | maximum number of rows to return |
| offset | number | no | rows to skip, for paging |
WHY an Apple Ads creative, custom product page or Maps brand was REJECTED: Apple’s reason code, type and level (default or custom product page, or a locale of one), the asset or product page it applies to, the country and language, and Apple’s comment — read live. Use it whenever list_apple_ads_creatives shows systemStatus INVALID, or the user asks why an Apple ad is not running. Pass adamId for a
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adamId | string | no | the app’s App Store id — every reason Apple holds for it |
| mapsBrandId | string | no | a Maps brand id (list_apple_ads_brands) — its policy rejection reasons. Not brandId: over the REST API that name means a Hermoso brand |
| rejectionReasonId | string | no | one reason by id |
| limit | number | no | maximum number of reasons to return |
| offset | number | no | reasons to skip, for paging |
Who changed what on this Apple Ads account, and when. Call it with start and/or end to get one row per TRANSACTION GROUP — a single user action that may have touched several fields, which is what changeCount counts — with the user, the entity and the event type (CREATE / UPDATE / DELETE). Then pass a row’s detailId back as detailId to get the field-by-field before and after. THE TIME BOUND IS MAND
| Parameter | Type | Required | Notes |
|---|---|---|---|
| detailId | string | no | Fetch field-level detail for ONE change. This is a COMPOSITE id ({transactionId}-{entityType}-{entityId}) that appears on each summary row — not a campaign or ad group id. |
| start | string | no | YYYY-MM-DD. Required unless end or an explicit eventTime filter is given. |
| end | string | no | YYYY-MM-DD. |
| filters | array | no | Extra conditions, e.g. [{"field":"campaignId","operator":"EQUALS","value":"123"}]. Non-time fields accept only EQUALS and IN. |
| limit | number | no | maximum number of changes to return |
| offset | number | no | changes to skip, for paging |
Budget orders on this Apple Ads account — a spend CEILING SHARED ACROSS CAMPAIGNS, which is how an agency caps a client for a quarter: total spend across every campaign assigned to one cannot exceed its value, however generous the individual daily budgets are. Each row carries the value, the active window, the systemStatus and, when it is blocking, the systemStatusReasons saying why (EXHAUSTED, EX
| Parameter | Type | Required | Notes |
|---|---|---|---|
| id | string | no | Fetch one budget order, including its invoice details and assigned ad accounts. |
| filters | array | no | Query conditions, e.g. [{"field":"name","operator":"CONTAINS","value":"Q3"}]. |
| limit | number | no | maximum number of budget orders to return |
| offset | number | no | budget orders to skip, for paging |
A BUDGET ORDER NEEDS A LINE OF CREDIT ORGANIZATION. Measured on prod 2026-08-17 against org 19534840 (PAYG): Apple answers 403 “The organization does not have a Line of Credit(LOC). Creating Budget Order is permissible only for LOC Organizations. [FORBIDDEN]” — so on a pay-as-you-go account this whole family is unusable and no request shape fixes it. That is Apple’s account tier, not a Hermoso lim
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | REQUIRED. |
| startTime | string | yes | REQUIRED. Must be TOMORROW or later (midnight UTC) — Apple rejects today. |
| endTime | string | no | Omit for an open-ended budget. |
| amount | string | yes | REQUIRED total budget as a decimal string, e.g. "50000.00". |
| currency | string | no | ISO 4217; defaults to the ad account’s currency, which it must match. |
| primaryBuyerEmail | string | yes | REQUIRED — Apple bills and notifies this address. |
| billingEmail | string | yes | REQUIRED. |
| invoiceName | string | no | name shown on the invoice |
| orderNumber | string | no | purchase order or internal order number |
| clientName | string | no | client name shown on the invoice |
| primaryBuyerName | string | no | name of the primary buyer on the invoice |
Change a budget order: its name, window, total value, attached ad account or invoice details. Only the fields you send change; everything omitted keeps its value. RAISING amount IS HOW YOU EXTEND A BUDGET APPROACHING EXHAUSTION — an exhausted budget order stops every campaign drawing from it, so this is the tool to reach for when campaigns stop serving with budget left on their own daily caps. A
| Parameter | Type | Required | Notes |
|---|---|---|---|
| id | string | yes | REQUIRED. |
| name | string | no | new budget order name |
| startTime | string | no | Must still be TOMORROW or later. |
| endTime | string | no | Can only SHORTEN an active budget order’s window. |
| openEnded | boolean | no | true clears the end date entirely, making the budget open-ended. This is the only way to extend rather than shorten. |
| amount | string | no | New total, e.g. "75000.00". Raise it to extend a budget nearing exhaustion. |
| currency | string | no | ISO 4217 currency code |
| adAccountId | string | no | Exactly one — Apple rejects more. |
| orderNumber | string | no | purchase order or internal order number |
| invoiceName | string | no | name shown on the invoice |
| clientName | string | no | client name shown on the invoice |
| primaryBuyerName | string | no | name of the primary buyer on the invoice |
| primaryBuyerEmail | string | no | email address Apple bills and notifies for this budget order |
| billingEmail | string | no | billing contact email for invoices |
Create or update up to 1,000 Apple Ads keywords or negative keywords in ONE call — how you seed a whole campaign structure from apple_ads_keyword_suggestions, or re-bid a list after a performance review, instead of one call per keyword. A single request can span DIFFERENT AD GROUPS. op:"create" takes full keyword rows; op:"update" takes ids and can change ONLY bid and status, because a keyword’s t
| Parameter | Type | Required | Notes |
|---|---|---|---|
| kind | string | no | Default keyword. |
| op | string | no | Default create. |
| items | array | yes | REQUIRED. create: [{adGroupId,text,matchType,bid?}] for keywords, or [{campaignId|adGroupId,text,matchType}] for negatives. update: [{id,bid?,status?}]. |
| allowPartialSuccess | boolean | no | true keeps the items that succeeded when others fail. Apple’s default is all-or-nothing. |
| currency | string | no | ISO 4217 for bids; defaults to the ad account’s currency. |
| confirm | boolean | no | Required only when the target ad group or campaign is already ENABLED, where the new keywords could serve immediately. |
Resolve a place NAME into the geo id an Apple Ads ad group actually targets. THIS IS THE ONLY WAY TO GET THOSE IDS: an ad group’s targeting takes adminAreaIds / localityIds / postalCodeIds / countryIds, it does NOT accept place names, and nothing else in Hermoso produces one — so without this tool those targeting fields cannot be used at all. (A campaign’s own countries field is a different thin
| Parameter | Type | Required | Notes |
|---|---|---|---|
| supplySource | string | yes | REQUIRED. APPSTORE for App Store campaigns, MAPS for Apple Maps. Decides which entity types exist. |
| query | string | no | Name to search, minimum 2 characters. "*" returns everything at the entity level. |
| entity | string | no | Narrow to one granularity. |
| countryCode | string | no | ISO 3166-1 alpha-2 to scope the search. |
| eligible | boolean | no | false excludes soft-blocked geos entirely. Default includes them with eligibility data. |
| geos | array | no | Look UP known geos instead of searching by name: [{"id":"12345","entity":"Locality"}] or [{"legacyId":"US|CA|SanFrancisco","entity":"Locality"}]. Exactly one of id or legacyId per entry. |
| limit | number | no | maximum number of geo locations to return |
| offset | number | no | geo locations to skip, for paging |
Every country and region Apple Ads serves, with the languages supported in each market. Use it to VALIDATE A LOCALE BEFORE setting it on an Apple Maps creative’s localizedText or defaultLocale, or on an ad group — a locale a market does not support makes the creative INVALID after it is built, which surfaces as a serving failure long after the mistake was made. Also the definitive answer to "can w
| Parameter | Type | Required | Notes |
|---|---|---|---|
| filters | array | no | Scope to specific markets, e.g. [{"field":"countryCode","operator":"IN","value":["US","DE"]}]. |
| limit | number | no | maximum number of markets to return |
| offset | number | no | markets to skip, for paging |
The full record for an Apple Ads ad account — its name, currency, timezone, payment model, productFeatures and the advertiser resources delegated to it. Called with no id it returns the account this connection is pinned to. THE FIELD THAT DECIDES WHAT THIS ACCOUNT CAN DO IS productFeatures, and it is IMMUTABLE: APPSTORE_APP_MANUAL can never run Apple Maps ads and BUSINESS_BRAND_MANUAL can never ru
| Parameter | Type | Required | Notes |
|---|---|---|---|
| id | string | no | Defaults to the ad account this Apple Ads connection resolved. |
The brands and content providers this Apple Ads ORGANIZATION can delegate to an ad account — the ids create_apple_ads_ad_account and update_apple_ads_ad_account need. resourceType CONTENT_PROVIDER returns App Store Connect content providers, each identified by its CPID, which is what links an ad account to App Store advertising; BUSINESS_BRAND returns Apple Maps brands by Brand ID. resourceType is
| Parameter | Type | Required | Notes |
|---|---|---|---|
| resourceType | string | yes | REQUIRED. CONTENT_PROVIDER = App Store (CPID); BUSINESS_BRAND = Apple Maps (Brand ID). |
Create a new ad account under this Apple Ads organization — the container an agency opens per client, or a business opens to run Apple Maps ads alongside App Store ones. AN AD ACCOUNT SPENDS NOTHING: it holds campaigns, and every campaign inside it is still born PAUSED. productFeatures is REQUIRED, is a CHOICE OF ONE, and is IMMUTABLE FOREVER — APPSTORE_APP_MANUAL for App Store advertising or BUSI
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | REQUIRED. |
| productFeatures | string | yes | REQUIRED and IMMUTABLE. App Store or Apple Maps, never both. |
| delegations | array | no | [{"resourceType":"CONTENT_PROVIDER","resourceId":"<CPID>"}] for App Store, or BUSINESS_BRAND + Brand ID for Maps. Must match productFeatures. |
Rename an Apple Ads ad account or change which advertiser resources it delegates — how you link App Store Connect (a CONTENT_PROVIDER / CPID) to an account that cannot yet advertise an app, or attach a brand for Apple Maps. WARNING: DELEGATIONS REPLACE RATHER THAN MERGE. The list you send becomes the account’s ENTIRE delegation set and anything omitted is removed, so to ADD one, read the current s
| Parameter | Type | Required | Notes |
|---|---|---|---|
| id | string | no | Defaults to the ad account this connection resolved. |
| name | string | no | new ad account name |
| delegations | array | no | REPLACES the entire set. [{"resourceType":"CONTENT_PROVIDER","resourceId":"<CPID>"}]. |
Change a LIVE X campaign — budget, name, delivery pacing. THIS IS HOW YOU THROTTLE OR RAISE SPEND on a running campaign without rebuilding it, and lowering dailyBudget is the fastest way to slow money down short of pausing. Budgets are in the ad account's own currency. Only fields X actually applies are offered: startTime/endTime are DEPRECATED on an X campaign (its schedule lives on the LINE ITEM
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| campaignId | string | yes | from list_x_ads_campaigns |
| name | string | no | new campaign name |
| dailyBudget | number | no | in the ad account's currency |
| totalBudget | number | no | new lifetime budget in ad account currency |
| standardDelivery | boolean | no | false = accelerated. Works on a campaign-budget campaign; with line item budgets set it on the line item |
| purchaseOrderNumber | string | no | purchase order number recorded on the campaign |
Change an X line item — BID, bid strategy, SCHEDULE, goal or name. An X campaign's start and end times are deprecated, so the schedule genuinely lives HERE. Lowering bidAmount is also the fix when X refuses a campaign budget with “Bid is too close to Budget”. NOT changeable after creation: objective and productType — X answers 200 and silently keeps the old value (measured), so both are refu
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| lineItemId | string | yes | from list_x_ads_line_items |
| name | string | no | new line item (ad group) name |
| bidAmount | number | no | in the ad account's currency |
| bidStrategy | string | no | AUTO, GUARANTEED, MAX or TARGET; MAX and TARGET need bidAmount |
| goal | string | no | requires bidAmount too |
| startTime | string | no | ISO 8601 |
| endTime | string | no | ISO 8601 end time for this line item |
| dailyBudget | number | no | only if the campaign is not budget-optimised |
| totalBudget | number | no | only if the campaign is not budget-optimised |
| standardDelivery | boolean | no | false = accelerated, only with line item budgets; X refuses it under a campaign budget |
List the promoted posts (the CREATIVES) attached to an X line item, or across the whole ad account. Two things only this can tell you: each post's APPROVAL STATUS, so an ad X rejected — which can never serve however the statuses are set — is visible rather than mysterious; and the promoted-tweet ID, which is the only way to remove one. NOTE a promoted post cannot be PAUSED on X (its PUT accepts on
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| lineItemId | string | no | scope to one line item |
| limit | number | no | maximum number of promoted posts to return |
List every targeting criterion an X line item carries, WITH THE ID of each — the id needed to remove one with delete_x_ads_object. READ THE EMPTY CASE CORRECTLY: no targeting criteria on X means the line item is UNRESTRICTED and will reach the broadest possible audience once ACTIVE — it does NOT mean it cannot serve. Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| lineItemId | string | yes | X line item id from list_x_ads_line_items |
List the funding instruments (payment methods) on an X ad account — type, currency, credit limit, credit remaining, and whether each can currently fund a campaign. THIS IS THE ANSWER TO “why is my X campaign not delivering?” whenever the cause is a cancelled card or an exhausted credit line, which is invisible from the campaign itself, and it shows what a campaign will spend against BEFORE anyone
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
Resolve X targeting ids by name for every vocabulary BEYOND location — INTEREST, CONVERSATION, PLATFORM, DEVICE, LANGUAGE, APP_STORE_CATEGORY, NETWORK_OPERATOR, TV_MARKET, TV_SHOW, EVENT. X's targeting ids are opaque (an interest is a 19-digit number, a device is “96”) and X has NO name-based targeting parameter, so this is the only way to obtain one — without it add_x_ads_targeting accepts vocabu
| Parameter | Type | Required | Notes |
|---|---|---|---|
| kind | string | yes | targeting vocabulary to search, e.g. INTEREST, DEVICE, LANGUAGE or EVENT |
| query | string | no | filter by name |
| osType | string | no | APP_STORE_CATEGORY |
| countryCode | string | no | NETWORK_OPERATOR, e.g. US |
| locale | string | no | TV_SHOW — required; get one from kind:"TV_MARKET" |
| eventTypes | string | no | EVENT |
| limit | number | no | maximum number of results to return |
PERMANENTLY delete an X campaign, line item, promoted post, promoted account or targeting criterion — or ARCHIVE a lead form (it stops collecting and its leads can no longer be exported, so run get_x_ads_leads first), or remove a card, a Media Library item (type media: id is the media_key) or a custom audience (custom_audience). X CASCADES AND PUBLISHES NO UNDO: deleting a campaign destroys its li
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| type | string | yes | kind of object the id names; for media, id is the media_key |
| id | string | yes | campaign/line-item id, or the id from list_x_ads_promoted_tweets / list_x_ads_targeting |
| lineItemId | string | no | REQUIRED for type "targeting_criterion" — X cannot look one up without its line item |
| confirm | boolean | no | omit on the first call to see the blast radius |
| confirmName | string | no | the target's exact name — required once it has children, is live, or has spent |
| confirmChildren | number | no | the real number of children, from the unconfirmed call |
List the line items on an X ad account — X's name for an ad group, and the level that carries the objective, the placements, the bid, the targeting and the creatives. Pass campaignId to scope it to one campaign. servable is X's own verdict on whether the line item could run. Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| campaignId | string | no | scope to one campaign's line items; omit for all |
| limit | number | no | maximum number of line items to return |
Create a line item — X's ad group — under a campaign. ALWAYS CREATED PAUSED with no override. THE TREE ON X IS campaign -> line item -> promoted post, and a campaign ALONE CANNOT SERVE: this is the middle level, and it still cannot serve until you attach a post with create_x_ads_promoted_tweet. Targeting attaches HERE (add_x_ads_targeting), never to the campaign. objective is validated before di
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| campaignId | string | yes | from create_x_ads_campaign or list_x_ads_campaigns |
| name | string | no | line item (ad group) name |
| objective | string | yes | one per campaign (X refuses a second objective). For an X LEAD AD use WEBSITE_CLICKS with a lead-form card — X retired LEAD_GENERATION |
| productType | string | no | default PROMOTED_TWEETS — PROMOTED_ACCOUNT when objective is FOLLOWERS, and that line item serves the ACCOUNT attached with create_x_ads_promoted_account |
| placements | array | no | default ALL_ON_TWITTER |
| goal | string | no | optimization goal, e.g. LINK_CLICKS or WEBSITE_CONVERSIONS; these are goals, not objectives |
| standardDelivery | boolean | no | false = accelerated, only with line item budgets; X refuses it under a campaign budget |
| bidStrategy | string | no | AUTO lets X set the bid; MAX and TARGET need bidAmount |
| bidAmount | number | no | required for MAX/TARGET; in the ad account's currency |
| totalBudget | number | no | lifetime budget for this line item in ad account currency |
| startTime | string | no | ISO 8601; defaults to now |
| endTime | string | no | ISO 8601 end time for this line item |
THE CREATIVE OF AN X FOLLOWER CAMPAIGN. A line item with objective FOLLOWERS (productType PROMOTED_ACCOUNT, which create_x_ads_line_item now defaults to for FOLLOWERS) promotes an X ACCOUNT, and until one is attached here it has nothing to show and never serves. Omit userId and the ad account’s own promotable account is used; if it may promote several, the choice is refused rather than guessed (li
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| lineItemId | string | yes | a PROMOTED_ACCOUNT line item (objective FOLLOWERS) from create_x_ads_line_item |
| userId | string | no | the numeric X user id to promote — omit for the ad account’s own promotable account |
The X accounts this ad account may promote — FULL (can be promoted for followers with create_x_ads_promoted_account and have its posts promoted) or RETWEETS_ONLY (posts only). Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
List the lead forms on an X ad account (or one, with leadFormId) — fields, privacy policy, and the customer-list audience X builds from each form's leads. Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| leadFormId | string | no | one lead form id; omit for all forms |
| withDeleted | boolean | no | include archived forms |
| limit | number | no | maximum number of lead forms to return |
Create an X lead form — the native form an X lead ad opens, so people sign up without leaving X. The X lead ad chain: lead form -> create_x_ads_card(leadFormId) -> create_x_ads_post(cardUri, lineItemId) on a WEBSITE_CLICKS line item (X retired the LEAD_GENERATION objective) -> get_x_ads_leads. Needs name, purpose, fields (at least one required), and an https privacy policy URL + company name. X ti
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| name | string | yes | internal name of the lead form |
| purpose | string | yes | what the leads are used for (shown to the person) |
| fields | array | yes | at least one must not be optional |
| privacyPolicyUrl | string | yes | https |
| companyName | string | yes | company name shown on the form |
| headline | string | no | intro card headline |
| description | string | no | intro card text |
| thankYouHeadline | string | no | headline on the thank-you page shown after submit |
| thankYouDescription | string | no | text on the thank-you page shown after submit |
| thankYouUrl | string | no | https link on the thank-you button |
| thankYouButton | string | no | label of the thank-you page button |
| conversionEventId | string | no | Event ID from X Events Manager — required when the ad account has no X Pixel |
Change an X lead form. Only the fields you pass change — but X replaces the fields list and each section (intro, thank-you page, privacy policy) WHOLE, so pass the complete new value of any you touch. An archived form cannot be changed. Read back from X. Free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| leadFormId | string | yes | lead form id from list_x_ads_lead_forms |
| name | string | no | new internal name of the lead form |
| purpose | string | no | what the leads are used for, shown to the person |
| fields | array | no | replaces the whole field list; each entry as in create_x_ads_lead_form |
| privacyPolicyUrl | string | no | https link to the privacy policy shown on the form |
| companyName | string | no | company name shown on the form |
| headline | string | no | intro card headline |
| description | string | no | intro card text |
| thankYouHeadline | string | no | thank-you page headline; X replaces the whole section |
| thankYouDescription | string | no | thank-you page text; X replaces the whole section |
| thankYouUrl | string | no | https link on the thank-you button |
| thankYouButton | string | no | thank-you page button label |
| conversionEventId | string | no | X Events Manager event id; needed when the account has no X Pixel |
Read the leads submitted to an X lead form over a window (default the last 30 days) — each lead's answers keyed by field (EMAIL, FULL_NAME…). X builds the export as a job; this waits up to ~45s and, if X is still working, returns an exportId to collect later with no new job. X lets only an ACCOUNT ADMIN export leads, and refuses an ARCHIVED form, so export before archiving. format csv/xlsx returns
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| leadFormId | string | no | X lead form id from list_x_ads_lead_forms; required unless exportId is given |
| startTime | string | no | ISO 8601 |
| endTime | string | no | ISO 8601; default now |
| days | number | no | window length when startTime is omitted; default 30 |
| format | string | no | json (default, returns leads inline) | csv | xlsx (returns a download link) |
| exportId | string | no | collect an export that was still running |
Upload an image or video (any URL — a Hermoso render, an upload_file URL, a public file) to an X ad account and return its media_key, for create_x_ads_card or create_x_ads_post. Added to the ad account's Media Library and read back. A card image must be 1:1 ≥600×600, 1.91:1 ≥600×314, 16:9 ≥600×337, 9:16 ≥600×1066, 2:3 ≥600×900 or 4:5 ≥600×750. Free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| url | string | yes | public image or video URL: a Hermoso render, an upload_file URL or any public file |
| name | string | no | label in the Media Library |
List the X ad account's Media Library — media keys usable in cards and promoted-only posts. Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| mediaType | string | no | IMAGE, VIDEO or GIF; omit for all types |
| limit | number | no | max items to return, 1-50; default 20 |
Create an X card — the big image/video with a headline or button that an X ad clicks through on. ONE destination: websiteUrl (a website card, with a headline title), leadFormId (opens an X lead form — headline, or a button with buttonLabel), or an app (iosAppId/googlePlayAppId + countryCode, a button). Media: mediaKey, or 2–6 mediaKeys for a swipeable card (upload_x_ads_media). utm {source,mediu
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| name | string | yes | internal name for the card |
| mediaKey | string | no | one media key from upload_x_ads_media (single image or video) |
| mediaKeys | array | no | 2-6 media keys from upload_x_ads_media, for a swipeable card |
| title | string | no | headline, for website and lead-form cards |
| websiteUrl | string | no | destination URL for a website card |
| utm | object | no | UTM tags object: source, medium, campaign, content, term; appended to websiteUrl |
| leadFormId | string | no | X lead form id from list_x_ads_lead_forms, for a lead-form card |
| buttonLabel | string | no | button text shown on a lead-form or app card |
| iosAppId | string | no | iOS App Store app id, for an app card |
| googlePlayAppId | string | no | Google Play package id, for an app card |
| countryCode | string | no | 2-letter store country for an app card, e.g. US |
| iosDeepLink | string | no | iOS deep link for an app card |
| googlePlayDeepLink | string | no | Android deep link for an app card |
List the cards on an X ad account with their card_uri and destination. Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| query | string | no | name prefix |
| limit | number | no | max cards to return, 1-200; default 50 |
Write an X post FOR ADS — PROMOTED-ONLY by default (X's 'nullcast'): it never appears on the brand's timeline or in followers' feeds and serves only as an ad, so you can test several creatives without spamming followers. Carry a card (cardUri from create_x_ads_card) OR up to 4 media (mediaKeys from upload_x_ads_media). Posted as the ad account's own promotable user (asUserId to choose). Pass lineI
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| text | string | no | post text |
| cardUri | string | no | card_uri from create_x_ads_card; use instead of mediaKeys |
| mediaKeys | array | no | up to 4 media keys from upload_x_ads_media |
| name | string | no | internal name in ads.x.com |
| promotedOnly | boolean | no | default true |
| confirmPublic | boolean | no | true is required with promotedOnly:false, to publish a public post |
| asUserId | string | no | promotable user id to post as; omit for the ad account's own user |
| conversationSettings | string | no | who may reply to the post: EVERYONE, FOLLOWING or MENTIONED_USERS |
| videoTitle | string | no | title of a video post |
| videoDescription | string | no | description of a video post |
| videoCta | string | no | call-to-action button on a video post: VISIT_SITE or WATCH_NOW |
| videoCtaUrl | string | no | URL the video call-to-action links to |
| utm | object | no | UTM tags object: source, medium, campaign, content, term; appended to the destination URL |
| lineItemId | string | no | attach to this line item right away |
List the posts an X ad account can promote — promoted-only ones by default (timeline NULLCAST), or ORGANIC / ALL. Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| timeline | string | no | NULLCAST = promoted-only posts (default); ORGANIC = organic posts; ALL = both |
| tweetType | string | no | post state filter: DRAFT, PUBLISHED or SCHEDULED; omit for all |
| tweetIds | array | no | return only these post ids |
| asUserId | string | no | promotable user id whose posts to list; omit for the ad account's own user |
| limit | number | no | max posts to return |
See X posts exactly as the ad will render — X returns a preview page per post (works for promoted-only posts). Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| tweetIds | array | yes | post ids to preview, from list_x_ads_posts |
| tweetType | string | no | state of the posts: DRAFT, PUBLISHED or SCHEDULED |
List the custom audiences (customer lists) on an X ad account — size, whether each can be targeted yet and why not. Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| audienceIds | array | no | return only these audience ids |
| query | string | no | name prefix |
| shared | boolean | no | lists shared with this account instead of owned |
| limit | number | no | max audiences to return |
Create an X custom audience (a customer list for retargeting or exclusion), optionally filled with people in the same call. Pass ordinary emails, phones (E.164), X handles, X user ids or device ids — they are normalised and SHA-256 hashed on Hermoso's server before X sees them; 64-hex hashes pass as is; anything unusable is named, never sent. X needs up to 48h and about 100 matched people before i
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| name | string | yes | unique per account |
| description | string | no | optional description of the audience |
| emails | array | no | plain addresses or 64-hex SHA-256 hashes |
| phones | array | no | E.164, e.g. +14155550100, or hashes |
| handles | array | no | X handles, with or without @ |
| twitterIds | array | no | X user ids to add; hashed on Hermoso's server before X receives them |
| deviceIds | array | no | IDFA / AdID / Android id |
| users | array | no | one person per entry with several identifiers: {email, phone, handle, twitterId, deviceId} |
| expiresAt | string | no | ISO 8601; X defaults to 13 months |
Change an X custom audience: rename / redescribe it, and/or ADD or REMOVE people (operation add|remove) — same identifiers as create_x_ads_audience, hashed on our server. X reports how many it processed and Hermoso relays it; a mismatch is flagged as not fully applied. Read back. Free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| audienceId | string | yes | custom audience id to change, from list_x_ads_audiences |
| name | string | no | new audience name |
| description | string | no | new audience description |
| operation | string | no | add or remove: whether the people in this call are added to or removed from the audience |
| emails | array | no | plain addresses or 64-hex SHA-256 hashes |
| phones | array | no | E.164, e.g. +14155550100, or hashes |
| handles | array | no | X handles, with or without @ |
| twitterIds | array | no | X user ids to add; hashed on Hermoso's server before X receives them |
| deviceIds | array | no | IDFA / AdID / Android id |
| users | array | no | one person per entry with several identifiers: {email, phone, handle, twitterId, deviceId} |
| effectiveAt | string | no | ISO 8601 time the membership change takes effect (sent to X as effective_at) |
| expiresAt | string | no | ISO 8601 time the members expire; must be later than effectiveAt |
Report conversions to X server-side (the X Conversion API) — purchases, sign-ups, leads — so X attributes them to the ads that caused them and conversion bidding (goal WEBSITE_CONVERSIONS_V2) has something to optimise toward. pixelId and each eventId come from X Events Manager. Each event needs a click id (twclid from the landing URL), an email or an E.164 phone — emails and phones are SHA-256 has
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| pixelId | string | yes | the X Pixel (event source) id from Events Manager |
| eventId | string | no | default Event ID for every event |
| events | array | yes | conversion events to report, one entry per purchase, sign-up or lead |
Attach an existing X post to a line item so the line item has a creative — the LAST piece of the tree, and without it nothing can ever serve however the statuses are set. The tweet id is the number at the end of the post URL (x.com/<handle>/status/<id>), not the URL. Several ids may be attached at once; X creates one promoted-tweet row per post. ATTACHMENT IS VERIFIED by re-reading the line item's
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| lineItemId | string | yes | from create_x_ads_line_item |
| tweetIds | array | yes | post ids — digits only, from the end of the post URL |
Add targeting criteria to an X LINE ITEM. Targeting does NOT attach to a campaign on X — a campaign carries only budget and funding. Pass locationIds resolved with x_ads_geo_search, and/or criteria[] for any of X's other 37 targeting vocabularies when you already hold the ids. X takes ONE criterion per API call, so a set is several calls: each is reported individually, a partial failure NAMES what
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| lineItemId | string | yes | line item id to target, from list_x_ads_line_items |
| locationIds | array | no | opaque ids from x_ads_geo_search |
| criteria | array | no | [{targetingType, targetingValue, operatorType?}] — operatorType defaults to EQ |
Look up X targeting location ids by name — countries, regions, metros, cities and postal codes. X location ids are opaque hashes (Canada is 3376992a082d67c7), so this is the ONLY way to obtain one and there is no name-based targeting parameter to fall back on. Pass the ids to add_x_ads_targeting as locationIds. Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| query | string | yes | a place name, e.g. "Canada" or "Austin" |
| locationType | string | no | COUNTRIES, REGIONS, METROS, CITIES or POSTAL_CODES; omit to search all types |
| limit | number | no | max results to return |
Performance stats for X campaigns, line items or promoted posts. placement here is SINGULAR and comes from a FOUR-value set (ALL_ON_TWITTER / PUBLISHER_NETWORK / SPOTLIGHT / TREND) — deliberately not the twelve placements a line item accepts; do not carry one across. THE TRAP THIS REPORTS: an entity id that does not exist on the account answers 200 with every metric null, which is indistinguisha
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | X ad account id (list_x_ads_accounts); omit if only one is shared |
| accountId | string | no | older name for adAccountId (MCP only) |
| entity | string | no | default CAMPAIGN |
| entityIds | array | yes | max 20 |
| startTime | string | no | ISO 8601; defaults to 7 days before endTime |
| endTime | string | no | ISO 8601; defaults to now |
| granularity | string | no | default TOTAL |
| placement | string | no | default ALL_ON_TWITTER |
| metricGroups | array | no | default ENGAGEMENT |
List the conversion event settings on the connected ChatGPT Ads account. Their ids are what a campaign points at (conversionEventSettingIds) so it optimises for CONVERSIONS rather than raw clicks — without one, conversion-optimised bidding has nothing to optimise toward. Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| limit | number | no | max conversion events to return |
Create a ChatGPT Ads web pixel — the thing that observes actions on the site. IT RECORDS NOTHING until its snippet is installed on the site, so say that rather than implying tracking is live. MATCH RATE: the snippet’s oaiq("init", { user: {…} }) call accepts email_sha256, phone_number_sha256, external_id_sha256, first_name_sha256, last_name_sha256 (all SHA-256, lowercase hex) plus raw country, cit
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | a name for the pixel |
| automaticAdvancedMatching | boolean | no | default true (OpenAI’s own default). false stops the Pixel collecting and hashing customer information from the page. |
Define what counts as a conversion on ChatGPT Ads, measured from one or more pixels. THIS IS THE PREREQUISITE for a conversion-optimised campaign: pass the returned id as conversionEventSettingIds to create_openai_ads_campaign. Creating one cannot spend and cannot serve — it is a definition, so it is not confirm-gated.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | display name of the conversion event |
| eventType | string | yes | e.g. order_created, lead_created, registration_completed — the plausible words "purchase", "lead" and "signup" are all REFUSED by ChatGPT Ads |
| sourceIds | array | yes | exactly ONE source id from create_openai_ads_pixel (ChatGPT Ads takes one source per event; create one event per pixel) |
| customEventName | string | no | for a non-standard event |
| attributionWindowDays | number | no | default 30, which is what OpenAI recommends; 1-90 |
List the custom audiences on the connected ChatGPT Ads account. Pass intendedUse to see only the ones eligible for exclusion, inclusion or a bid multiplier (inclusion and bid multipliers need roughly 25,000 matched users). Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| limit | number | no | max audiences to return |
| intendedUse | string | no | only audiences eligible for this use |
Create a ChatGPT Ads custom audience from a customer list. Pass plain emails and/or phone numbers: Hermoso NORMALISES AND SHA-256 HASHES THEM LOCALLY and uploads only the digests, so no plaintext personal data leaves Hermoso. OR pass fileUrl: a public link to a UTF-8 .csv or .txt customer list (a CSV needs a header naming its column: email, phone_number, email_sha256, phone_number_sha256 or gaid;
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | audience name |
| members | array | no | emails and/or phone numbers (already-SHA256-hashed emails are passed through as-is); or use fileUrl |
| fileUrl | string | no | public link to a UTF-8 .csv or .txt customer list; upload_file turns a local file into one |
| fileName | string | no | the file name with .csv or .txt, when the link hides it |
| identifierType | string | no | the one identifier the file holds (inferred from a CSV header when omitted) |
| identifierResolution | string | no | auto = read every identifier column of a mixed CSV |
| description | string | no | optional audience description |
Read one ChatGPT Ads custom audience: its processing status, how many users matched, what it can actually be USED for, and its membership revision. Read-only, free. STATUS is not the same question as eligibility — "ready" only means processing succeeded. EXCLUSION (suppression) has NO minimum size, but INCLUSION and bid multipliers need roughly 25,000 matched users, so a small list used for inclus
| Parameter | Type | Required | Notes |
|---|---|---|---|
| audienceId | string | yes | ChatGPT Ads audience id, from list_openai_ads_audiences |
Change who is IN a ChatGPT Ads custom audience. Pass plain emails, phone numbers and/or Google Advertising IDs (GAID, UUID-shaped) in ONE list — identifier types may be mixed in a single request: Hermoso normalises and SHA-256 hashes emails and phones locally and only the digests are sent; a GAID rides raw, as the API defines it. SIZE RULES (OpenAI, read 2026-09-03): an audience of ANY size, even
| Parameter | Type | Required | Notes |
|---|---|---|---|
| audienceId | string | yes | ChatGPT Ads audience id, from list_openai_ads_audiences |
| operation | string | yes | add | remove | replace — replace swaps the entire membership |
| members | array | yes | emails and/or phone numbers (already-SHA256-hashed emails pass through as-is) |
| expectedRevision | number/string | no | REQUIRED for replace, optional for add/remove — membershipRevision from get_openai_ads_audience |
List the membership operations (add, remove, replace) ChatGPT Ads has accepted on one custom audience, each with its status. Use it to find an operation whose id was lost, or to see what is still pending, before get_openai_ads_audience_operation, cancel_openai_ads_audience_operation or resume_openai_ads_audience_operation. Read-only, free. Pages with limit (1-100, default 25) and cursor.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| audienceId | string | yes | ChatGPT Ads audience id whose operations to list |
| limit | number | no | 1-100, default 25 |
| cursor | string | no | nextCursor from the previous page |
Cancel an accepted ADD or REMOVE on a ChatGPT Ads custom audience BEFORE its membership changes begin. ChatGPT Ads refuses once the operation is already applying changes, and that refusal is reported as it came. Free. The reply is the status read back after the cancel, never the request alone.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| audienceId | string | yes | audience the operation belongs to |
| operationId | string | yes | operation id returned by update_openai_ads_audience_members |
Resume an existing ADD or REMOVE on a ChatGPT Ads custom audience from its own checkpoints with its original inputs, so nothing is uploaded again. Free. The reply is the status read back after the resume, never the request alone.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| audienceId | string | yes | audience the operation belongs to |
| operationId | string | yes | operation id of the stopped add or remove |
Poll an add/remove/replace submitted by update_openai_ads_audience_members until it reports succeeded or failed. Read-only, free. Until it succeeds the membership has NOT changed, so never report an audience update as done on the strength of the submission alone.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| audienceId | string | yes | audience the operation belongs to |
| operationId | string | yes | operation id returned by update_openai_ads_audience_members |
Combine 2 to 64 existing ChatGPT Ads custom audiences into ONE new audience. The SOURCE audiences are left unchanged, later updates to them do NOT propagate into the merged one, and no existing campaign switches to the new id by itself — re-point targeting deliberately if that is the intent. Creates a new audience; changes nothing that is already serving.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | name for the new merged audience, at least 3 characters |
| audienceIds | array | yes | 2 to 64 DISTINCT audience ids from list_openai_ads_audiences |
Retire a ChatGPT Ads custom audience. ARCHIVING IS PERMANENT AND THERE IS NO DELETE TO UNDO IT: an archived audience can never be restored, targeted or bid on again, and any campaign that includes or excludes it loses that audience. Without confirm:true this archives NOTHING and instead reports the audience’s real name, status and matched size read back from ChatGPT Ads, so the cost is visible bef
| Parameter | Type | Required | Notes |
|---|---|---|---|
| audienceId | string | yes | ChatGPT Ads audience id to archive; archiving is permanent |
| confirm | boolean | no | must be true to actually archive — this cannot be undone |
Build a campaign on the connected ChatGPT Ads account — the ads that appear below ChatGPT answers. ALWAYS created PAUSED at every level, with no override: it spends NOTHING until you activate it with set_openai_ads_status(confirm:true). The object graph is campaign -> ad group -> ad, and a campaign ON ITS OWN CANNOT SERVE AN IMPRESSION, so pass adGroup{name, contextHints, ad{creative}} and this bu
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | campaign name, at least 3 characters |
| description | string | no | optional campaign description |
| dailyBudget | number | no | daily cap in the AD ACCOUNT’S currency — ChatGPT Ads’ own minimum for a DAILY budget is 25.00 |
| lifetimeBudget | number | no | lifetime cap in the account currency — no 25.00 floor applies here, so use this to spend less than that in total. Pass this and/or dailyBudget; a budget is required. |
| businessAgentId | string | no | run a BUSINESS AGENT campaign (mode business_agent): the id from create_openai_ads_business_agent, which must be PUBLISHED |
| objective | string | no | the campaign objective. objective "conversions" with billingEventType "impression" is the impression-billed, conversion-optimised shape |
| billingEventType | string | no | what the campaign is billed on |
| landingPageQueryTemplate | string | no | query-string template appended to landing URLs (e.g. utm_source=chatgpt&utm_campaign={campaign_id}) — ChatGPT Ads’ landing_page_configuration.query_string_template |
| biddingType | string | no | default clicks (CPC). "conversions" is oCPC — you still pay per click, but ChatGPT Ads optimises toward a conversion event, and it REQUIRES conversionEventSettingIds naming exactly one active event se |
| countries | array | no | 2-letter country codes |
| locationIds | array | no | ids from openai_ads_geo_search — up to 2,500 |
| platforms | array | no | WHICH CHATGPT SURFACES THIS CAMPAIGN RUNS ON — OpenAI’s “Eligible platforms”: ios_app (the ChatGPT iOS app), android_app (the Android app), web (ChatGPT in ANY browser), or narrower browser targets de |
| customAudienceIds | array | no | TARGET a CUSTOM AUDIENCE — ids from list_openai_ads_audiences (created with create_openai_ads_audience, then filled with members). Until 2026-08-12 an audience could be created AND uploaded and then p |
| excludedCustomAudienceIds | array | no | EXCLUDE custom audiences — same ids, opposite effect (suppressing existing customers, say). An id in BOTH lists is refused rather than resolved by a guess, because OpenAI does not document which side |
| conversionEventSettingIds | array | no | REQUIRED when biddingType is "conversions" (oCPC): exactly one active conversion-event-setting id from list_openai_ads_conversion_events, the event ChatGPT Ads optimises toward. Ignored for clicks/imp |
| startTime | number | no | unix seconds |
| endTime | number | no | unix seconds |
| adGroup | object | no | build the ad group (and its ad) in the same call — a campaign alone cannot serve |
Add an ad group to an existing ChatGPT Ads campaign. Created PAUSED by default. Bidding: leave it out and the ad group gets INTELLIGENT BIDDING (OpenAI’s renamed “Maximize results”: maximize_clicks / maximize_conversions, no amount; needs a daily campaign budget). From 2026-11-09: bidStrategy target_cpa + targetCpa (conversions campaigns) or target_cpc + targetCpc (clicks campaigns). A fixed maxBi
| Parameter | Type | Required | Notes |
|---|---|---|---|
| campaignId | string | yes | campaign id to add the ad group to |
| name | string | yes | ad group name |
| description | string | no | optional ad group description |
| maxBid | number | no | DEPRECATED — Max Bid / Max CPC, retired by ChatGPT Ads on 2027-01-01: a fixed bid in the account currency (bidStrategy fixed_bid; on a conversions campaign it is the CPA bid). Needed only on an impres |
| bidStrategy | string | no | HOW THIS AD GROUP BIDS. OMIT IT (and maxBid) and a clicks or conversions campaign with a DAILY budget gets INTELLIGENT BIDDING — OpenAI’s new name for “Maximize results”: maximize_clicks / maximize_co |
| targetCpa | number | no | Target CPA in the account currency: the average cost per conversion OpenAI aims for (bidStrategy target_cpa, conversions campaign, from 2026-11-09). Billing stays per click. Not with maxBid. |
| targetCpc | number | no | Target CPC in the account currency: the average cost per click OpenAI aims for (bidStrategy target_cpc, clicks campaign, from 2026-11-09). Not with maxBid. |
| billingEvent | string | no | default click |
| contextHints | array | no | up to 2,000, deduplicated server-side |
| landingPageQueryTemplate | string | no | query string added to every landing page click, e.g. utm_source=chatgpt&utm_content={ad_id}. Placeholders: {campaign_id} {ad_group_id} {ad_id} {ad_account_id} {oppref}; oppref and olref cannot be para |
| audienceBidMultipliers | array | no | bid more or less for people in a custom audience (eligible ids: list_openai_ads_audiences with intendedUse bid_multiplier, about 25,000 matched users). On an update this REPLACES the list; [] removes |
| status | string | no | default paused |
| confirm | boolean | no | REQUIRED true to create this ACTIVE under a live campaign (real spend) |
Create the actual AD inside a ChatGPT Ads ad group — the object that carries the creative; a campaign or ad group alone shows nothing at all. The creative is a TEXT + IMAGE CARD: a title of 3–50 characters, body copy of 100 characters maximum, one landing page URL and one still image. THERE IS NO VIDEO FORMAT ON THIS PLATFORM — a video URL is refused outright, so never offer one. Created paused; c
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adGroupId | string | yes | ad group id to create the ad in |
| name | string | no | internal name — defaults to the title |
| creative | object | yes | title 3-50 characters, body up to 100, landing page URL and one still image; no video |
| landingPageQueryTemplate | string | no | query string added to every landing page click, e.g. utm_source=chatgpt&utm_content={ad_id}. Placeholders: {campaign_id} {ad_group_id} {ad_id} {ad_account_id} {oppref}; oppref and olref cannot be para |
| status | string | no | default paused |
| confirm | boolean | no | REQUIRED true to create this ACTIVE in a live ad group (real spend) |
Create or update up to 1,000 ChatGPT Ads campaigns, ad groups and ads in ONE asynchronous job. It builds CHAT-CARD campaigns only: its campaign.create has no product-feed mode, and OpenAI’s spreadsheet bulk creation of product-feed campaigns and ad groups with auto-generated ad templates (announced 2026-09-30) is an Ads Manager feature with no API. Build a product-feed campaign with create_openai_
| Parameter | Type | Required | Notes |
|---|---|---|---|
| operations | array | yes | 1–1000 operations: {operation_id, type, idempotency_key (creates) or target_resource_id (updates), input:{…}} |
| validateOnly | boolean | no | true = FREE dry run; nothing is created or changed |
| partialFailure | boolean | no | default true (independent operations continue after an error). false skips later operations after a failure and does NOT roll back what already completed. |
| idempotencyKey | string | no | request-level key that makes an uncertain retry safe. Reusing it with a DIFFERENT body is an error at OpenAI. To rerun a failed job, submit the same body with a NEW request-level key. |
Poll a ChatGPT Ads bulk job and read the result of EVERY operation in it. Reports the job status (pending, in_progress, completed, partially_failed, failed — the last three are terminal) plus a per-operation verdict: created, updated, validated, failed or skipped, each with the new resource id or the error. THE VERDICT COMES FROM THE OPERATIONS, NEVER FROM THE JOB STATUS — "the job finished" and "
| Parameter | Type | Required | Notes |
|---|---|---|---|
| jobId | string | yes | the id openai_ads_bulk returned |
| limit | number | no | 1–100 results per page, default 100 |
| after | string | no | the last operation_id from the previous page — only available once complete is true |
Update the PRICE, TITLE or AVAILABILITY of variants already in a ChatGPT Ads product feed — send only what changed instead of re-uploading the catalog. This is what stops a stale feed serving ads for out-of-stock items. Pass feedId and products:[{id, variants:[{id, title?, price?:{amount,currency}, availability?:{available|status}}]}], where products[].id is the PARENT product id and variants[].id
| Parameter | Type | Required | Notes |
|---|---|---|---|
| feedId | string | yes | the product feed already linked to this ad account |
| products | array | yes | [{id, variants:[{id, title?, price?:{amount:int minor units, currency}, availability?:{available:bool}|{status:"in_stock"|"out_of_stock"}}]}] |
EDIT an existing ChatGPT Ads object in place — rename it, change a campaign’s budget or geo targeting, rewrite an ad group’s context hints or bid, or replace an ad’s title, body, landing page or image. Pass level:"campaign" + campaignId, level:"adGroup" + adGroupId, or level:"ad" + adId. Only the fields you pass are changed, but note that context hints, bidding, TARGETING and the creative are REPL
| Parameter | Type | Required | Notes |
|---|---|---|---|
| level | string | no | inferred from which id you pass |
| campaignId | string | no | campaign id to edit; use with level campaign |
| adGroupId | string | no | ad group id to edit; use with level adGroup |
| adId | string | no | ad id to edit; use with level ad |
| name | string | no | new name |
| description | string | no | new description |
| dailyBudget | number | no | new daily budget in the account currency; campaign level |
| lifetimeBudget | number | no | new lifetime budget in the account currency; campaign level |
| countries | array | no | 2-letter country codes; replaces the whole geo list, so send the full list |
| locationIds | array | no | location ids from openai_ads_geo_search; replaces the whole list |
| endTime | number | no | unix seconds when the campaign stops; campaign level |
| platforms | array | no | REPLACES which ChatGPT surfaces the campaign runs on (ios_app / android_app / web, or the narrower browser targets desktop_web / ios_web / android_web; never web together with those, since web already |
| customAudienceIds | array | no | REPLACES the campaign’s targeted custom audiences. TARGETING IS REPLACED WHOLESALE, not merged — a patch that omits something the campaign already targets is REFUSED by name rather than silently dropp |
| excludedCustomAudienceIds | array | no | REPLACES the campaign’s excluded custom audiences — same wholesale rule as customAudienceIds. |
| conversionEventSettingIds | array | no | NOT CHANGEABLE: ChatGPT Ads refuses a new conversion event on an existing campaign ("cannot be modified after creation"). Passing it is refused with the way forward: build a new campaign with create_o |
| contextHints | array | no | REPLACES the existing list |
| maxBid | number | no | DEPRECATED — Max Bid / Max CPC, retired by ChatGPT Ads on 2027-01-01: a fixed bid in the account currency (bidStrategy fixed_bid; on a conversions campaign it is the CPA bid). Needed only on an impres |
| billingEvent | string | no | required alongside a maxBid — bidding is replaced wholesale; maximize_* and target_* are click-billed and need no billingEvent |
| bidStrategy | string | no | CHANGE HOW THIS AD GROUP BIDS. BIDDING IS REPLACED WHOLESALE, so a patch that moves the bid without restating the strategy would DEMOTE a maximize_* or target_* ad group, and one that sets a strategy |
| targetCpa | number | no | Target CPA in the account currency: the average cost per conversion OpenAI aims for (bidStrategy target_cpa, conversions campaign, from 2026-11-09). Billing stays per click. Not with maxBid. |
Change a ChatGPT Ads campaign’s budget — a daily cap, a lifetime cap, or both, in the ad account’s currency. A LIFETIME (total) budget is PACED by ChatGPT Ads to spread spend evenly across the campaign’s dates (actual daily spend still varies with available delivery), so a total budget is not a first-come-first-served pool. ChatGPT Ads’ own minimum for a DAILY budget is 25.00 (measured live 2026-0
| Parameter | Type | Required | Notes |
|---|---|---|---|
| campaignId | string | yes | campaign id whose budget changes |
| dailyBudget | number | no | daily cap in the account currency; ChatGPT Ads minimum is 25.00 |
| lifetimeBudget | number | no | lifetime cap in the account currency; no 25.00 minimum applies |
| confirm | boolean | no | REQUIRED true when the campaign is live |
ATTRIBUTED CONVERSIONS for ChatGPT Ads — the number the whole pixel + conversion-event setup exists to produce, beyond what openai_ads_report shows: that report carries conversions and CPA for the account, a campaign, ad group or ad, while this tool adds the click-through / view-through split, totals per id across many ids, and the received-event sample. Pass entityIds — the campaign / ad group /
| Parameter | Type | Required | Notes |
|---|---|---|---|
| level | string | no | inferred from which id you pass — default ad_account |
| entityIds | array | no | the campaign, ad group or ad ids to report on, required below the account level; omit for level ad_account, which sums every campaign |
| campaignId | string | no | campaign id to report on; sets the level to campaign |
| adGroupId | string | no | ad group id to report on; sets the level to ad_group |
| adId | string | no | ad id to report on; sets the level to ad |
| since | string | no | YYYY-MM-DD |
| until | string | no | YYYY-MM-DD |
| granularity | string | no | daily = one row per day per entity, plus a byDate total; default none = one total per entity |
| breakdown | string | no | split each row by device or by country |
| recentEvents | boolean | no | true = list a recent sample of the events OpenAI RECEIVED on the pixel instead of attributed totals |
| pixelId | string | no | with recentEvents: the pixel_id to read; omit to use the account's first pixel |
| limit | number | no | with recentEvents: how many events, 1-50 (default 50) |
| attributedEvents | boolean | no | default true; false = goal counts only |
| eventNames | array | no | only these events, e.g. ["order_created"] |
| attributionTimeBasis | string | no | default ad_event_time |
| clickWindowDays | no | default 30 | |
| viewWindowDays | no | default 1; 0 = no views | |
| includeZeroRows | boolean | no | default true |
Render a real preview of an existing ChatGPT Ads ad, so a human can LOOK at what will run instead of reading a list of ids back. Returns a hosted preview URL. THE LINK EXPIRES 24 HOURS AFTER IT IS CREATED — state that whenever you hand it to anyone, and generate a fresh one rather than re-sending an old one, because a dead link given to a client is worse than no link. Changes nothing about the ad;
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adId | string | yes | the ad to preview — list_openai_ads_campaigns with an adGroupId lists them |
Send conversion events to ChatGPT Ads from a SERVER (the Conversions API). USER MATCHING (schema of 2026-09-03): pass plaintext email / phone (with country code) / externalId / firstName / lastName and Hermoso hashes them locally, plus raw country / city / region / zip_code and gaid (Android advertising id); each becomes the API’s plural list (emails_sha256, phone_numbers_sha256, …), the first thr
| Parameter | Type | Required | Notes |
|---|---|---|---|
| pixelSnippetId | string | yes | OpenAI’s pixel_id — create_openai_ads_pixel returns it as pixelSnippetId. NOT the clidsrc_… pixelId, which is the conversion source id. |
| apiKey | string | yes | the Conversions API key (create_openai_ads_conversion_api_key) — NOT the Advertiser API key the connector stores |
| events | array | yes | up to 1000 events. Each: {id, type (order_created / lead_created / page_viewed / custom / …), timestamp_ms?, source_url? (required for web), action_source?, user? {email or externalId — Hermoso hashes |
| validateOnly | boolean | no | true validates the batch against ChatGPT Ads and SAVES NOTHING — no conversion is recorded |
| integrationSource | string | no | stable identifier for the integration sending the batch; defaults to "hermoso" |
Create a ChatGPT Ads Conversions API key — the credential a SERVER uses to send conversion events, which is the only way a conversion that happens off the page (an offline sale, a webhook, a mobile backend) can be counted at all. PERMANENT AND UNREPEATABLE, and both halves of that are why it is confirm-gated even though a key cannot spend: ChatGPT Ads publishes NO way to list, rotate or delete a k
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | a descriptive name, 3–1000 characters — it can never be listed or renamed afterwards |
| confirm | boolean | no | REQUIRED true — the key is permanent and its secret is shown once |
Update the ChatGPT Ads account’s names, primary URL and/or favicon. brandName is shown IN ADS, accountName is internal to Ads Manager, legalName is the business. Per OpenAI, a brand-name change PAUSES AD DELIVERY until brand review passes and a legal-name change restarts business review and may pause serving; an account name triggers no review. So a brand or legal change needs confirm:true: withou
| Parameter | Type | Required | Notes |
|---|---|---|---|
| brandName | string | no | shown in ads; pauses delivery until review (confirm) |
| accountName | string | no | internal; no review |
| legalName | string | no | restarts business review (confirm) |
| confirm | boolean | no | required for a brand or legal change |
| name | string | no | legacy: sets all three names |
| url | string | no | the account’s primary destination — a public http(s) URL. OpenAI accepts this field even though their published parameter table omits it. |
| faviconUrl | string | no | public image URL (or the brand home page) to upload and assign as the account favicon — minimum 128×128 |
| faviconFileId | string | no | an already-uploaded favicon file id, if you have one |
Turn a ChatGPT Ads AD ACCOUNT, campaign, AD GROUP or AD on (active), off (paused), or ARCHIVE it. Pass level:"account" (NO id — the connection IS the account), level:"campaign" + campaignId, level:"adGroup" + adGroupId, or level:"ad" + adId. LEVEL "account" IS THE EMERGENCY STOP AND BEHAVES DIFFERENTLY FROM EVERY OTHER LEVEL: BOTH directions take confirm:true, because pausing it stops ALL ChatGPT
| Parameter | Type | Required | Notes |
|---|---|---|---|
| level | string | no | inferred from which id you pass — default campaign. "account" is the whole ad account and takes no id. |
| campaignId | string | no | campaign id to change; use with level campaign |
| adGroupId | string | no | ad group id to change; use with level adGroup |
| adId | string | no | ad id to change; use with level ad |
| status | string | yes | active = start spending; paused = stop; archived = permanent (not available at level "account") |
| confirm | boolean | no | REQUIRED true to activate (real spend), to archive (irreversible), or for EITHER direction at level "account" |
| confirmName | string | no | when archiving or removing: its EXACT name, as the unconfirmed call names it. Required when it has children or is live |
| confirmChildren | number | no | when archiving or removing: the exact number of children the unconfirmed call reported. Required when it has any |
Retire a ChatGPT Ads campaign, ad group or ad. THE OPENAI ADVERTISER API HAS NO DELETE — archiving is its only teardown, and OpenAI’s own guidance is "only archive objects you have no further use for, as archiving isn’t reversible": there is no un-archive, not even through support. So say ARCHIVED, never "deleted". Pass level:"campaign" + campaignId, level:"adGroup" + adGroupId, or level:"ad" + ad
| Parameter | Type | Required | Notes |
|---|---|---|---|
| level | string | no | what to archive — default campaign |
| campaignId | string | no | campaign id to archive; use with level campaign |
| adGroupId | string | no | ad group id to archive; use with level adGroup |
| adId | string | no | ad id to archive; use with level ad |
| confirm | boolean | no | REQUIRED true — archiving cannot be undone |
| confirmName | string | no | the object’s EXACT name, required when it has children or is live |
| confirmChildren | number | no | the exact number of children reported by the unconfirmed call, required when it has any |
The ChatGPT Ads LEAD FORMS on this ad account — id, status (draft/published/archived), draft and published revision ids, fields. A lead form is what a Business Agent uses to collect a name/email/choice inside the chat; it only collects once PUBLISHED, and leads only arrive somewhere once a lead-sync subscription exists (list_openai_ads_lead_sync). Read-only, 0 credits. Needs ChatGPT Ads connected.
No parameter schema on file.
Draft a ChatGPT Ads LEAD FORM: a name, an optional privacy policy URL, and fields — each {fieldId, fieldType "text" | "choice", label, required, options (choice only)}. Created as a DRAFT with a draftRevisionId; publish_openai_ads_lead_form makes it live, then reference it from a Business Agent (leadFormId). Needs ChatGPT Ads connected.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | lead form name |
| privacyPolicyUrl | string | no | privacy policy URL shown with the form |
| fields | array | yes | form questions, in the order shown |
One ChatGPT Ads lead form with its fields and revision ids (pass revisionId to read a specific revision). Read-only, 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| leadFormId | string | yes | lead form id, from create_openai_ads_lead_form or the account |
| revisionId | string | no | read this revision instead of the latest one |
Save a NEW DRAFT REVISION of a ChatGPT Ads lead form (the whole definition: name + fields are replaced). expectedDraftRevisionId must be the draftRevisionId you last read — ChatGPT Ads refuses a stale one so two editors cannot clobber each other. The published version is untouched until publish_openai_ads_lead_form.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| leadFormId | string | yes | lead form id to edit |
| expectedDraftRevisionId | string | yes | draftRevisionId from the last read; refused if stale |
| name | string | yes | lead form name; the whole definition is replaced |
| privacyPolicyUrl | string | no | privacy policy URL shown with the form |
| fields | array | yes | all form questions in order; replaces the whole field list |
Publish the current draft of a ChatGPT Ads lead form so Business Agents can use it. expectedDraftRevisionId is the draftRevisionId you last read. Show the user the fields before publishing: the form is what real people will see.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| leadFormId | string | yes | lead form id to publish |
| expectedDraftRevisionId | string | yes | draftRevisionId from the last read; refused if stale |
Archive a ChatGPT Ads lead form. Not reversible through the API, and refused by ChatGPT Ads while a PUBLISHED Business Agent references it. confirm:true is required; without it nothing changes and the reply says what would.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| leadFormId | string | yes | lead form id to archive |
| confirm | boolean | no | true to archive; archiving cannot be reversed through the API |
Queue an explicitly SYNTHETIC, signed test lead for a PUBLISHED lead form to the account’s lead-sync webhook, so the receiving system can be checked end to end. expectedPublishedRevisionId is the form’s publishedRevisionId. The payload is marked synthetic; never count it as a lead.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| leadFormId | string | yes | published lead form id |
| expectedPublishedRevisionId | string | yes | publishedRevisionId of the form, from get_openai_ads_lead_form |
The lead-sync subscription(s) on this ChatGPT Ads account — the webhook that receives every lead a lead form collects. With none, leads have nowhere to go. Read-only, 0 credits.
No parameter schema on file.
Provision lead delivery for this ChatGPT Ads account. OMIT destinationUrl to deliver to HERMOSO: leads land in Hermoso’s own signed receiver, list_openai_ads_leads reads them and the HubSpot lead sync can push them into HubSpot (no secret to store). Or pass your own https destinationUrl (a managed webhook), optionally with your own signingSecret; then THE SIGNING SECRET IS RETURNED ONCE — every le
| Parameter | Type | Required | Notes |
|---|---|---|---|
| destinationUrl | string | no | omit to deliver the leads to Hermoso |
| signingSecret | string | no | your own webhook signing secret; only used with destinationUrl, returned once |
The ChatGPT Ads leads delivered to Hermoso for this brand, newest first: each lead's answers keyed by the form's question labels (and by email / phone / full_name), the form id, the campaign / ad group / ad ids, submittedAt. ChatGPT Ads has no endpoint that LISTS leads — they exist here only after set_openai_ads_lead_sync was called WITHOUT a destinationUrl, and only from then on. Synthetic test s
| Parameter | Type | Required | Notes |
|---|---|---|---|
| formId | string | no | return only leads from this lead form id |
| since | string | no | ISO date |
| limit | number | no | default 100, max 1000 |
| includeSynthetic | boolean | no | true = include synthetic test leads, which are hidden by default |
Delete the lead-sync subscription: every lead form on the account stops delivering leads. confirm:true required.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| subscriptionId | string | yes | lead-sync subscription id, from the subscriptions list of list_openai_ads_leads |
| confirm | boolean | no | true to delete; every lead form on the account stops delivering leads |
The BUSINESS AGENTS on this ChatGPT Ads account — a Business Agent is a branded assistant ChatGPT serves inside an ad (mode business_agent on the campaign): instructions, conversation starters, optional product feeds, tools and a lead form. Shows draft/published status and pending changes. Read-only, 0 credits.
No parameter schema on file.
The eligible TOOLS installed for this ChatGPT Ads account that a Business Agent can be given (pass their ids as toolIds to create_openai_ads_business_agent). Read-only, 0 credits.
No parameter schema on file.
Draft a ChatGPT Ads BUSINESS AGENT: name, instructions (the system prompt real users will experience — write it as the brand), optional description, privacy policy URL, conversation starters, product feed ids, connector ids, tool ids (list_openai_ads_business_agent_tools) and a published lead form id. Pass businessAgentId to REPLACE an existing agent’s configuration (it becomes a pending change).
| Parameter | Type | Required | Notes |
|---|---|---|---|
| businessAgentId | string | no | replace THIS agent’s configuration instead of creating a new one |
| name | string | yes | business agent name |
| instructions | string | yes | system prompt that users experience, written in the brand's voice |
| description | string | no | optional description of the agent |
| privacyPolicyUrl | string | no | privacy policy URL shown with the agent |
| conversationStarters | array | no | suggested opening questions shown to users |
| productFeedIds | array | no | product feed ids the agent may draw on, from list_openai_ads_feeds |
| connectorIds | array | no | connector ids the agent may use |
| toolIds | array | no | tool ids the agent may use, from list_openai_ads_business_agent_tools |
| leadFormId | string | no | published lead form id the agent collects leads with |
One Business Agent with its full configuration and publish state. Read-only, 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| businessAgentId | string | yes | business agent id |
Send a message (or a whole messages[] transcript) to a ChatGPT Ads Business Agent and get its reply — a PREVIEW that publishes and serves nothing. Use it to test instructions before publish_openai_ads_business_agent.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| businessAgentId | string | yes | business agent id to chat with |
| message | string | no | one user message to send; use messages for a full transcript |
| messages | array | no | full transcript as [{role, content}], in place of message |
Publish a Business Agent’s draft so every campaign referencing it serves this version to real users. confirm:true required; show the user the instructions first.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| businessAgentId | string | yes | business agent id to publish |
| confirm | boolean | no | true to publish; campaigns serve this version to real users |
Account-level SPENDING LIMITS on ChatGPT Ads, both kinds: the date-range SPEND-LIMIT WINDOWS (a ceiling on what the whole account may spend between an inclusive start date and an exclusive end date, with amount and spent so far) and the DAILY LIMIT (a per-day ceiling that renews at midnight in the account timezone, with what is spent and left today). Also returns the configuration revision that
No parameter schema on file.
Create (no windowId) or edit (windowId) an account-level spend-limit window on ChatGPT Ads: startDate YYYY-MM-DD inclusive, endDate YYYY-MM-DD EXCLUSIVE (the window ends the day before), amount in the account currency (sent as micros), optional name and insertion-order id (ioId). A window is a CEILING on what the whole account may spend in that range; it never makes anything spend, and campaign bu
| Parameter | Type | Required | Notes |
|---|---|---|---|
| windowId | string | no | spend-limit window id to edit; omit to create a new window |
| startDate | string | no | YYYY-MM-DD, first day of the window (inclusive) |
| endDate | string | no | YYYY-MM-DD, end of the window (exclusive; the window ends the day before) |
| amount | number | no | spend ceiling for the whole account over the window, in the account currency |
| name | string | no | window name |
| ioId | string | no | insertion-order id, when billing uses one |
Delete an active or scheduled spend-limit window before it ends — this REMOVES a spending ceiling. confirm:true required.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| windowId | string | yes | spend-limit window id to delete; removes that spending ceiling |
| confirm | boolean | no | true to delete the window |
Create or change the ACCOUNT-WIDE DAILY SPENDING LIMIT on ChatGPT Ads: the most all campaigns together may spend each day, renewing at midnight in the account timezone, until it is removed or reaches its optional end date. OpenAI offers it only on ad accounts with postpaid INVOICE billing and it needs billing-management permission; a card-billed account is refused whatever key is used. amount is p
| Parameter | Type | Required | Notes |
|---|---|---|---|
| amount | number | yes | daily limit in the account currency; 0 stops all account spend |
| windowId | string | no | the daily rule to change, from list_openai_ads_spend_windows; required when the account has more than one |
| newRule | boolean | no | true adds another non-overlapping daily rule instead of editing one |
| expectedRevision | number | no | the revision from list_openai_ads_spend_windows |
| startDate | string | no | YYYY-MM-DD, first day of a NEW daily limit (tomorrow or later) |
| endDate | string | no | YYYY-MM-DD, exclusive |
| noEndDate | boolean | no | true removes the end date |
| confirm | boolean | no | true applies the change; needs expectedRevision as well |
Remove the account-wide DAILY spending limit on ChatGPT Ads. This takes a spending CEILING off the whole account, so campaigns may then spend up to their own budgets every day; date-range spend-limit windows are not touched. Like every account spending-limit request it needs postpaid INVOICE billing and billing-management permission; a card-billed account is refused whatever key is used. Without c
| Parameter | Type | Required | Notes |
|---|---|---|---|
| windowId | string | no | the daily rule to remove; required when the account has more than one |
| expectedRevision | number | no | the revision from list_openai_ads_spend_windows |
| confirm | boolean | no | true removes the limit; needs expectedRevision as well |
CURRENTLY UNAVAILABLE: OpenAI no longer serves account-level negative keywords. The live API answers "Invalid URL" (measured 2026-09-13) and the endpoint left its published ChatGPT Ads spec the same week, so nothing can be set through Hermoso or any other API client. Tell the user that plainly instead of retrying. When it did work it REPLACED the whole account list, and the API never offered a way
| Parameter | Type | Required | Notes |
|---|---|---|---|
| keywords | array | yes | negative keywords; currently unavailable because OpenAI no longer serves this endpoint |
The PRODUCT FEEDS on this ChatGPT Ads account (id, name, countries, currencies, the count of ingested products eligible for ads, and campaign count). A product-feed campaign (mode product_feed) advertises products from one of these; update_openai_ads_feed_products fills one. Read-only, 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| limit | number | no | max feeds to return |
| after | string | no | pagination cursor from the previous page |
Create an EMPTY product feed on ChatGPT Ads with a name and the countries it serves (ISO codes). Fill it with update_openai_ads_feed_products, or set up SFTP bulk delivery with set_openai_ads_feed_sftp. inventoryType:"hotel" creates a HOTEL property feed instead (schema hotel_property_v1, filled with a full property snapshot over SFTP; an OpenAI limited beta for approved pilot advertisers, so an a
| Parameter | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | product feed name |
| countries | array | no | ISO country codes the feed serves, e.g. US |
| inventoryType | string | no | default product; hotel = a Hotel property feed (limited beta) |
Recent product-feed UPLOADS across the account with per-upload status, rows accepted / rejected / ads-eligible and OpenAI’s DIAGNOSTICS (code, severity, field and rows affected, e.g. invalid_value on price) — the way to learn why products are not serving. Per-product review status (why ONE product was rejected) is shown only in ChatGPT Ads Manager > Products: OpenAI does not expose it through the
| Parameter | Type | Required | Notes |
|---|---|---|---|
| limit | number | no | max uploads to return |
Archive a product feed — its products leave every campaign using it. confirm:true required.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| feedId | string | yes | product feed id to archive |
| confirm | boolean | no | true to archive; its products leave every campaign that uses it |
List the products in a feed that match filters ({field, operator in|not_in|gt|gte|lt|lte|contains|not_contains|starts_with, values[]}) — the SAME filter shape an ad group’s productSet takes, so this previews exactly which products that ad group would advertise. Each product carries its price and filter values. It does not carry a per-product review status: that is only in ChatGPT Ads Manager > Pro
| Parameter | Type | Required | Notes |
|---|---|---|---|
| feedId | string | yes | product feed id to query |
| filters | array | no | product filters; same shape as an ad group's productSet |
| limit | number | no | max products per page |
| after | string | no | pagination cursor from the previous page |
Whether SFTP bulk delivery is enabled for a ChatGPT Ads product feed, its connection URI and authentication method. Read-only, 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| feedId | string | yes | product feed id |
Manage SFTP bulk delivery for a ChatGPT Ads product feed (the alternative to pushing products through update_openai_ads_feed_products). action "create" (default): create or REPLACE the feed's SFTP credentials — authenticationMethod "password" (OpenAI returns the password ONCE in this reply; store it) or "ssh_key" (pass the public key as sshPublicKey). Replacing credentials invalidates the previous
| Parameter | Type | Required | Notes |
|---|---|---|---|
| feedId | string | yes | product feed id |
| action | string | no | create (default) sets up or rotates credentials; activate or pause switches delivery only |
| authenticationMethod | string | no | password (returned once in the reply) or ssh_key |
| sshPublicKey | string | no | SSH public key, used with authenticationMethod ssh_key |
A product feed’s name, countries and whether its organic ChatGPT search listing respects each product’s is_eligible_search flag. Read-only, 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| feedId | string | yes | product feed id |
Rename a product feed and/or set searchEligibleRespected (its organic ChatGPT search listing follows each product’s is_eligible_search flag). Touches no ads or spend; the reply is what OpenAI stored.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| feedId | string | yes | product feed id to edit |
| name | string | no | new feed name |
| searchEligibleRespected | boolean | no | true = the organic ChatGPT listing follows each product's is_eligible_search flag |
Who changed what on the ChatGPT Ads account and when: each create/update with its actor and every changed field’s before and after value. Read it first when a campaign starts or stops spending unexpectedly. Filter by campaignId (includes its ad groups and ads), adGroupId, adId, actorId, since/until (date, ISO time or unix seconds). Read-only, 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| campaignId | string | no | include changes to this campaign and its ad groups and ads |
| adGroupId | string | no | only changes to this ad group |
| adId | string | no | only changes to this ad |
| actorId | string | no | only changes made by this actor id |
| since | string | no | start of the window: date, ISO time or unix seconds |
| until | string | no | end of the window: date, ISO time or unix seconds |
| order | string | no | asc or desc, by time |
| limit | number | no | 1-100, default 25 |
| after | string | no | pagination cursor for the next page |
| before | string | no | pagination cursor for the previous page |
The saved evidence from OpenAI’s landing-page crawler for an ad’s submitted version: crawl status and failure code, the requested and final URL (a redirect shows here), capture time, whether robots.txt allowed the crawler and its user agent, and a screenshot of what it saw. Read it when an ad is rejected or reports a landing-page crawl serving issue. The screenshot link expires within 24 hours. av
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adId | string | yes | the ad, e.g. ad_… from list_openai_ads_campaigns with an adGroupId |
Delivery per HOTEL PROPERTY for a ChatGPT Ads Hotel feed: impressions, clicks, spend, CTR, CPC and CPM per property (hotel id, name, brand, city, star rating), optionally per day or month and broken down by campaign, ad group or ad; timeseries:true returns daily totals across the selected properties instead. Filter by campaignIds / adGroupIds / adIds, exact properties [{feedId, hotelId}], or filte
| Parameter | Type | Required | Notes |
|---|---|---|---|
| since | string | yes | YYYY-MM-DD, account timezone |
| until | string | yes | YYYY-MM-DD, account timezone, no later than today |
| timeseries | boolean | no | true = daily totals across the selected properties |
| granularity | string | no | none (one total per property), daily or monthly rows |
| breakdown | string | no | expand each property into hierarchy rows; each row repeats its property total under property |
| campaignIds | array | no | return only these campaign ids |
| adGroupIds | array | no | return only these ad group ids |
| adIds | array | no | return only these ad ids |
| properties | array | no | exact feed/property pairs |
| filters | array | no | filters on hotel attributes or metrics |
| includeZeroActivity | boolean | no | also list active properties with no delivery |
| sortField | string | no | metric to sort rows by |
| sortDirection | string | no | asc or desc |
| limit | number | no | property/time-bucket groups per page, up to 2000 |
| after | string | no | pagination cursor for the next page |
| includeTotalCount | boolean | no | true to return the total row count |
Read the brand’s connected Pinterest AD account(s). Call with NO adAccountId to list the ad accounts shared with this profile — do this first to pick a target. Call WITH adAccountId to list that account’s campaigns (id, name, status, objective, budget, and Pinterest’s own summary status). Add campaignId to get that ONE campaign’s whole tree — its AD GROUPS and ADS with their ids, statuses and revi
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to list the ad accounts shared with this profile |
| campaignId | string | no | one campaign -> its ad groups and ads too (the only way to enumerate them) |
| statuses | array | no | filter by ACTIVE, PAUSED, ARCHIVED or DRAFT; omit to include all four |
Performance for a Pinterest ad account — spend, impressions, clicks, CTR, effective CPC and conversions — at ANY of Pinterest’s four levels: the whole ACCOUNT, by CAMPAIGN, by AD GROUP, or by individual AD. Ad level is how you answer "WHICH AD IS WINNING". EVERY SUB-ACCOUNT LEVEL NEEDS ITS IDS — this was measured, not read: Pinterest refuses campaign level without campaignIds, ad-group level witho
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id; omit a list call to see the shared accounts |
| level | string | no | which level to report at — omit and it is inferred from the ids you pass (none -> the whole account) |
| campaignIds | array | no | REQUIRED for campaign level; also the second half of the ad-level pinIds pair |
| adGroupIds | array | no | REQUIRED for ad-group level — Pinterest has no all-of-them form there |
| adIds | array | no | REQUIRED for ad level — Pinterest refuses /ads/analytics without it, unless you pass pinIds AND campaignIds instead |
| pinIds | array | no | ad level only, and only TOGETHER with campaignIds — every ad promoting these Pins |
| since | string | no | YYYY-MM-DD, default 30 days ago |
| until | string | no | YYYY-MM-DD, default today |
| granularity | string | no | default TOTAL |
| columns | array | no | Pinterest metric column names — omit for the standard set for that level |
Build a campaign on a connected Pinterest ad account. ALWAYS created PAUSED — worth knowing that Pinterest’s own API defaults new campaigns to ACTIVE, so this deliberately overrides that; it spends NOTHING until you activate it with set_pinterest_ads_status(confirm:true). Pinterest’s object graph is campaign -> ad group -> ad, and an ad PROMOTES AN EXISTING PIN, so a campaign ON ITS OWN CANNOT SER
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id, from list_pinterest_ads_campaigns |
| name | string | yes | campaign name, ≤255 characters |
| objective | string | yes | Pinterest requires an objective and will not guess one |
| dailyBudget | number | no | daily cap in the ad account’s currency |
| lifetimeBudget | number | no | lifetime cap instead of a daily one — Pinterest then requires endTime |
| isPerformancePlus | boolean | no | Pinterest Performance+ on this campaign; with no lifetime budget Pinterest also requires isFlexibleDailyBudgets:true |
| isAutomatedCampaign | boolean | no | Pinterest automated campaign |
| isTopOfSearch | boolean | no | top-of-search campaign |
| isFlexibleDailyBudgets | boolean | no | flexible daily budgets |
| isLtvOptimized | boolean | no | optimize for long-term value |
| isCampaignBudgetOptimization | boolean | no | default true at Pinterest: the budget sits on the campaign |
| intendedPromotionType | string | no | the kind of ads it will carry |
| bidOptions | object | no | Pinterest bid_options object: multipliers by age, gender, placement, audience or frequency |
| startTime | number | no | Unix timestamp in SECONDS |
| endTime | number | no | Unix timestamp in SECONDS |
| adGroup | object | no | build the serving tree in the same call — WITHOUT this you get a campaign shell that can never show an ad |
Add an ad group to an existing Pinterest campaign — optionally with its ad in the same call. Created PAUSED by default. Pinterest REQUIRES every ad group to target at least one place, so targetingSpec must carry a GEO array or a LOCATION object. If the parent campaign is already LIVE (ACTIVE), creating this ad group ACTIVE starts REAL AD SPEND on the next auction, exactly like activating it — show
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id, from list_pinterest_ads_campaigns |
| campaignId | string | yes | the campaign this ad group belongs to |
| name | string | yes | ad group name |
| billableEvent | string | no | LEAVE THIS OUT unless you know better — Pinterest ties it to the campaign objective and refuses a mismatch: CONSIDERATION takes CLICKTHROUGH; AWARENESS, SALES, LEADS, WEB_CONVERSION, VIDEO_COMPLETION |
| bid | number | no | REQUIRED — what you pay per billable event, in the ad account’s currency. Pinterest rejects an ad group without one and Hermoso will not invent a bid. It must also be BELOW the campaign budget and abo |
| budget | number | no | ad-group budget — only valid when the campaign is NOT budget-optimized (Pinterest optimizes at campaign level by default) |
| placementGroup | string | no | where the ads run: ALL, SEARCH, BROWSE or OTHER |
| pacing | string | no | delivery speed: STANDARD or ACCELERATED |
| targetingSpec | object | no | Pinterest targeting object, e.g. {"GEO":["US"],"AGE_BUCKET":["25-34","35-44"]} — at least one GEO or LOCATION is REQUIRED by Pinterest. Age: use AGE_BUCKET, or MINIMUM_AGE and MAXIMUM_AGE TOGETHER (18 |
| status | string | no | default PAUSED |
| ad | object | no | create the ad in the same call |
| confirm | boolean | no | REQUIRED true to create this ACTIVE under a LIVE campaign |
Create the actual AD inside a Pinterest ad group — this is the object that carries the creative; a campaign or ad group alone shows nothing. A Pinterest ad PROMOTES AN EXISTING PIN, so pass pinId. Created PAUSED; if the parent ad group and campaign are both ACTIVE, creating this ACTIVE starts REAL AD SPEND immediately — get an explicit yes and pass confirm:true. Pinterest reviews ads: the read-bac
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id, from list_pinterest_ads_campaigns |
| adGroupId | string | yes | the ad group this ad lives in |
| pinId | string | yes | The numeric id of an existing Pin |
| creativeType | string | no | default REGULAR |
| name | string | no | internal ad name |
| destinationUrl | string | no | where the click goes |
| utm | object | no | UTM tags merged into destinationUrl as utm_source / utm_medium / utm_campaign / utm_term / utm_content — Pinterest has no UTM field of its own. Other query parameters and Pinterest macros like {campai |
| status | string | no | default PAUSED |
| confirm | boolean | no | REQUIRED true to create this ACTIVE in a live ad group |
EDIT an existing Pinterest ad group in place — its name, bid, budget, pacing, placement and, above all, its TARGETING. Before this, targeting was frozen the moment an ad group was created: a mistyped bid or a missing country meant building the whole tree again, and because PINTEREST HAS NO DELETE every correction left a permanent archived shell behind. This is a PARTIAL edit — a field you do not n
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id, from list_pinterest_ads_campaigns |
| adGroupId | string | yes | the ad group to edit |
| status | string | no | REFUSED HERE ON PURPOSE — use set_pinterest_ads_status, which confirm-gates real spend and Pinterest’s archive-as-delete |
| name | string | no | new ad group name |
| bid | number | no | what you pay per billable event, in the ad account’s currency |
| budget | number | no | only on a campaign that is NOT budget-optimized |
| budgetType | string | no | budget mode: DAILY, LIFETIME or CBO_ADGROUP |
| billableEvent | string | no | Pinterest ties this to the campaign objective and refuses a mismatch |
| placementGroup | string | no | where the ads run: ALL, SEARCH, BROWSE or OTHER |
| pacing | string | no | delivery speed: STANDARD or ACCELERATED |
| autoTargeting | boolean | no | true or false: Pinterest automatic targeting |
| targetingSpec | object | no | REPLACES the whole targeting spec — get ids from search_pinterest_ads_targeting, never guess one |
| trackingUrls | object | no | Pinterest tracking URL object for the ad group |
| targetingTemplateIds | array | no | Pinterest refuses these alongside targetingSpec / trackingUrls / autoTargeting / placementGroup |
| optimizationGoalMetadata | object | no | Pinterest optimization goal metadata object |
| startTime | number | no | Unix timestamp in SECONDS |
| endTime | number | no | Unix timestamp in SECONDS |
| lifetimeFrequencyCap | number | no | CPM (IMPRESSION-billed) ad groups only, and Pinterest requires endTime with it |
| campaignId | string | no | MOVE the ad group into a different campaign — it then spends from that budget |
| confirm | boolean | no | REQUIRED true to change bid / budget / campaign on a LIVE ad group |
EDIT an existing Pinterest ad in place — its name, DESTINATION URL, creative type, tracking URLs, deep links, lead form, or which ad group it sits in. A PARTIAL edit: a field you do not name is left exactly as Pinterest has it. THE PIN CANNOT BE SWAPPED ON A REAL AD — Pinterest documents pinId as updatable "only for draft ads", so to promote a different Pin create a new ad in the same ad group wit
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id, from list_pinterest_ads_campaigns |
| adId | string | yes | the ad to edit |
| status | string | no | REFUSED HERE ON PURPOSE — use set_pinterest_ads_status, which confirm-gates real spend and Pinterest’s archive-as-delete |
| name | string | no | new ad name |
| destinationUrl | string | no | where the click goes |
| utm | object | no | UTM tags merged into destinationUrl (or, if you omit destinationUrl, into the ad’s CURRENT destination) as utm_source / utm_medium / utm_campaign / utm_term / utm_content — Pinterest has no UTM field |
| creativeType | string | no | creative format, e.g. REGULAR, VIDEO, CAROUSEL or APP |
| adGroupId | string | no | move the ad into a different ad group |
| trackingUrls | object | no | Pinterest tracking URL object for the ad |
| clickTrackingUrl | string | no | click-through tracking URL |
| viewTrackingUrl | string | no | view-through tracking URL |
| leadFormId | string | no | Pinterest lead form id for a lead ad |
| iosDeepLink | string | no | iOS deep link for an app ad |
| androidDeepLink | string | no | Android deep link for an app ad |
| carouselDestinationUrls | array | no | one destination URL per carousel card, in card order |
| pinId | string | no | DRAFT ads only — Pinterest refuses it on any other status |
Change a Pinterest campaign’s budget — a DAILY cap or a LIFETIME cap, in the ad account’s currency. Pinterest allows only one of the two per campaign, so passing both is refused rather than silently picking one. Raising it on a LIVE (ACTIVE) campaign increases real spend immediately — you MUST show the user the new amount, get an explicit yes, then call with confirm:true. Read back after the chang
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id, from list_pinterest_ads_campaigns |
| campaignId | string | yes | the campaign whose budget changes |
| dailyBudget | number | no | daily cap in the ad account currency; use one budget type per campaign |
| lifetimeBudget | number | no | lifetime cap in the ad account currency; use instead of dailyBudget |
| confirm | boolean | no | REQUIRED true to change the budget of a LIVE campaign |
Turn a Pinterest campaign, AD GROUP or AD on (ACTIVE) or off (PAUSED) — and, because Pinterest has NO DELETE anywhere in its API, this is also the only way to retire one (ARCHIVED). Pass level:"campaign" + campaignId, level:"adGroup" + adGroupId, or level:"ad" + adId. ACTIVATING STARTS REAL AD SPEND, and ARCHIVING is effectively a delete: both require you to show the user exactly what changes, get
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id, from list_pinterest_ads_campaigns |
| level | string | no | what to change — default campaign |
| campaignId | string | no | campaign id to change; use with level campaign |
| adGroupId | string | no | ad group id to change; use with level adGroup |
| adId | string | no | ad id to change; use with level ad |
| status | string | yes | ACTIVE = start spending; PAUSED = stop; ARCHIVED = retire (Pinterest’s delete) |
| confirm | boolean | no | REQUIRED true for ACTIVE (real spend) or ARCHIVED (irreversible retirement) |
| confirmName | string | no | when archiving or removing: its EXACT name, as the unconfirmed call names it. Required when it has children or is live |
| confirmChildren | number | no | when archiving or removing: the exact number of children the unconfirmed call reported. Required when it has any |
Retire a Pinterest campaign, ad group or ad. PINTEREST API v5 HAS NO DELETE for any of the three — ARCHIVED is its terminal state, and Pinterest’s own campaign docs call an archived campaign "deleted" and say reversing it means filing a ticket with their customer ops team, so there is no un-archive you or the user can call. Say ARCHIVED, not "deleted": the object stays on the account with its repo
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| level | string | no | what to archive — default campaign |
| campaignId | string | no | campaign id to archive; used with level:"campaign" |
| adGroupId | string | no | ad group id to archive; used with level:"adGroup" |
| adId | string | no | ad id to archive; used with level:"ad" |
| confirm | boolean | no | REQUIRED true — archiving is not self-service reversible |
| confirmName | string | no | the object’s EXACT name, required when it has children or is live |
| confirmChildren | number | no | the exact number of children reported by the unconfirmed call, required when it has any |
Read the brand’s Pinterest CONVERSION TAGS — the Pinterest tag a WEB_CONVERSION or SALES campaign optimises on. view:"all" (default) lists every tag with status, when it last fired (a tag that has NEVER fired is not installed) and its code snippet; conversionTagId reads one tag in full; view:"ocpm_eligible" lists the (tag, event) pairs a conversion ad group can OPTIMISE for — put one in create_pin
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| conversionTagId | string | no | read one tag in full — code snippet and enhanced-match config |
| view | string | no | all (default) lists tags; ocpm_eligible lists (tag, event) goals; page_visit lists page-visit events |
| includeDeleted | boolean | no | true also includes deleted tags in the list |
Create a Pinterest conversion TAG on the ad account — the base code a website installs so Pinterest sees page visits, sign-ups and checkouts, and a WEB_CONVERSION / SALES campaign has something to optimise for. Returns the tag id and the code snippet to paste into every page’s <head>, READ BACK from Pinterest. enhancedMatch:true (or a per-field object) turns on Pinterest’s Automatic Enhanced Match
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| name | string | yes | tag name, as shown in Pinterest Ads Manager |
| enhancedMatch | no | true enables all Automatic Enhanced Match fields; or an object to enable individual fields |
Send conversions to Pinterest server-side — purchases, sign-ups, leads, add-to-carts or any of Pinterest’s 22 events — so Pinterest attributes them to the ads that caused them and conversion campaigns can optimise. Pass ordinary emails, phones and names: they are normalised Pinterest’s way and SHA-256 hashed on our server before they reach Pinterest. Every event needs something to match on: an ema
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| test | boolean | no | validate only — Pinterest records nothing |
| events | array | yes | up to 1,000 events |
List the Pinterest AUDIENCES on the ad account (what:"audiences", default) or its CUSTOMER LISTS (what:"customer_lists"), or read one by audienceId / customerListId. An audience id is what an ad group’s targetingSpec takes as {"AUDIENCE_INCLUDE":[id]} (retarget) or AUDIENCE_EXCLUDE (suppress). READY delivers; TOO_SMALL (under 100 people) never does. ownership:"RECEIVED" lists audiences shared with
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| what | string | no | audiences (default) or customer_lists |
| audienceId | string | no | read one audience in full by its id |
| customerListId | string | no | read one customer list in full by its id |
| ownership | string | no | OWNED or RECEIVED; RECEIVED lists audiences shared with this account |
Create a Pinterest AUDIENCE to retarget or suppress. audienceType: VISITOR (people who fired your Pinterest tag — conversionTagIds, optional event, retentionDays 1–540), ENGAGEMENT (people who engaged with your Pins — engagementDomains REQUIRED, your claimed domain; optional engagementType click/save/closeup/comment/like), ACTALIKE (people similar to seedAudienceIds — country US, CA or GB, percent
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| name | string | yes | audience name |
| audienceType | string | yes | audience kind; decides which other fields are required (see tool description) |
| description | string | no | optional note describing the audience |
| conversionTagIds | array | no | VISITOR: the tag(s) whose visitors to collect |
| event | string | no | VISITOR: pagevisit, signup, checkout, viewcategory, search, addtocart, watchvideo, lead, custom (or a partner-defined tag event) |
| retentionDays | number | no | VISITOR / ENGAGEMENT: 1–540, Pinterest default 180 |
| eventSources | array | no | web, mobile and/or offline: where the audience's events come from |
| ingestionSources | array | no | tag, mmp, file_upload and/or conversions_api: how events reach Pinterest |
| engagementDomains | array | no | ENGAGEMENT: REQUIRED — your claimed website domain |
| engagementType | string | no | click, save, closeup, comment or like; omit to include all engagements |
| engagerType | number | no | ENGAGEMENT: 1 or 2 |
| pinIds | array | no | ENGAGEMENT: only people who engaged with these Pins |
| seedAudienceIds | array | no | ACTALIKE: the audience(s) to find people similar to |
| country | string | no | ACTALIKE |
| percentage | number | no | ACTALIKE: 1 (most similar) to 10 (widest) |
| customerListId | string | no | CUSTOMER_LIST: id of an existing customer list to target |
| emails | array | no | CUSTOMER_LIST: upload these as a new list (hashed here) and make the audience in one call |
| maids | array | no | CUSTOMER_LIST: mobile ad ids to upload as a new list, hashed on our server |
| rule | object | no | raw Pinterest rule fields, merged last — expert use |
Rename a Pinterest audience, change its description or rule, or REMOVE it (remove:true). Removing is destructive — any ad group that includes or excludes it loses that targeting and Pinterest has no undo — so it needs confirm:true; without it nothing changes and the reply names what would go. Read back from Pinterest afterwards. 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| audienceId | string | yes | id of the audience to rename, edit or remove |
| name | string | no | new audience name |
| description | string | no | new audience description |
| rule | object | no | raw Pinterest rule fields merged onto the audience's existing rule (expert use) |
| remove | boolean | no | true removes the audience; requires confirm:true |
| confirm | boolean | no | true required together with remove:true |
Upload a Pinterest CUSTOMER LIST from emails (or mobile ad ids) — buyers, subscribers or leads — and, by default, make the CUSTOMER_LIST audience that targets it in the same call. Every address is SHA-256 hashed on our server; no plain address reaches Pinterest. Pinterest must find at least 100 of them on Pinterest or the list ends TOO_SMALL and cannot deliver. READ BACK from Pinterest. 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| name | string | yes | customer list name |
| emails | array | no | email addresses to upload; hashed on our server before sending |
| maids | array | no | mobile ad ids to upload; hashed on our server before sending |
| listType | string | no | IDFA or MAID for mobile ids; ignored when emails are sent (list becomes EMAIL) |
| createAudience | boolean | no | default true |
| audienceName | string | no | name of the CUSTOMER_LIST audience made from this list (when createAudience is on) |
ADD people to (or REMOVE people from) an existing Pinterest customer list by email or mobile ad id — hashed on our server before they leave it. The list’s counts are read back before and after. Pinterest matches in the background, so the audience size can take hours to move. 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| customerListId | string | yes | id of the customer list to change |
| operation | string | no | default ADD |
| emails | array | no | email addresses to add or remove (see operation); hashed on our server |
| maids | array | no | mobile ad ids to add or remove (see operation); hashed on our server |
Read the KEYWORDS on a Pinterest ad group or campaign: the searches a SEARCH placement serves on, each with its match type. Pass adGroupId or campaignId (neither lists the account’s own negative keywords). Read-only, 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| adGroupId | string | no | the ad group |
| campaignId | string | no | the campaign |
| matchTypes | array | no | only these match types |
| bookmark | string | no | the next page, from the last reply |
Add keywords to a Pinterest ad group, or archive them. action:"add" needs parentId (an ad group; a campaign takes only the _NEGATIVE match types) and keywords; Pinterest’s per-keyword refusals are reported. action:"archive" needs keywordIds; archiving is how Pinterest deletes a keyword. Read back from Pinterest. pinterest_ads_keyword_ideas finds candidates. 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| action | string | no | default add |
| parentId | string | no | add: the ad group, or a campaign for negative keywords |
| keywords | array | no | add: the keywords |
| keywordIds | array | no | archive: ids from list_pinterest_ads_keywords |
Estimate how many people a Pinterest ad group’s targeting can reach per month, before spending. Pass targetingSpec (e.g. {"GEO":["US"],"AGE_BUCKET":["25-34"]}), keywords and/or productGroupIds. A range from Pinterest’s historical data, not a promise. Read-only, 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| targetingSpec | object | no | Pinterest targeting object, the same one an ad group takes |
| keywords | array | no | keywords to size |
| placementGroup | string | no | default ALL |
| autoTargetingEnabled | boolean | no | default true at Pinterest |
| creativeTypes | array | no | e.g. REGULAR, VIDEO, SHOPPING |
| productGroupIds | array | no | product groups to size |
Pinterest’s BULK lane: many campaigns, ad groups, ads, keywords or product groups in one request. action:"download" exports them; action:"upsert" creates or updates them from create and update, in Pinterest’s own object shapes; action:"status" reads a request by requestId and gives the result file link. Download and upsert run in the background, so read status next. A created object with no status
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| action | string | no | default download |
| requestId | string | no | status: the id download or upsert answered |
| entityTypes | array | no | download: up to 6 types |
| outputFormat | string | no | download: default JSON |
| entityIds | array | no | download: only these ids |
| campaignFilter | object | no | download: Pinterest’s campaign_filter object |
| updatedSince | string | no | download: a Unix timestamp |
| create | object | no | upsert: objects to create by type, e.g. {"campaigns":[...],"ad_groups":[...],"keywords":[...]} |
| update | object | no | upsert: objects to change by type |
| confirm | boolean | no | upsert: required true when it starts delivery or changes a budget or bid |
Read Pinterest ad-account data. what: conversion_eqs (Event Quality Score of the conversion signals; lookback 1d or 14d, optional sourcePlatform, ingestionSource), metrics_ready_state (whether a date’s numbers are final; date YYYY-MM-DD), report_templates, promotions, targeting_templates. Read-only, 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| what | string | yes | which data |
| lookback | string | no | conversion_eqs: default 14d |
| sourcePlatform | string | no | conversion_eqs: e.g. WEB |
| ingestionSource | string | no | conversion_eqs: e.g. TAG, CONVERSIONS_API |
| date | string | no | metrics_ready_state: YYYY-MM-DD |
| searchQuery | string | no | targeting_templates: by name |
| includeSizing | boolean | no | targeting_templates: add the audience size |
| bookmark | string | no | the next page, from the last reply |
Save a Pinterest targeting set to reuse across ad groups, or remove one. action:"create" needs name and targetingSpec; "remove" needs templateId. Pinterest does not let an ordinary account edit a saved one, so a changed set is a new template. Read back from Pinterest. 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| action | string | no | default create |
| templateId | string | no | remove: from pinterest_ads_account_data what:targeting_templates |
| name | string | no | create: the template name |
| targetingSpec | object | no | Pinterest targeting object, e.g. {"GEO":["US"],"AGE_BUCKET":["25-34"]} |
| keywords | array | no | keywords to hold |
| placementGroup | string | no | create: default ALL |
| autoTargetingEnabled | boolean | no | create: default true at Pinterest |
Create, change or delete a Pinterest PROMOTION, the discount an ad group can show. action:"create" needs title and type (the kind of offer, which sets the text shown) and takes code, startTime, endTime (Unix seconds), templateValues (what fills the text), discountStatus, and any other Pinterest field in fields; "update" needs promotionId; "delete" needs promotionId and confirm:true. Read back from
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| action | string | no | default create |
| promotionId | string | no | update, delete: from pinterest_ads_account_data what:promotions |
| title | string | no | create: the internal name |
| type | string | no | create: the kind of offer |
| templateValues | array | no | what fills the offer text, at most 2, in Pinterest template_values shape |
| code | string | no | the code a buyer redeems |
| customId | string | no | your own id for it |
| discountStatus | string | no | Pinterest’s discount_status value |
| startTime | number | no | Unix seconds |
| endTime | number | no | Unix seconds |
| fields | object | no | any other promotion field, by Pinterest’s own name |
| confirm | boolean | no | delete: required true |
Run a saved Pinterest report template (pinterest_ads_account_data what:report_templates lists them) for an optional date range. It runs in the background and answers a token; pinterest_ads_async_report with that token gives the rows. 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| templateId | string | yes | the template id |
| startDate | string | no | YYYY-MM-DD |
| endDate | string | no | YYYY-MM-DD |
| granularity | string | no | default the template’s own |
Pinterest KEYWORD RESEARCH through the Pinterest Ads connection. mode:"related" (default): terms related to each of your terms ("workout" -> "yoga workout", "workout motivation"); mode:"suggested": popular searches that BEGIN with the term; mode:"metrics": Pinterest-wide search volume for up to 2,000 keywords in one country. Feeds ad-group KEYWORD targeting and Pin titles. Read-only, 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| mode | string | no | related (default), suggested (searches that begin with the term) or metrics (search volume) |
| terms | array | yes | keywords or seed terms; suggested mode uses the first one only |
| limit | number | no | suggested: 1–10 |
| countryCode | string | no | metrics: two-letter country, default US |
List the PRODUCT GROUPS being promoted in one Pinterest Shopping ad group — Pinterest lists them per ad group, so adGroupId is required — with status and bid. A CATALOG_SALES ad group with none has no products to serve. Read-only, 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| adGroupId | string | yes | Shopping ad group whose product-group promotions to list |
Attach catalog PRODUCT GROUPS to a Pinterest Shopping (CATALOG_SALES) ad group — this is what makes a Shopping ad group serve products. Created PAUSED by default; creating them ACTIVE under a LIVE campaign and ad group starts real spend, so that needs an explicit yes and confirm:true. Optional bid (ad account currency), included:false to exclude a group, creativeType, cta, trackingUrl. Read back f
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| adGroupId | string | yes | CATALOG_SALES ad group to attach the product groups to |
| productGroupIds | array | yes | catalog product group ids to promote |
| status | string | no | default PAUSED |
| bid | number | no | bid in the ad account currency |
| included | boolean | no | false excludes these product groups instead of including them |
| creativeType | string | no | SHOPPING, COLLECTION, CAROUSEL or MAX_VIDEO |
| cta | string | no | catalog sales CTAs: SHOP_NOW, BOOK_NOW, ON_SALE, GET_DEAL |
| trackingUrl | string | no | tracking URL appended to clicks on these promotions |
| confirm | boolean | no | true required to create ACTIVE promotions under a live campaign and ad group |
Change one Pinterest product-group promotion — status (ACTIVE / PAUSED / ARCHIVED), bid, included, trackingUrl. Activating it, or moving its bid, under a LIVE campaign and ad group changes real spend and needs confirm:true. Read back from Pinterest. 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| promotionId | string | yes | promotion id from list_pinterest_ads_product_group_promotions |
| status | string | no | ACTIVE, PAUSED or ARCHIVED |
| bid | number | no | new bid in the ad account currency |
| included | boolean | no | false excludes the product group from the ad group |
| trackingUrl | string | no | new tracking URL for clicks |
| confirm | boolean | no | true required when activating or changing the bid under a live campaign |
See a Pinterest AD before spending — Pinterest renders a preview link (valid 7 days, iframe-able) from an existing promotable Pin (pinId), from an image not yet pinned (imageUrl + title), or from a catalog product group (productGroupId, for a shopping ad). Creates no ad, spends nothing, 0 credits.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Pinterest ad account id — omit to use the single shared account |
| pinId | string | no | existing promotable Pin id to preview |
| imageUrl | string | no | public image URL to preview before it is pinned; needs title |
| title | string | no | required with imageUrl |
| productGroupId | string | no | catalog product group id, for a shopping ad preview |
| creativeType | string | no | pin: SHOPPING, COLLECTION, MAX_VIDEO, MAX_WIDTH_VIDEO_COLLECTION, MAX_WIDTH_REGULAR_COLLECTION; product group: SHOPPING, COLLECTION, CAROUSEL, MAX_WIDTH_COLLECTION |
| itemId | string | no | catalog item (product) id to preview within the product group |
Read the brand’s connected Reddit AD account(s). Call with NO adAccountId to list the ad accounts shared with this profile — do this first to pick a target. Call WITH adAccountId to read that account’s whole tree at once: campaigns, ad groups and ads, each with its configured status and Reddit’s own effective status (the effective one is what says whether it could actually serve — PENDING_APPROVAL
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_…) — omit to list the ad accounts shared with this profile |
Performance for a Reddit ad account — impressions, clicks, spend, CTR, CPC, eCPM, reach and any of Reddit’s ~450 metric fields, optionally broken down by campaign, ad group, ad, date, hour, community, country, gender, interest, keyword, placement and more. Money comes back in WHOLE UNITS of the ad account’s currency (Reddit reports micro-currency; the conversion is done for you) — and that currenc
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| since | string | no | YYYY-MM-DD or full ISO timestamp, default 30 days ago |
| until | string | no | YYYY-MM-DD or full ISO timestamp, default today |
| fields | array | no | Reddit metric names, UPPER_SNAKE (IMPRESSIONS, CLICKS, SPEND, CTR, CPC, ECPM, REACH, FREQUENCY, CONVERSION_ROAS, VIDEO_WATCHED_100_PERCENT, CONVERSION_PURCHASE_TOTAL_VALUE…). Omit for a sensible defau |
| breakdowns | array | no | up to 3 (4 if both COUNTRY and REGION). Omit for one aggregate row. |
| filter | string | no | Reddit filter expression to restrict rows to particular ids/values |
| timeZoneId | string | no | IANA zone, e.g. America/New_York |
List the Reddit PROFILES attached to an ad account. A Reddit ad promotes a POST, and every post is published AS one of these profiles — so this is the first call in any Reddit creative build, and its id is what create_reddit_ads_post needs. If it comes back empty, the ad account has no profile attached and nothing can be advertised from it yet. Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
Look up the exact values Reddit ad-group targeting expects, so none of them has to be guessed. kind:"communities" searches subreddits by keyword and returns each one’s NAME plus its subscriber count — targeting wants the bare name ("running"), NOT the t5_ id and NOT "r/running". kind:"community_suggestions" asks Reddit which communities fit some topics (query, comma-separated) and/or a websiteUrl
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| kind | string | no | default communities |
| query | string | no | keyword — required for communities; topics for community_suggestions; seeds for keywords; filters interests / devices / carriers / languages; searches cities for geolocations |
| keywords | array | no | keywords / keyword_check: the terms (up to 1,000, each ≤100 characters) |
| websiteUrl | string | no | community_suggestions: suggest communities related to this site |
| country | string | no | 2-letter country code, geolocations only |
| postalCode | string | no | postal code to narrow a geolocations search |
| limit | number | no | max results, default 15 |
Ask Reddit how big a targeting set is and roughly what a budget would buy — total reachable audience, the targeted slice, and estimated impressions, clicks and reach. Free, creates nothing, spends nothing, so run it BEFORE building an ad group to sanity-check targeting that may be far too narrow or far too broad. Budget is an ordinary amount in the ad account’s currency. These are Reddit’s estimat
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| budget | number | yes | budget in the ad account’s currency (not micro-currency — the conversion is handled) |
| objective | string | no | default CLICKS |
| goalType | string | no | DAILY_SPEND or LIFETIME_SPEND; LIFETIME_SPEND needs endTime |
| bidType | string | no | CPC, CPM, CPV, CPV6 or CPV15; must fit the objective |
| bidStrategy | string | no | BIDLESS, MANUAL_BIDDING, MAXIMIZE_VOLUME or TARGET_CPX |
| bidAmount | number | no | bid in the ad account currency; needed for paid bid types |
| startTime | string | no | ISO 8601 |
| endTime | string | no | ISO 8601 end of the forecast window |
| targeting | object | no | same shape as create_reddit_ads_ad_group targeting |
Reddit Campaign Planner’s forecast for a proposed campaign: the audience, projected reach, impressions, clicks, frequency and CPM/CPC/CPV for this budget, and a curve of reach at ten budgets to compare. Creates nothing and spends nothing. Use reddit_ads_forecast for a quick audience size. Reddit allows 30 of these a minute. These are Reddit’s estimates, not a guarantee, and they reserve nothing; s
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| objective | string | yes | campaign objective |
| goalType | string | yes | whether budget is per day or for the whole plan |
| budget | number | yes | budget in the ad account’s currency (not micro-currency) |
| startTime | string | no | ISO 8601; default now |
| endTime | string | no | ISO 8601; default 30 days after the start, at most 365 |
| bidType | string | no | CPC, CPM, CPV, CPV6 or CPV15 |
| bidStrategy | string | no | BIDLESS, MANUAL_BIDDING, MAXIMIZE_VOLUME or TARGET_CPX |
| bidAmount | number | no | bid in the ad account currency |
| useCatalog | boolean | no | SALES objective: true forecasts catalog sales |
| targeting | object | no | same shape as create_reddit_ads_ad_group targeting |
Reddit’s own suggested bid for a given objective, bid type and targeting — a median, a sensible range, and the hard floor below which Reddit will not accept a bid. Amounts come back in the ad account’s currency. Free, creates nothing. Use it to pick bidAmount for create_reddit_ads_ad_group instead of guessing a number that either never wins an auction or overpays.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| budget | number | yes | budget in the ad account’s currency |
| objective | string | no | default CLICKS |
| bidType | string | no | default CPC — must fit the campaign objective |
| bidStrategy | string | no | BIDLESS, MANUAL_BIDDING, MAXIMIZE_VOLUME or TARGET_CPX |
| goalType | string | no | DAILY_SPEND or LIFETIME_SPEND |
| startTime | string | no | ISO 8601 start of the flight to price |
| endTime | string | no | ISO 8601 end of the flight to price |
| currency | string | no | currency code, e.g. USD; returned amounts use the ad account currency |
| targeting | object | no | targeting block, same shape as create_reddit_ads_ad_group targeting |
List the POSTS on a Reddit profile — these are the creatives Reddit ads promote. Use it to find an existing post to advertise rather than creating a near-duplicate. Call list_reddit_ads_profiles first for redditProfileId. Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| redditProfileId | string | yes | the Reddit profile id (t2_…) from list_reddit_ads_profiles |
| type | string | no | TEXT, IMAGE, VIDEO or CAROUSEL; omit for all types |
| limit | number | no | page size, 1-100, default 25 |
Create the CREATIVE for a Reddit ad. This is the step people skip: a Reddit ad has no creative of its own — it points at a post — so a campaign and ad group with no post behind them can never serve. Types are TEXT (headline + body), IMAGE, VIDEO and CAROUSEL (up to 6 images), and an IMAGE, VIDEO or CAROUSEL post can carry body text too. For image/video/carousel pass media[] with a PUBLIC mediaUrl;
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| redditProfileId | string | yes | the Reddit profile id (t2_…) to publish as — from list_reddit_ads_profiles |
| jobId | string | no | PICK UP a post Reddit was still building: the jobId from an earlier pending answer. Creates nothing new; every other field is ignored. |
| type | string | no | default TEXT |
| headline | string | yes | the post title — this is the ad’s headline |
| body | string | no | body copy: the text of a TEXT post, or text shown with an IMAGE, VIDEO or CAROUSEL post. Up to 40,000 characters. |
| textFormat | string | no | how Reddit reads body: PLAIN_TEXT (the default) or RICH_TEXT_JSON, a Reddit rich-text JSON document serialized as a string. Cannot be changed after the post is created. |
| media | array | no | one entry for IMAGE/VIDEO, up to 6 for CAROUSEL |
| callToAction | string | no | applies to every media entry that has none |
| thumbnailUrl | string | no | required for VIDEO posts |
| allowComments | boolean | no | Reddit ads can carry a public comment thread — decide deliberately |
Turn comments ON or OFF on an existing Reddit ad post. THAT IS THE ONLY EDIT REDDIT ALLOWS: its post-update schema permits exactly one field, allow_comments, and REQUIRES it — headline and body both answer “Additional fields not permitted” once a post is published (measured live 2026-08-05). So a copy change is not an edit at all: create a new post with create_reddit_ads_post and point the ad at
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| postId | string | yes | the post id (t3_…) |
| allowComments | boolean | yes | REQUIRED — Reddit demands allow_comments on every post update, and it is the only field it permits |
Create the top tier of a Reddit ad — the campaign, which sets the OBJECTIVE everything under it optimises toward and (optionally) a lifetime spend cap. ALWAYS created PAUSED, with no override; it spends nothing until set_reddit_ads_status(confirm:true). Pick the objective deliberately, because the ad group’s bid type has to match it and it cannot be changed afterwards: CLICKS is Reddit’s name for
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_…) — omit when only one is shared |
| name | string | yes | campaign name |
| objective | string | no | default CLICKS — which is what Reddit calls website traffic |
| spendCapCents | number | no | lifetime spend ceiling for the whole campaign, in minor units of the ad account’s currency |
| useCatalog | boolean | no | SALES only: true makes it a product-catalog campaign (the new spelling of CATALOG_SALES) — every ad group under it must then use a catalog (create_reddit_ads_ad_group with productSetId + shoppingType; |
| isCampaignBudgetOptimization | boolean | no | CAMPAIGN BUDGET OPTIMIZATION (Reddit’s recommended structure): the budget and bidding live on the campaign and Reddit spreads the budget across its ad groups, which then inherit goal type, bid type an |
| budget | number | no | CBO only: the campaign budget in the ad account’s currency, paired with goalType |
| goalType | string | no | CBO only, default DAILY_SPEND; LIFETIME_SPEND needs endTime |
| startTime | string | no | CBO only: ISO 8601 |
| endTime | string | no | CBO only: ISO 8601, required for LIFETIME_SPEND |
| bidStrategy | string | no | CBO only (MANUAL_BIDDING is ad-group only) |
| bidType | string | no | CBO only, must fit the objective — CLICKS takes CPC |
| bidAmount | number | no | CBO only: target cost in the ad account’s currency — required for TARGET_CPX; when set here no ad group may set its own |
| optimizationGoal | string | no | CBO only: e.g. PURCHASE, SIGN_UP, LEAD, ADD_TO_CART, PAGE_VISIT, CLICKS. Cannot be changed later |
| viewThroughConversionType | string | no | CBO only: the conversion attribution window. Cannot be changed later |
| conversionPixelId | string | no | CBO only: needed when the ad account has more than one pixel |
| schedule | array | no | CBO only: weekly delivery windows (day 0 = Sunday, hour 0-23) — replaces every ad group’s schedule |
| specialAdCategories | array | no | HOUSING_EMPLOYMENT_CREDIT for housing, employment or credit ads — Reddit then restricts their targeting. Cannot be changed after creation |
| appId | string | no | APP_INSTALLS only: the App Store id (e.g. 1064216828) or Google Play package (e.g. com.reddit.frontpage) |
| fundingInstrumentId | string | no | bill this campaign to a specific Reddit funding instrument |
Create an ad group under an existing Reddit campaign — this is the tier that holds the budget, the bid and ALL the targeting. ALWAYS created PAUSED; it spends nothing until set_reddit_ads_status(confirm:true). Reddit refuses the create unless it has: a bidType, a bidStrategy, a startTime, a budget with its goalType, a bidAmount whenever the bid type is a paid rate, and a conversion pixel (resolved
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| campaignId | string | yes | the campaign this ad group belongs to |
| name | string | yes | ad group name |
| budget | number | no | budget in the ad account’s currency, paired with goalType — required, EXCEPT under a campaign-budget-optimization campaign, where the budget lives on the campaign and must be left off |
| goalType | string | no | default DAILY_SPEND; under a CBO campaign it is taken from the campaign |
| bidType | string | no | required — must fit the campaign objective, CLICKS campaigns take CPC; under a CBO campaign it is taken from the campaign |
| bidStrategy | string | no | required — MANUAL_BIDDING needs bidAmount; under a CBO campaign it is taken from the campaign |
| bidAmount | number | no | bid in the ad account’s currency — required for paid bid types; ask reddit_ads_bid_suggestion |
| startTime | string | no | ISO 8601, e.g. 2026-08-15T00:00:00Z — required (Reddit rejects the create without one), except under a CBO campaign, where it defaults to the campaign’s start |
| endTime | string | no | ISO 8601 end time; omit for no end |
| conversionPixelId | string | no | only needed when the ad account has more than one pixel |
| optimizationGoal | string | no | the event this ad group optimizes toward, e.g. PURCHASE, SIGN_UP, LEAD, ADD_TO_CART, PAGE_VISIT, CLICKS, VIDEO_VIEW_6S; leave it unset on a CONVERSIONS or SALES campaign to take the campaign’s. CUSTOM |
| savedAudienceId | string | no | reuse a saved audience instead of spelling targeting out — from list_reddit_ads_saved_audiences |
| viewThroughConversionType | string | no | the conversion window counted. SEVEN_DAY_CLICKS_ONE_DAY_VIEW is only accepted with a conversion goal (PURCHASE, SIGN_UP, LEAD, ADD_TO_CART, PAGE_VISIT), and Reddit requires it for a catalog SALES ad g |
| productSetId | string | no | CATALOG SALES — the Reddit product set this ad group advertises (a UUID from Reddit Ads Manager > Catalogs). Required on every ad group under a SALES campaign created with useCatalog:true; Reddit sile |
| shoppingType | string | no | CATALOG SALES — DYNAMIC builds each ad from the product set, STATIC links a normal ad to the catalog. Required on every ad group under a useCatalog:true SALES campaign |
| targeting | object | no | targeting block; every key optional (communities, geolocations, interests, keywords, audiences, devices) |
| schedule | array | no | weekly dayparting windows — omit to run all week |
Change an existing Reddit ad group’s name, budget, goal type, bid, schedule dates or targeting. Budget and bid are ordinary amounts in the ad account’s currency. Targeting MERGES KEY BY KEY — measured live 2026-08-05, and it is NOT a wholesale replace: a key you send replaces that whole list, a key you LEAVE OUT is kept exactly as it was, and an explicit empty array (geolocations: []) is the only
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| adGroupId | string | yes | id of the ad group to edit |
| name | string | no | new ad group name |
| budget | number | no | new budget in the ad account currency, paired with goalType |
| goalType | string | no | DAILY_SPEND or LIFETIME_SPEND |
| bidAmount | number | no | new bid in the ad account currency |
| bidType | string | no | CPC, CPM, CPV, CPV6 or CPV15; must fit the campaign objective |
| bidStrategy | string | no | BIDLESS, MANUAL_BIDDING, MAXIMIZE_VOLUME or TARGET_CPX |
| startTime | string | no | ISO 8601 new start time |
| endTime | string | no | ISO 8601 new end time |
| savedAudienceId | string | no | point this ad group at a saved audience instead |
| productSetId | string | no | CATALOG SALES — rebind the ad group to another product set (a UUID from Reddit Ads Manager > Catalogs). Reddit stores an edit without checking the set exists or that the campaign uses a catalog, so pa |
| shoppingType | string | no | CATALOG SALES — DYNAMIC or STATIC product ads. Reddit has been measured keeping the value an ad group was created with, and the note says when it did not change |
| targeting | object | no | the targeting keys to change, in the same shape as create_reddit_ads_ad_group: each key sent replaces that whole list, keys left out are kept, and an empty list clears one |
| schedule | array | no | weekly delivery windows: startDay, endDay, startHour, endHour (0 = Sunday) |
Create the ad itself — the object that binds a POST (the creative) to an AD GROUP (the targeting and budget). Create the post first with create_reddit_ads_post and pass its id as postId; without a post there is nothing to show. A CATALOG ad (an ad group with a productSetId under a SALES useCatalog campaign) has no post: pass shoppingCreative (headline, callToAction, optional destinationUrl, second
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| adGroupId | string | yes | the ad group whose targeting and budget this ad runs under |
| name | string | yes | ad name |
| postId | string | no | the post to promote (t3_…) from create_reddit_ads_post or list_reddit_ads_posts — required except for a catalog ad built from shoppingCreative |
| clickUrl | string | no | leave unset for TEXT-post ads — Reddit rejects "Free form ads cannot have a click url" |
| clickUrlQueryParameters | object | no | UTM / tracking parameters Reddit appends to the click URL, e.g. {"utm_source":"reddit","utm_medium":"paid","utm_content":"{{AD_ID}}"} — up to 14 |
| previewDays | number | no | 1-30: switch on a preview link that stays valid this many days |
| redditProfileId | string | no | the post author profile — required for catalog sales campaigns |
| eventTrackers | array | no | third-party measurement URLs; only Reddit-approved providers are accepted |
| productIds | array | no | catalog ads: up to 6 product ids to feature |
| shoppingCreative | object | no | catalog (shopping) ads only |
Build a Reddit MAX campaign in one call: an automated campaign (campaign budget optimization, automatic bidding), its one automated ad group (seed targeting) and its one template ad, from which Reddit GENERATES the ads out of your creative-library assets. ALL CREATED PAUSED: nothing spends until the user approves and set_reddit_ads_status(confirm:true) runs on the campaign, the ad group and the te
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| name | string | yes | campaign name, 3 to 500 characters |
| objective | string | no | default CLICKS; CONVERSIONS is the legacy name of SALES |
| budget | number | yes | campaign budget in the ad account’s currency (a Max budget lives on the campaign) |
| goalType | string | no | default DAILY_SPEND; LIFETIME_SPEND needs endTime |
| startTime | string | yes | ISO 8601, e.g. 2026-09-20T00:00:00Z |
| endTime | string | no | ISO 8601 end time; required for LIFETIME_SPEND |
| bidStrategy | string | no | default BIDLESS |
| bidType | string | no | default CPC for CLICKS; required for the other objectives |
| bidAmount | number | no | target cost per result in the ad account’s currency; required for TARGET_CPX |
| optimizationGoal | string | no | default CLICKS for CLICKS; required otherwise: SALES / CONVERSIONS take e.g. PURCHASE, ADD_TO_CART, SIGN_UP, LEAD or CUSTOM_EVENT (a custom pixel event Reddit switches on per ad account), LEAD_GENERAT |
| spendCap | number | no | lifetime spend ceiling in the ad account’s currency; DAILY_SPEND only, at or above the daily budget |
| invoiceLabel | string | no | up to 80 characters on the invoice; ignored on card-funded accounts |
| conversionPixelId | string | no | only needed when the ad account has more than one pixel |
| appId | string | no | App Store or Google Play id; required for APP_INSTALLS |
| specialAdCategories | array | no | cannot change after publishing |
| viewThroughConversionType | string | no | conversion attribution window: SEVEN_DAY_CLICKS or SEVEN_DAY_CLICKS_ONE_DAY_VIEW |
| fundingInstrumentId | string | no | funding instrument id from list_reddit_ads_funding_instruments |
| schedule | array | no | weekly windows, day 0 = Sunday |
| targeting | object | yes | SEED signals: at least one of communities, interests, keywords or customAudienceIds; geolocations and the excluded lists are hard controls |
Read a Reddit Max template ad: its status, its assets, its destination and whether Reddit has finished generating ads from it (materialization SUCCESS, PARTIAL or FAILED with Reddit’s reasons, or still generating). Poll this after create_reddit_ads_max_campaign or update_reddit_ads_max_template. Read-only, free.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| adId | string | yes | the template ad id |
Change a Reddit Max template ad: its assets (5 to 45 ACTIVE, at least 3 HEADLINE and 2 IMAGE or VIDEO, read and counted before anything is sent), thumbnails, destination URL, supplementary text, user-generated content enrollment or name. The reply is the template read back, including whether Reddit regenerated the ads. Pausing, activating or archiving it is set_reddit_ads_status.
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| adId | string | yes | template ad id, from create_reddit_ads_max_campaign |
| name | string | no | new template ad name |
| assetIds | array | no | REPLACES the template’s assets |
| thumbnailAssetIds | array | no | up to 10; an empty list clears them |
| destinationUrl | string | no | new https landing page |
| displayUrl | string | no | new display URL; same domain as destinationUrl |
| supplementaryText | string | no | up to 100 characters |
| userGeneratedContent | string | no | OPT_IN or OPT_OUT of Reddit user-generated content enhancements |
Edit a Reddit ad, including a LIVE one: rename it, point it at a different post, change its click url, set or clear its UTM / tracking parameters (clickUrlQueryParameters; pass null to clear), switch on or refresh its PREVIEW link (previewDays 1-30, read back as previewUrl; previewOff:true switches it off), replace its third-party event trackers, or for a catalog ad change its shoppingCreative / f
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| adId | string | yes | id of the ad to edit |
| name | string | no | new ad name |
| postId | string | no | promote a different post |
| clickUrl | string | no | pass an empty string to clear it |
| clickUrlQueryParameters | no | UTM / tracking parameters, e.g. {"utm_source":"reddit","utm_content":"{{AD_ID}}"} (≤14); null clears them | |
| previewDays | number | no | 1-30: (re)enable the preview link for this many days |
| previewOff | boolean | no | true switches the preview link off |
| eventTrackers | array | no | replaces the third-party trackers (Reddit-approved providers only) |
| redditProfileId | string | no | Reddit profile id (t2_...) the ad runs as |
| productIds | array | no | catalog ads: up to 6 featured product ids |
| shoppingCreative | object | no | catalog ads: same fields as create_reddit_ads_ad |
Change an existing Reddit campaign’s name, spend cap, budget, goal type or flight dates. Amounts are ordinary numbers in the ad account’s currency. The OBJECTIVE and useCatalog cannot be changed: Reddit freezes both at creation and refuses a change even from an old objective name to its new one (IMPRESSIONS to BRAND_AWARENESS), so create a new campaign instead. This does NOT activate, pause or arc
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| campaignId | string | yes | id of the campaign to edit |
| name | string | no | new campaign name |
| spendCap | number | no | lifetime spend ceiling in the ad account’s currency |
| budget | number | no | new campaign budget in the ad account currency |
| goalType | string | no | DAILY_SPEND or LIFETIME_SPEND |
| startTime | string | no | ISO 8601 new flight start |
| endTime | string | no | ISO 8601 new flight end |
The one switch that arms real money on Reddit. Pass kind ("campaign", "ad_group" or "ad") plus the object id. ACTIVE starts real spend as soon as Reddit approves — show the user exactly what will run and get an explicit yes, then call again with confirm:true. PAUSED is always safe and never gated. REDDIT HAS NO DELETE OPERATION for these three: removal is a status, and ARCHIVED/DELETED here run th
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| kind | string | yes | campaign, ad_group or ad: which type the id refers to |
| id | string | yes | id of the campaign, ad group or ad |
| status | string | yes | ACTIVE = start spending; PAUSED = stop; ARCHIVED = retire; DELETED = permanent, and blocked for 3h after any change |
| confirm | boolean | no | REQUIRED true for ACTIVE (real spend), ARCHIVED and DELETED |
| confirmName | string | no | when archiving or removing: its EXACT name, as the unconfirmed call names it. Required when it has children or is live |
| confirmChildren | number | no | when archiving or removing: the exact number of children the unconfirmed call reported. Required when it has any |
Remove a Reddit campaign, ad group or ad. REDDIT HAS NO DELETE VERB for any of the three — its whole Ads API has exactly four HTTP DELETE endpoints and none of them is a campaign, ad group or ad — so removal is the configured_status field: DELETED (permanent) or ARCHIVED (out of service, and can be set back to PAUSED). DELETED is additionally TIME-GATED: Reddit refuses to delete anything modifie
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| kind | string | yes | campaign, ad_group or ad: which type the id refers to |
| id | string | yes | id of the campaign, ad group or ad to remove |
| status | string | no | DELETED = permanent (default); ARCHIVED = out of service but reversible to PAUSED |
| confirm | boolean | no | REQUIRED true |
| confirmName | string | no | the object’s EXACT name, required when it has children or is live |
| confirmChildren | number | no | the exact number of children reported by the unconfirmed call, required when it has any |
List the conversion pixels on a Reddit ad account, each with the LAST TIME IT FIRED — which is the difference between "a pixel exists" and "conversion tracking works". Call this before building anything: since 13 July 2026 Reddit REQUIRES a pixel on every ad group and every CBO campaign, so an account with none cannot run ads at all. IMPORTANT: the Reddit API has no operation that creates a pixel
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_…) — omit when only one is shared |
Read the Reddit PRODUCT CATALOGS behind a shared ad account — what a catalog SALES campaign (create_reddit_ads_campaign objective SALES + useCatalog:true) advertises. With no catalogId: every catalog on the ad account’s business, with its product counts (total / approved / rejected / invalid), currency, language and attached pixels. With catalogId: that catalog’s linked FEEDS (url, REPLACE/UPDATE
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_…) — omit when only one is shared |
| catalogId | string | no | a catalog id from the list — returns its feeds, product sets and recent imports |
Create an empty Reddit PRODUCT CATALOG on the business behind the shared ad account — the product inventory a catalog SALES campaign advertises (dynamic product ads, collection ads). Spends nothing. Name it, give the CURRENCY your prices are in (USD, GBP, CAD, EUR, AUD, JPY, CHF, NZD, SEK, NOK or BRL — Reddit’s list) and optionally the language and the Reddit pixel(s) to match products against (li
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| name | string | yes | catalog name |
| defaultCurrency | string | yes | the currency your product prices are in — fixed once created |
| defaultLanguage | string | no | default en — fixed once created |
| pixelIds | array | no | Reddit pixel ids from list_reddit_ads_pixels to match products against; each must be on the shared ad account |
Have Reddit FETCH your product file (CSV/TSV/XML at a public https:// or FTP/SFTP address — a Shopify, Google Merchant or WooCommerce product feed works) into a catalog, once or on a schedule. mode REPLACE (default) treats the file as the whole catalog, so products missing from it are removed; UPDATE only adds and changes. A catalog takes at most TWO feeds and they must be of different types. Feed
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| catalogId | string | yes | catalog to import the feed into |
| url | string | yes | the product file Reddit fetches |
| mode | string | no | default REPLACE — the file is the whole catalog |
| name | string | no | feed name, for your reference |
| username | string | no | only if the url needs a login |
| password | string | no | password for the feed URL; sent to Reddit and never echoed back |
| schedule | object | no | how often Reddit re-fetches the file; omit for a one-off |
Define a PRODUCT SET in a Reddit catalog — the rule that picks which products a catalog ad group advertises; its id is the productSetId create_reddit_ads_ad_group takes. The filter is Reddit’s JSON rule: one rule {"attribute":{"operator":value}} or several combined with {"and":[…]} / {"or":[…]}. Attributes: id, title, description, price, currency, sale_price, availability, brand, color, gender, pr
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| catalogId | string | yes | catalog the product set belongs to |
| name | string | yes | product set name |
| filter | yes | the rule as an object (or a JSON string), e.g. {"availability":{"eq":"IN_STOCK"}} |
Change a Reddit catalog (kind "catalog": its name or attached pixelIds — currency and language are fixed), a product FEED (kind "feed": url, mode, name, credentials or its schedule, including pausing it with schedule.isPaused) or a PRODUCT SET (kind "product_set": name or filter, same rule format as create_reddit_ads_product_set). A changed product-set filter changes what every catalog ad group us
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| kind | string | yes | catalog, feed or product_set: which type the id refers to |
| id | string | yes | the catalog, feed or product set id |
| name | string | no | new name |
| pixelIds | array | no | catalog only — replaces the attached pixels |
| url | string | no | feed only |
| mode | string | no | feed only |
| username | string | no | feed only |
| password | string | no | feed only |
| schedule | object | no | feed only — same shape as create_reddit_ads_product_feed |
| filter | no | product_set only — the new rule |
PERMANENTLY delete a Reddit catalog (with all its products, feeds and product sets), a product feed, or a product set. Unlike campaigns these really are deleted, and it cannot be undone. Call WITHOUT confirm first: nothing is deleted, and the answer lists what would go with it — including any ad group on the shared ad account still advertising an affected product set. Show that to the user, get an
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| kind | string | yes | catalog, feed or product_set: which type the id refers to |
| id | string | yes | id of the catalog, feed or product set to delete |
| confirm | boolean | no | true only after the user approved the listed impact |
Read the products in a Reddit catalog (catalogId) or in one product set (productSetId), each with its title, price, availability, link, image and Reddit’s own review state and issues (the "reddit" block). Filter by approvalDecision (PENDING / APPROVED / REJECTED — REJECTED is how to find out why products are not serving), a search term, or specific ids. Paginated: pass the returned nextPageToken a
| Parameter | Type | Required | Notes |
|---|---|---|---|
| adAccountId | string | no | Reddit ad account id (a2_...); omit when only one is shared |
| catalogId | string | no | one of catalogId or productSetId |
| productSetId | string | no | list the products in this product set instead of the whole catalog |
| approvalDecision | string | no | PENDING, APPROVED or REJECTED: filter by Reddit review state |
| search | string | no | text to match against product fields |
| ids | array | no | specific product ids to read |
| limit | number | no | page size, default 50, max 1000 |
| pageToken | string | no | nextPageToken from the previous page |
- Every tool — any call limit is its publisher’s, not ours
- No card, and mcprush charges nothing for it
- Runs at its publisher’s own address — this marketplace is not in the path
- Nothing to cap, because mcprush bills nothing
What counts against your monthly calls
| Tool | Unit | Calls used | Out of the allowance |
|---|
Nothing here is billable: mcprush charges nothing to install Hermoso: Ads Manager or to call it, at any volume.
Two independent axes, because powerful and malicious are different questions. The grade is threat only. The capability level is blast radius, and it is never a penalty on the grade — it is priced as one subtract-only term in the score, where you can see it.
This listing is an address its publisher runs, and you connect to it directly — this marketplace is not in the path. The scanner reads packages and public source, and an address is neither: the repository linked on this page is where this record was collected from, not a copy of what the address serves, so it is not graded in its place. So there is no grade — not a withheld one, an unmeasured one.
Release history
This is an address its publisher runs: which release it serves is theirs to choose, so there is nothing here to pin. The history below is for reading; the current release on file is 1.0.0.
No release note was published with this version.
One review per account, from a signed-in account that does not publish this listing. Nothing else is asked — you can say what you think before you install it. Publishers can reply once per review, and a review can be edited or deleted by whoever wrote it.
Any signed-in account that does not publish Hermoso: Ads Manager can review it, once — before installing it or after. A review from an account that has installed it is marked as one.
Nobody has reviewed this listing. The rating on the card is the mean of the reviews written here and nothing else, so there is no rating until somebody writes the first — which takes a signed-in account that does not publish it, and nothing else.