hermosodocs

Ad campaign management

Build and run paid campaigns across seven ad platforms. Everything is created paused.

111 tools. Every name, description and parameter on this page is generated from the running MCP server — see how this reference is built.

PINTEREST DEEP ANALYTICS

pinterest_ads_async_report#

Pinterest deep (async) ad report read-only

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 / KEYWORD / PRODUCT_GROUP / PRODUCT_ITEM plus their *_TARGETING twins. Pinterest generates it asynchronously, so this may come back pending:true with a token — CALL AGAIN WITH THAT TOKEN to pick it up, and never re-submit without it (a second submit generates a second report). Pinterest’s own windows are enforced here with the reason rather than as an opaque 400: 914 days back over at most 186 days; at HOUR granularity 8 days back over 3; at a PRODUCT_ITEM level 92 back over 31. A finished report link is valid five minutes and the report one hour, so an EXPIRED status means run it again, not that anything failed. Read-only, 0 credits.

ParameterTypeDescription
adAccountIdstring
tokenstringRESUME a pending report — pass the token back instead of re-submitting
sincestringYYYY-MM-DD (default 30 days ago)
untilstringYYYY-MM-DD (default today)
granularity"TOTAL" | "DAY" | "HOUR" | "WEEK" | "MONTH"
levelstringADVERTISER | CAMPAIGN | AD_GROUP | PIN_PROMOTION | KEYWORD | PRODUCT_GROUP | PRODUCT_ITEM (+ _TARGETING variants) — default CAMPAIGN. An unknown level is refused with the list.
columnsstring[]Pinterest async metric columns — omit for the standard spend/impressions/clicks/CTR/conversions set
campaignIdsstring[]
adGroupIdsstring[]
adIdsstring[]
targetingTypesstring[]only valid with a *_TARGETING level
reportFormat"JSON" | "CSV"

pinterest_targeting_analytics#

Pinterest ads by audience segment read-only

WHICH AUDIENCE SEGMENT actually delivered on Pinterest — ad performance broken down by keyword, targeted interest, age bucket, gender, location, region, country, placement, app type, media type and more. targetingTypes is REQUIRED because it is what the report breaks down BY. scope:"account" covers the whole ad account; "campaign" / "adGroup" / "ad" each REQUIRE their own id list, because Pinterest publishes no all-of-them form at those levels — that is Pinterest’s shape, not a limitation here. 90 days back in windows of at most 90 days, refused locally with the reason. An unknown targeting type is refused BY NAME; note Pinterest’s four per-level enums differ slightly, so a value valid at one level can still be refused at another. Read-only, 0 credits.

ParameterTypeDescription
adAccountIdstring
scope"account" | "campaign" | "adGroup" | "ad"default account
targetingTypesrequiredstring[]REQUIRED — e.g. KEYWORD, AGE_BUCKET, GENDER, LOCATION, PLACEMENT, MEDIA_TYPE, TARGETED_INTEREST, PINNER_INTEREST, COUNTRY, REGION
campaignIdsstring[]
adGroupIdsstring[]
adIdsstring[]
sincestring
untilstring
granularity"TOTAL" | "DAY" | "HOUR" | "WEEK" | "MONTH"
columnsstring[]

pinterest_audience_insights#

Pinterest audience insights read-only

WHO the Pinterest audience IS, rather than what it did — interest categories each carrying an affinity INDEX, plus demographics (ages, countries, devices, genders, metros). Three audiences: YOUR_TOTAL_AUDIENCE, YOUR_ENGAGED_AUDIENCE, and PINTEREST_TOTAL_AUDIENCE as the baseline to compare the other two against. This is an input to a creative brief, not a performance report. SAY THIS WHEN REPORTING: an affinity index is how much MORE likely this audience is to engage with a category than Pinterest’s baseline — it is a comparison, never a count — and when Pinterest flags size_is_upper_bound the audience size is an upper bound, not a measurement. There is no date range: Pinterest returns its current snapshot and names the date it is for. Read-only, 0 credits.

ParameterTypeDescription
adAccountIdstring
insightType"YOUR_TOTAL_AUDIENCE" | "YOUR_ENGAGED_AUDIENCE" | "PINTEREST_TOTAL_AUDIENCE"default YOUR_TOTAL_AUDIENCE

Meta ad-set targeting, shared by create_meta_ad and create_m…

create_meta_ad#

Build a full Meta ad (campaign → ad set → ad, paused)

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–10 cards each with its own headline/description/link) all work. Targeting is the `targeting` object: geo down to cities with a radius, age, gender, interests, behaviours, custom audiences and lookalikes, languages, placements, devices and OS. For a conversion objective pass pixelId + conversionEvent and the ad set optimizes for that conversion. Schedule with startTime/endTime + dayparting; bid with bidStrategy + bidAmountUsd/minRoas; use lifetimeBudgetUsd (with endTime) for a fixed flight. Attach to an existing campaign with campaignId or an existing ad set with adSetId. Everything is READ BACK from Meta before you are told it exists — print the returned summary verbatim (it now carries Meta-rendered PREVIEW LINKS for the first ad, valid 24 hours — hand them to the user so they can see the ad; preview_meta_ad renders any ad in any placement). Needs ads-management on the connected account.

ParameterTypeDescription
adAccountIdrequiredstringad account id (act_… or digits — from list_meta_pages)
format"auto" | "carousel"auto = one ad per asset (image or video); carousel = ONE multi-card ad
imageUrlstringpublic https image URL for the ad creative
imageUrlsstring[]several image URLs → one ad each, or the carousel cards in order
videoUrlstringa video URL → a real Meta VIDEO ad (uploaded + transcoded + thumbnailed for you)
thumbnailUrlstringcustom video thumbnail (otherwise Meta picks a frame)
messagestringprimary ad text
headlinestringheadline
descriptionstringthe smaller description line under the headline
cardsobject[]carousel cards in order — each may set its own headline/description/link
carouselEndCardbooleanappend the Page end card to a carousel
linkstringdestination URL (defaults to the brand domain)
ctastringcall-to-action, e.g. SHOP_NOW / LEARN_MORE / SIGN_UP (default LEARN_MORE)
objective"OUTCOME_TRAFFIC" | "OUTCOME_AWARENESS" | "OUTCOME_ENGAGEMENT" | "OUTCOME_LEADS" | "OUTCOME_SALES"default OUTCOME_TRAFFIC
dailyBudgetUsdnumberad-set daily budget USD (1–10000, default 10) — spends only once ACTIVE
lifetimeBudgetUsdnumbera fixed total instead of a daily budget — REQUIRES endTime
countrystring2-letter shorthand when you are not passing full targeting (default US)
targetingobjectfull 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 — it is never dropped, because a dropped geo key used to fall back to targeting the United States.
pixelIdstringMeta Pixel id — with this the ad set optimizes for a real CONVERSION instead of falling back to link clicks
conversionEventstringPURCHASE | LEAD | COMPLETE_REGISTRATION | ADD_TO_CART | INITIATED_CHECKOUT | …
customConversionIdstring
applicationIdstringapp-promotion ads
objectStoreUrlstring
optimizationGoalstringoverride, e.g. OFFSITE_CONVERSIONS / LANDING_PAGE_VIEWS / THRUPLAY / VALUE
billingEventstringdefault IMPRESSIONS
bidStrategy"LOWEST_COST_WITHOUT_CAP" | "LOWEST_COST_WITH_BID_CAP" | "COST_CAP" | "LOWEST_COST_WITH_MIN_ROAS"
bidAmountUsdnumberREQUIRED for a bid cap / cost cap
minRoasnumberREQUIRED for LOWEST_COST_WITH_MIN_ROAS, e.g. 1.1
startTimestringISO-8601 with offset, e.g. 2026-08-01T09:00:00-0700
endTimestringREQUIRED with lifetimeBudgetUsd
adsetScheduleobject[]dayparting — minutes from midnight (0–1440), days 0=Sunday…6=Saturday
attributionSpecobject[]e.g. [{event_type:"CLICK_THROUGH",window_days:7}]
specialAdCategories"HOUSING" | "EMPLOYMENT" | "CREDIT" | "ISSUES_ELECTIONS_POLITICS" | "ONLINE_GAMBLING_AND_GAMING" | "FINANCIAL_PRODUCTS_SERVICES"[]legally required when the ad falls in one of these categories — it restricts targeting
instagramUserIdstringrun it on Instagram under the brand’s own handle
namestringbase name for the campaign/ad set/ads
campaignIdstringattach to an existing campaign instead of creating one
adSetIdstringattach the ad(s) to an EXISTING ad set (skips ad-set creation)
pageIdstringPage id from list_meta_pages; omit = first Page

create_meta_adset#

Create a Meta ad set (audience + budget + schedule)

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 and read back from Meta. It has NO ads until you call create_meta_ad(adSetId:…).

ParameterTypeDescription
adAccountIdrequiredstringad account id (act_… or digits)
campaignIdrequiredstringthe campaign this ad set belongs to
namestringad set name
dailyBudgetUsdnumberad-set daily budget USD (1–10000, default 10) — spends only once ACTIVE
lifetimeBudgetUsdnumbera fixed total instead of a daily budget — REQUIRES endTime
countrystring2-letter shorthand when you are not passing full targeting (default US)
targetingobjectfull 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 — it is never dropped, because a dropped geo key used to fall back to targeting the United States.
pixelIdstringMeta Pixel id — with this the ad set optimizes for a real CONVERSION instead of falling back to link clicks
conversionEventstringPURCHASE | LEAD | COMPLETE_REGISTRATION | ADD_TO_CART | INITIATED_CHECKOUT | …
customConversionIdstring
applicationIdstringapp-promotion ads
objectStoreUrlstring
optimizationGoalstringoverride, e.g. OFFSITE_CONVERSIONS / LANDING_PAGE_VIEWS / THRUPLAY / VALUE
billingEventstringdefault IMPRESSIONS
bidStrategy"LOWEST_COST_WITHOUT_CAP" | "LOWEST_COST_WITH_BID_CAP" | "COST_CAP" | "LOWEST_COST_WITH_MIN_ROAS"
bidAmountUsdnumberREQUIRED for a bid cap / cost cap
minRoasnumberREQUIRED for LOWEST_COST_WITH_MIN_ROAS, e.g. 1.1
startTimestringISO-8601 with offset, e.g. 2026-08-01T09:00:00-0700
endTimestringREQUIRED with lifetimeBudgetUsd
adsetScheduleobject[]dayparting — minutes from midnight (0–1440), days 0=Sunday…6=Saturday
attributionSpecobject[]e.g. [{event_type:"CLICK_THROUGH",window_days:7}]
pageIdstringPage id; omit = first Page

find_meta_audiences#

Look up Meta targeting ids read-only

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 in targeting.geo.cities/regions/zips; type:"adlocale" (q:"french") returns language ids for targeting.locales. Read-only and free. Use it whenever the user names an audience in words — never guess an id.

ParameterTypeDescription
typerequired"adinterest" | "adTargetingCategory" | "adgeolocation" | "adlocale" | "adcountry" | "adzipcode" | "adeducationschool" | "adeducationmajor" | "adworkemployer" | "adworkposition"what kind of targeting object to search
qstringwhat to search for
classstringfor adTargetingCategory, e.g. "behaviors" or "interests"
locationTypesstringcomma-separated: country,region,city,zip,geo_market
countryCodestring2-letter hint to disambiguate a city name
adAccountIdstringsearch with that ad account’s token
limitnumber

Meta: READ / MEASURE / EDIT / DELETE existing objects

list_meta_ads#

List Meta campaigns / ad sets / ads read-only

Read the EXISTING campaigns, ad sets, or ads on a connected Meta ad account — id, name, status, budget, objective. Pass adAccountId (from list_meta_pages) 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 inspect an account before editing/deleting, or to answer "what’s running?".

ParameterTypeDescription
adAccountIdrequiredstringad account id (act_… or digits — from list_meta_pages)
level"campaign" | "adset" | "ad"what to list (default campaign)
campaignIdstringlist the ad sets / ads under this campaign
adsetIdstringlist the ads under this ad set
statusstringfilter by effective status, e.g. ACTIVE / PAUSED
limitnumbermax rows (1–200, default 50)

meta_insights#

Meta ad performance metrics read-only

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 total says an ad cost $X, never WHO it worked on: pass breakdowns:"age,gender", "publisher_platform,platform_position" (which placement), "country" / "region" / "dma" (where), "impression_device" / "device_platform" (what they held). Comma-separated; "placement", "device" and "geo" are accepted as aliases; an unknown value is REJECTED, never silently ignored. THREE breakdowns need an ad-account OPT-IN from 2026-08-06 — impression_device, hourly_stats_aggregated_by_audience_time_zone and frequency_value: Meta returns NO ROWS (not an error) for an account that has not opted in, so they are always ATTEMPTED, and if nothing comes back the report is re-run WITHOUT them and `droppedBreakdowns` + a note name the missing dimension and say an account admin can enable it in Ads Manager. A dropped dimension is ABSENT, never zero — never present the remaining total as if it were still split by it. Date window: datePreset OR since+until (YYYY-MM-DD). datePreset is Meta's OWN enum — 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_sun_sat, this_month, last_month, this_quarter, last_quarter, this_year, last_year, maximum, data_maximum. THERE IS NO "lifetime": Meta disabled it in Graph API v10.0 and replaced it with "maximum" (the last 37 months); anything unrecognised is refused by name here rather than 400ing at Meta. Read-only.

ParameterTypeDescription
adAccountIdrequiredstringad account id (act_… or digits)
objectIdstringa campaign / ad set / ad id to scope to (default: the whole account)
level"account" | "campaign" | "adset" | "ad"break the numbers down by this level
breakdownsstringcomma-separated, e.g. "age,gender" | "publisher_platform,platform_position" | "country" | "impression_device"
actionBreakdownsstringcomma-separated, e.g. "action_type,action_device" — splits the conversion/action counts
datePresetstringMeta'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_sun_sat | this_month | last_month | this_quarter | last_quarter | this_year | last_year | maximum | data_maximum. NOT "lifetime" — Meta retired it in v10.0; use "maximum" (37 months) or since+until.
sincestringstart date YYYY-MM-DD (use with until)
untilstringend date YYYY-MM-DD

Meta: SEE the ad, SIZE the audience, BUILD the audience

preview_meta_ad#

Preview a Meta ad exactly as it will appear read-only

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_feed, instagram_feed, instagram_story, instagram_reels, instagram_explore, instagram_profile_feed, messenger_inbox, messenger_story, audience_network — default facebook_feed + instagram_feed + instagram_story + instagram_reels. Free, read-only, spends nothing. THE LINKS EXPIRE AFTER 24 HOURS — always say so when handing them to a user. Use it straight after create_meta_ad, and whenever someone wants to approve an ad before it runs.

ParameterTypeDescription
adAccountIdrequiredstringad account id (act_… or digits)
adIdstringthe ad to preview (from list_meta_ads)
creativeIdstringpreview a creative directly instead of an ad
placementsstringcomma-separated placements (see the list above)

estimate_meta_reach#

Estimate how many people a Meta audience reaches read-only

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 daily-active estimate, and an explicit warning when the audience is too narrow to deliver. Free, read-only, creates nothing and spends nothing. Use it before recommending a budget and every time the user narrows a geo or piles on interests.

ParameterTypeDescription
adAccountIdrequiredstringad account id (act_… or digits)
adSetIdstringsize an EXISTING ad set using its own saved targeting
targetingobjecta targeting object, same shape as create_meta_ad.targeting
objectivestringOUTCOME_TRAFFIC | OUTCOME_SALES | … — picks the matching optimization goal
optimizationGoalstringoverride the goal, e.g. REACH / LINK_CLICKS / OFFSITE_CONVERSIONS
countrystring2-letter fallback country when targeting names no geo
pixelIdstringestimate a conversion goal against this pixel
conversionEventstringe.g. PURCHASE — used with pixelId

list_meta_audiences#

List Meta custom audiences + lookalikes read-only

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.

ParameterTypeDescription
adAccountIdrequiredstringad account id (act_… or digits)
limitnumbermax rows (1–200, default 50)

create_meta_audience#

Create a Meta custom audience or lookalike

Build a retargeting audience on a connected Meta ad account. Three 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:"lookalike" (sourceAudienceId + country + ratio 0.01–0.20, lookalikeType "similarity" or "reach"). CREATING AN AUDIENCE SPENDS NOTHING — it is a definition; money only moves when an ad set targets it and that campaign is activated through set_meta_campaign_status(confirm:true). Meta needs roughly 30 minutes and ~1,000 people before a new audience can be targeted, so a fresh one reporting no size is normal. Customer-list uploads are deliberately NOT supported here (hashed personal data + Meta’s Custom Audience Terms) — send the user to Ads Manager for those.

ParameterTypeDescription
adAccountIdrequiredstringad account id (act_… or digits)
kindrequired"website" | "engagement" | "lookalike"which kind of audience to build
namerequiredstringaudience name
descriptionstring
retentionDaysnumberhow long someone stays in it — website max 180, engagement max 730 (default 30)
pixelIdstringwebsite: the Meta Pixel that sees the traffic
urlContainsstringwebsite: the URL fragment that defines the audience (your domain = all visitors)
pageIdstringengagement: which connected Page (required only if the brand has several)
source"page" | "instagram"engagement: Facebook Page (default) or the linked Instagram business profile
eventstringengagement: 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_messaged_business | ig_business_profile_visit
sourceAudienceIdstringlookalike: the existing audience to model (from list_meta_audiences)
countrystringlookalike: 2-letter country to build it in
rationumberlookalike: 0.01–0.20 = the top 1%–20% most similar people in that country (default 0.01)
startingRationumberlookalike: optional lower bound, must be less than ratio
lookalikeType"similarity" | "reach"lookalike: similarity (tighter) or reach (broader) — default similarity
prefillbooleanwebsite/engagement: seed it with activity from BEFORE the audience existed (default true)

delete_meta_audience#

Delete a Meta custom audience destructive

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 its real name, how many people are in it and which lookalikes were built from it, read live from Meta. Show the user exactly that. A populated audience, or one with lookalikes, then needs confirmName set to its exact name (and confirmChildren set to the lookalike count when there are any). META REFUSES to delete an audience that has lookalikes derived from it (error 2656) — delete those first; the unconfirmed call names them. Pass adAccountId + audienceId (from list_meta_audiences). The result is READ BACK from Meta: it says deleted only when the id no longer resolves.

ParameterTypeDescription
adAccountIdrequiredstringad account id (act_… or digits)
audienceIdrequiredstringthe custom audience id (from list_meta_audiences)
confirmbooleanREQUIRED true — the deletion is permanent
confirmNamestringthe audience’s EXACT name, required when it holds people or has lookalikes
confirmChildrennumberthe exact number of derived lookalikes reported by the unconfirmed call, required when it has any

Google Ads: read + manage

list_google_ads_campaigns#

List Google Ads accounts / campaigns read-only

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 | THIS_MONTH | LAST_90_DAYS …) or since+until (YYYY-MM-DD). Read-only, free. Needs Google Ads connected (Settings ▸ Connectors ▸ Google Ads).

ParameterTypeDescription
customerIdstring10-digit account id (dashes ok) — omit to list accessible accounts
status"ENABLED" | "PAUSED" | "REMOVED"filter campaigns by status
datePresetstringmetrics window preset (default LAST_30_DAYS)
sincestringstart date YYYY-MM-DD (with until)
untilstringend date YYYY-MM-DD
metricsbooleaninclude performance metrics (default true)
loginCustomerIdstringmanager (MCC) id — only if reaching a client account through a manager
limitnumbermax campaigns (1–500, default 100)

GOOGLE ADS CHANGE HISTORY

create_google_ads_campaign#

Build a Google Ads campaign (paused)

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 group + ad + keywords in ONE ATOMIC operation (if any part is rejected, nothing at all is created — no half-built campaign to clean up). Also here: bidding strategy, locations by NAME ("United States", "Toronto" — resolved for you), languages, and start/end dates. Google requires 3–15 headlines (≤30 chars) and 2–4 descriptions (≤90 chars) on a search ad. Everything is READ BACK from Google before you are told it exists; print the returned note verbatim, and if it says the campaign cannot serve yet, say that rather than calling it a finished ad.

ParameterTypeDescription
customerIdstring10-digit account id (from list_google_ads_campaigns) — omit to use the brand’s selected default account
namerequiredstringcampaign name
dailyBudgetUsdnumberdaily budget USD (1–100000) — creates a budget inline; required unless budgetResourceName is given
budgetResourceNamestringreuse an existing budget instead of creating one
channelType"SEARCH" | "DISPLAY"default SEARCH
searchPartnersbooleanSEARCH only — also serve on Google search partners (default false)
biddingobjecthow the campaign bids — default MANUAL_CPC
locationsstring[]location NAMES to target, e.g. ["United States"] or ["Toronto","Vancouver"]. WITHOUT this the campaign runs WORLDWIDE — the most expensive default in Google Ads
excludedLocationsstring[]location names to EXCLUDE
languagesstring[]ISO language codes, e.g. ["en","fr"]
startDatestringYYYY-MM-DD
endDatestringYYYY-MM-DD
adGroupobjectbuild the serving tree in the same atomic call — WITHOUT this you get a campaign shell that can never show an ad
containsEuPoliticalAdsbooleanEU Political Advertising Regulation declaration. Google REQUIRES one on every campaign. Default false (a normal commercial ad) — set true ONLY for genuine EU political advertising
dryRunbooleanvalidate the WHOLE tree against Google without creating anything. Nothing is written and no budget is consumed
loginCustomerIdstringmanager id if operating through an MCC

create_google_ads_ad_group#

Add an ad group to a Google Ads campaign

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 (ENABLED), creating this ENABLED starts REAL AD SPEND immediately — show the user what would begin serving, get an explicit yes, then pass confirm:true. Leaving it PAUSED never needs confirmation.

ParameterTypeDescription
customerIdstringomit to use the brand’s selected default account
campaignIdrequiredstringthe campaign this ad group belongs to
namerequiredstringad group name
cpcBidUsdnumbermax CPC for this ad group — omit to inherit the campaign bidding
status"ENABLED" | "PAUSED"default PAUSED
adobjectbuild the ad in the same atomic call
keywordsobject[]a SEARCH ad group with no keywords never shows
confirmbooleanset true ONLY after the user explicitly approved starting spend — required when the parent campaign is already LIVE (ENABLED) and this object would serve immediately
dryRunboolean
loginCustomerIdstring

create_google_ads_ad#

Create a Google Ads ad

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, businessName plus BOTH a landscape (1.91:1) and a square (1:1) image asset from upload_google_ads_asset. The right format is chosen from the campaign's channel. Created PAUSED and read back from Google. If the parent campaign is already LIVE (ENABLED), creating this ENABLED starts REAL AD SPEND immediately — show the user what would begin serving, get an explicit yes, then pass confirm:true. Leaving it PAUSED never needs confirmation.

ParameterTypeDescription
customerIdstringomit to use the brand’s selected default account
adGroupIdrequiredstringthe ad group this ad lives in
finalUrlsstring[]the landing page(s) — at least one is required
headlinesstring | object[]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).
descriptionsstring | object[]SEARCH: 2–4 descriptions, each ≤90 characters. DISPLAY: 1–5. Pass a plain string, or {text, pin} to pin it.
path1stringSEARCH only — display-URL path segment, ≤15 chars
path2stringSEARCH only — second display-URL path segment, ≤15 chars
longHeadlinestringDISPLAY only — ≤90 characters
businessNamestringDISPLAY only — ≤25 characters
marketingImagesstring[]DISPLAY only — landscape 1.91:1 asset resource names from upload_google_ads_asset
squareMarketingImagesstring[]DISPLAY only — square 1:1 asset resource names
logoImagesstring[]DISPLAY only — logo asset resource names
status"ENABLED" | "PAUSED"default PAUSED
confirmbooleanset true ONLY after the user explicitly approved starting spend — required when the parent campaign is already LIVE (ENABLED) and this object would serve immediately
dryRunboolean
loginCustomerIdstring

add_google_ads_keywords#

Add Google Ads keywords

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 and ad group are already LIVE, a positive keyword starts bidding real money at once — get an explicit yes and pass confirm:true, or add it with paused:true. Negative keywords only restrict spend and never need confirmation.

ParameterTypeDescription
customerIdstringomit to use the brand’s selected default account
adGroupIdrequiredstringthe ad group to add them to
keywordsrequiredobject[]the keywords to add
confirmbooleanset true ONLY after the user explicitly approved starting spend — required when the parent campaign is already LIVE (ENABLED) and this object would serve immediately
dryRunboolean
loginCustomerIdstring

set_google_ads_targeting#

Set Google Ads location & language targeting

Set WHERE and in what LANGUAGE an existing Google Ads campaign runs. Pass locations by NAME ("United States", "California", "Toronto") — they are resolved to Google's geo target ids for you; excludedLocations blocks places; languages takes ISO codes ("en","fr"). A campaign with NO location targeting runs WORLDWIDE, which is the most expensive default in Google Ads. Changing a LIVE campaign's targeting moves real spend immediately, so that needs confirm:true.

ParameterTypeDescription
customerIdstringomit to use the brand’s selected default account
campaignIdrequiredstringthe campaign to target
locationsstring[]location NAMES to target
excludedLocationsstring[]location NAMES to exclude
languagesstring[]ISO language codes, e.g. ["en","es"]
countryCodestring2-letter hint to disambiguate a city name, e.g. CA for "London"
confirmbooleanREQUIRED true to change a LIVE (ENABLED) campaign
dryRunboolean
loginCustomerIdstring

set_google_ads_bidding#

Set a Google Ads bidding strategy

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 equivalents — maximize-conversions with a target CPA, and maximize-conversion-value with a target ROAS — so the read-back reports them as MAXIMIZE_CONVERSIONS / MAXIMIZE_CONVERSION_VALUE; report what the read-back says. The conversion-based strategies only deliver once conversion tracking is configured on the account. Changing a LIVE campaign's bidding changes what it pays immediately, so that needs confirm:true.

ParameterTypeDescription
customerIdstringomit to use the brand’s selected default account
campaignIdrequiredstringthe campaign to change
strategyrequired"MANUAL_CPC" | "MAXIMIZE_CLICKS" | "MAXIMIZE_CONVERSIONS" | "MAXIMIZE_CONVERSION_VALUE" | "TARGET_CPA" | "TARGET_ROAS"the bidding strategy
targetCpaUsdnumberREQUIRED for TARGET_CPA
targetRoasnumberREQUIRED for TARGET_ROAS — e.g. 4 = $4 revenue per $1 spent
maxCpcUsdnumberMAXIMIZE_CLICKS — the max CPC ceiling; REQUIRED when switching an existing campaign to it
enhancedCpcbooleanMANUAL_CPC only
confirmbooleanREQUIRED true to change a LIVE (ENABLED) campaign
dryRunboolean
loginCustomerIdstring

find_google_ads_locations#

Look up Google Ads locations read-only

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.

ParameterTypeDescription
queryrequiredstringone location name, or several comma-separated (up to 25)
countryCodestring2-letter hint, e.g. CA to disambiguate "London"
loginCustomerIdstring

set_google_ads_budget#

Set a Google Ads campaign budget

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.

ParameterTypeDescription
customerIdstring10-digit account id (dashes ok) — omit to use the brand’s selected default account
dailyBudgetUsdrequirednumberdaily budget in USD (1–100000)
budgetResourceNamestringexisting budget to UPDATE — omit to CREATE a new budget
namestringname for a newly created budget
confirmbooleanREQUIRED true to raise the budget of a LIVE campaign
loginCustomerIdstringmanager id if operating through an MCC

set_google_ads_status#

Enable, pause or remove a Google Ads campaign / ad group / ad

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 always safe. REMOVED is PERMANENT in Google Ads and is handled by the same gate as delete_google_ads_object — call it once WITHOUT confirm to see what goes with it, and expect to echo back the object’s name and child count when it has children, is live, or has spent. The resulting status is READ BACK from Google before you are told it took.

ParameterTypeDescription
customerIdstring10-digit account id (dashes ok) — omit to use the brand’s selected default account
level"campaign" | "adGroup" | "ad"what to change — default campaign
campaignIdstringcampaign id (level:"campaign")
adGroupIdstringad group id (level:"adGroup", or with adId for level:"ad")
adIdstringad id (level:"ad" — pass adGroupId too)
campaignResourceNamestringfull resource name, e.g. customers/{cid}/campaigns/{id}
statusrequired"ENABLED" | "PAUSED" | "REMOVED"ENABLED = start spending; PAUSED = stop; REMOVED = permanent
confirmbooleanREQUIRED true to ENABLE (real spend) or to REMOVE (permanent)
loginCustomerIdstringmanager id if operating through an MCC

delete_google_ads_object#

Remove a Google Ads campaign / ad group / ad / keyword / asset link / conversion action destructive

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"/"adGroupAsset" + the LINK’s full resourceName (get it from google_ads_report over campaign_asset / ad_group_asset — an asset id alone does not identify a link). THERE IS DELIBERATELY NO "asset" LEVEL: Google publishes no operation that deletes an Asset, only its links, so removing a link unlinks the asset and leaves it in the library. CALL IT WITHOUT confirm FIRST — nothing is removed and you get the object’s real name, status, LIFETIME SPEND and child counts read live from Google; show the user exactly that. A target with children, live delivery or real spend additionally needs confirmName (its exact name) and confirmChildren (the child count from that read-back). Removing a CAMPAIGN also removes its campaign-owned budget, and the note says whether it did. Removing the last ENABLED conversion action makes every smart-bidding campaign on the account undeliverable — the refusal says so. To stop delivery without removing, use set_google_ads_status(status:"PAUSED").

ParameterTypeDescription
customerIdstring10-digit account id (dashes ok) — omit to use the brand’s selected default account
level"campaign" | "adGroup" | "ad" | "keyword" | "campaignAsset" | "adGroupAsset" | "conversionAction"what to remove — default campaign
campaignIdstringcampaign id (level:"campaign")
adGroupIdstringad group id (level:"adGroup"; REQUIRED as the parent for "ad" and "keyword")
adIdstringad id (level:"ad" — pass adGroupId too)
keywordIdstringkeyword criterion id (level:"keyword" — pass adGroupId too)
conversionActionIdstringconversion action id (level:"conversionAction")
resourceNamestringfull resource name — REQUIRED for campaignAsset / adGroupAsset, accepted for any level
confirmbooleanREQUIRED true — REMOVED is permanent
confirmNamestringthe object’s EXACT name, required when it has children / is live / has spent
confirmChildrennumberthe exact number of children reported by the unconfirmed call, required when it has any
loginCustomerIdstringmanager id if operating through an MCC

upload_google_ads_asset#

Upload a creative to Google Ads

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, Google Ads uses YouTube-hosted videos — post the video to YouTube as UNLISTED first (post_to_youtube with privacy:"unlisted" — link-only, not public or searchable, and unlike "private" it CAN run as an ad), then pass its youtubeVideoId here. Returns the asset resource name. Pass customerId (from list_google_ads_campaigns).

ParameterTypeDescription
customerIdstring10-digit account id (dashes ok) — omit to use the brand’s selected default account
imageUrlstringa 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.
youtubeVideoIdstringa YouTube video id for a VIDEO asset (post_to_youtube first)
namestringasset name
loginCustomerIdstringmanager id if operating through an MCC

Google Ads breadth

create_google_ads_conversion_action#

Create a Google Ads conversion action

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), UPLOAD_CLICKS or UPLOAD_CALLS; every other Google conversion type (Firebase, Google Analytics 4, Floodlight, store visits) is READ-ONLY and is created in those products, not here. Set category to what actually happened (PURCHASE, SUBMIT_LEAD_FORM, SIGNUP, BOOK_APPOINTMENT…) and defaultValueUsd when a conversion has a known worth — TARGET_ROAS has nothing to maximise without a value. Created ENABLED and counted in "conversions" by default, because a conversion action that is neither records nothing. It CANNOT SERVE AN AD and cannot spend a cent, so it needs no confirmation. A WEBPAGE action records NOTHING until its Google tag is installed on the site — say that when you report it.

ParameterTypeDescription
customerIdstring10-digit account id (dashes ok) — omit to use the brand’s selected default account
namerequiredstringwhat the user calls this result, e.g. "Purchase", "Demo request"
type"WEBPAGE" | "UPLOAD_CLICKS" | "UPLOAD_CALLS"default WEBPAGE — a conversion that happens on the website
category"DEFAULT" | "PAGE_VIEW" | "PURCHASE" | "SIGNUP" | "DOWNLOAD" | "ADD_TO_CART" | "BEGIN_CHECKOUT" | "SUBSCRIBE_PAID" | "PHONE_CALL_LEAD" | "IMPORTED_LEAD" | "SUBMIT_LEAD_FORM" | "BOOK_APPOINTMENT" | "REQUEST_QUOTE" | "GET_DIRECTIONS" | "OUTBOUND_CLICK" | "CONTACT" | "ENGAGEMENT" | "STORE_VISIT" | "STORE_SALE" | "QUALIFIED_LEAD" | "CONVERTED_LEAD" | "YOUTUBE_FOLLOW_ON_VIEWS"what kind of result this is — default DEFAULT
status"ENABLED" | "PAUSED" | "REMOVED" | "HIDDEN"default ENABLED — anything else records nothing
countingType"ONE_PER_CLICK" | "MANY_PER_CLICK"ONE_PER_CLICK for leads, MANY_PER_CLICK for sales — defaults by category
defaultValueUsdnumberwhat one conversion is worth — required in practice for TARGET_ROAS
defaultCurrencyCodestring3-letter ISO code, e.g. USD
alwaysUseDefaultValuebooleanignore any value sent with the conversion and always use the default
clickThroughLookbackDaysnumber1–90 days
viewThroughLookbackDaysnumber1–30 days
includeInConversionsMetricbooleandefault true — false makes smart bidding IGNORE it
primaryForGoalbooleandefault true — whether this action is biddable for its category
dryRunbooleanvalidate against Google and create NOTHING
loginCustomerIdstringmanager id if operating through an MCC

list_google_ads_conversion_actions#

List Google Ads conversion actions read-only

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 ever learning. Shows each action’s status, type, category, counting type, and whether it counts toward "conversions". Read-only, free.

ParameterTypeDescription
customerIdstring10-digit account id (dashes ok) — omit to use the brand’s selected default account
includeRemovedbooleanalso list REMOVED conversion actions
loginCustomerIdstringmanager id if operating through an MCC

add_google_ads_assets#

Add sitelinks / callouts / structured snippets to a Google Ads campaign

Add SITELINKS, CALLOUTS or STRUCTURED SNIPPETS to a Google Ads campaign or ad group — and ATTACH them, which is the part that makes them do anything (an asset sitting in the account library shows nothing at all). Sitelinks are the highest-CTR free win on Search: extra links under the ad, each with its own landing page. Pass assetType plus assets[]: SITELINK needs {linkText (≤25 chars), finalUrl, and optionally description1 / description2}; CALLOUT needs {calloutText (≤25)}; STRUCTURED_SNIPPET needs {header, values[] — at least 3}. Or link assets that already exist with assetResourceNames[]. Assets and links go up in ONE atomic operation, so a rejected link never strands an orphan asset, and the links are READ BACK from Google before you are told they exist. Attaching a live asset to a LIVE (ENABLED) campaign changes what that ad shows on the very next auction — show the user what would appear, get an explicit yes, then pass confirm:true. On a paused campaign it never needs confirmation.

ParameterTypeDescription
customerIdstring10-digit account id (dashes ok) — omit to use the brand’s selected default account
assetTyperequired"SITELINK" | "CALLOUT" | "STRUCTURED_SNIPPET"what kind of asset to create and attach
level"campaign" | "adGroup"where to attach it — default campaign
campaignIdstringcampaign id (level:"campaign")
adGroupIdstringad group id (level:"adGroup")
assetsobject[]the assets to CREATE and attach
assetResourceNamesstring[]attach assets that ALREADY exist instead of creating new ones
status"ENABLED" | "PAUSED"the LINK status — default ENABLED
confirmbooleanset true ONLY after the user approved changing what a LIVE campaign shows
dryRunbooleanvalidate against Google and create NOTHING
loginCustomerIdstringmanager id if operating through an MCC

create_google_ads_performance_max_campaign#

Create a Google Ads Performance Max campaign

Build a PERFORMANCE MAX campaign — Google’s cross-surface campaign type (Search, YouTube, Display, Discover, Gmail, Maps) and the one Google pushes hardest at small advertisers. 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, because this REFUSES rather than build a campaign that cannot optimise. Creative lives in an ASSET GROUP, and Google’s minimums are enforced before anything is sent: 3–15 headlines (≤30 chars), 1–5 longHeadlines (≤90), 2–5 descriptions (≤90), one businessName (≤25), at least one LOGO (1:1), one MARKETING_IMAGE (1.91:1) and one SQUARE_MARKETING_IMAGE (1:1) — upload the images with upload_google_ads_asset first and pass their asset resource names. A YouTube video is optional (Google generates one from the asset group if you omit it). Brand guidelines: since Google Ads API v21 they are ON by default for new PMax campaigns, which means the businessName and LOGO assets are linked to the CAMPAIGN (CampaignAsset), not to the asset group — Hermoso does that for you. Leave brandGuidelinesEnabled alone unless the user wants the older asset-group layout, and pass false for that. Budget, campaign, location/language targeting, the asset group and every asset link go up in ONE ATOMIC operation — if any part is rejected, nothing at all is created — and the whole tree is READ BACK from Google before you are told it exists. Print the returned note verbatim; if it says the campaign cannot serve, say that instead of calling it finished. RETAIL / Shopping Performance Max (a Merchant Center product feed with listing groups) is NOT supported here and is refused by name.

ParameterTypeDescription
customerIdstring10-digit account id (dashes ok) — omit to use the brand’s selected default account
namerequiredstringcampaign name
dailyBudgetUsdnumberdaily budget in USD (1–100000) — or pass budgetResourceName
budgetResourceNamestringan existing budget to reuse
biddingobject
locationsstring[]place NAMES ("United States", "Toronto") — resolved for you
excludedLocationsstring[]places to block
languagesstring[]ISO codes, e.g. ["en"]
countryCodestring2-letter hint to disambiguate a city name
startDatestringYYYY-MM-DD
endDatestringYYYY-MM-DD
containsEuPoliticalAdsbooleantrue ONLY for genuine EU political advertising
brandGuidelinesEnabledbooleandefault 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
assetGrouprequiredobjectthe creative — Google requires every field above before a PMax campaign can serve
dryRunbooleanvalidate the whole tree against Google and create NOTHING
loginCustomerIdstringmanager id if operating through an MCC

Microsoft Advertising

list_microsoft_ads_campaigns#

List Microsoft Advertising accounts / campaigns read-only

Read the brand’s connected Microsoft Advertising (Bing Ads) account(s). Call with NO accountId to list the accounts shared with this brand — do this first to pick a target. Call WITH accountId 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 sets BudgetPaused, BudgetAndManualPaused and Suspended on its own, so report the status you read rather than assuming a paused campaign was paused by a person. Read-only, free. Needs Microsoft Advertising connected (Settings ▸ Connectors ▸ Microsoft Advertising).

ParameterTypeDescription
accountIdstringMicrosoft ad account id — omit to list the accounts shared with this brand

microsoft_ads_report#

Microsoft Advertising performance report read-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 ProfessionalDemographicsAudience (LinkedIn-sourced job function and industry, inside Bing), ConversionPerformance, DestinationUrlPerformance, ShareOfVoice, AssetPerformance, ProductDimensionPerformance, SearchCampaignChangeHistory ("what changed on Tuesday") and ~30 more — an unknown name is refused WITH the full list rather than forwarded. `aggregation` controls the row grain (Summary / Daily / Hourly / Weekly / Monthly / Yearly / HourOfDay / DayOfWeek). Two reports keep far less history than the usual 36 months — AssetPerformance 30 days, ShareOfVoice 6 — and the reply says so, because an empty short-retention report is a retention limit, not an absence of delivery. Window via timePeriod (Today | Yesterday | LastSevenDays | Last14Days | Last30Days | ThisWeek | LastWeek | LastFourWeeks | ThisMonth | LastMonth | LastThreeMonths | LastSixMonths | ThisYear | LastYear | ThisWeekStartingMonday | LastWeekStartingMonday | LastFourWeeksStartingMonday) or since+until (YYYY-MM-DD) — default Last30Days. An unrecognised timePeriod is REFUSED, never silently swapped for another window. Microsoft generates reports ASYNCHRONOUSLY: this can return pending:true with a reportRequestId, and you must call again rather than reporting any numbers. A report that succeeds with ZERO rows genuinely means there was no delivery in that window — say exactly that; never present zeros as measured performance. Read-only, free.

ParameterTypeDescription
accountIdstringMicrosoft ad account id — omit to use the brand’s single shared account
timePeriodstringpredefined Microsoft window, default Last30Days — must be one of the values in the description; anything else is rejected
sincestringYYYY-MM-DD custom range start (with until)
untilstringYYYY-MM-DD custom range end
columnsstring[]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 / Goal / TopVsOther in the same request.
reportTypestringwhich report — default CampaignPerformanceReportRequest. An unknown name is refused with the full list.
aggregation"Summary" | "Hourly" | "Daily" | "Weekly" | "Monthly" | "Yearly" | "HourOfDay" | "DayOfWeek" | "WeeklyStartingMonday"default Summary. Hourly accepts only Today/Yesterday or a custom range.
reportRequestIdstringpick up a report that came back pending — pass it back and this RESUMES that exact report instead of submitting a new one

MICROSOFT AD INSIGHT

microsoft_ads_keyword_ideas#

Microsoft Advertising keyword planner read-only

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, exactly as you would google_ads_keyword_ideas for Google. Unlike Google’s Keyword Planner there is NO planning-tier gate here — a developer token is sufficient. locationIds is REQUIRED and deliberately not defaulted: a search volume with no market attached is a number nobody can act on, and inventing a country would silently answer about the wrong market — use microsoft_ads_geo_search to resolve a country or city name to an id, free. SAY THIS WHEN REPORTING: Competition is Microsoft’s Low/Medium/High bucket, NOT a percentage; MonthlySearchCounts is a per-month series rather than one number; SuggestedBid is in the account currency. An empty result means Microsoft found no ideas for those seeds, never that nobody searches for them. Read-only, 0 credits.

ParameterTypeDescription
accountIdstring
keywordsstring[]seed terms to expand from
urlstringa landing page for Microsoft to mine ideas from
categoryIdnumber
locationIdsrequiredstring[]REQUIRED — Microsoft location ids (microsoft_ads_geo_search resolves names to ids, free)
languagestringdefault English
network"OwnedAndOperatedAndSyndicatedSearch" | "OwnedAndOperatedOnly" | "SyndicatedSearchOnly"
competitionstring[]filter to Low | Medium | High
minSearchVolumenumber
maxSearchVolumenumber
attributesstring[]which idea attributes to return — omit for all
expandIdeasbooleanfalse = do not expand; then keywords[] is mandatory

microsoft_ads_traffic_estimates#

Microsoft Advertising traffic estimates read-only

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 returns a MINIMUM and a MAXIMUM per keyword — quote the range, never average the two into a single figure — and every number here is a FORECAST, so never present it as measured performance. Read-only, 0 credits.

ParameterTypeDescription
accountIdstring
keywordsrequiredstring[]the keywords to estimate
maxCpcrequirednumberREQUIRED — the max CPC bid to estimate at, in the account currency
matchType"Exact" | "Phrase" | "Broad"default Exact
locationIdsrequiredstring[]REQUIRED — Microsoft location ids
languagestring
networkstring
dailyBudgetnumber

microsoft_ads_budget_opportunities#

Where Microsoft says budget is capping delivery read-only

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 real money, so it takes set_microsoft_ads_budget and an explicit yes from the user. Microsoft EXCLUDES user-paused campaigns from this analysis, so a paused campaign is absent by design rather than well-funded. Read-only, 0 credits.

ParameterTypeDescription
accountIdstring
campaignIdstringomit for the whole account

create_microsoft_ads_campaign#

Build a Microsoft Advertising campaign (paused)

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 builds the whole tree. Microsoft has NO atomic multi-object write (unlike Google), so the levels are created in sequence and the campaign is DELETED again if anything below it is rejected — you never inherit a half-built campaign. Microsoft requires 3–15 headlines (≤30 chars) and 2–4 descriptions (≤90 chars); expanded text ads can no longer be created at all. dailyBudget is in the ACCOUNT’S currency, not necessarily USD. LOCATION TARGETING: pass locations[] (country / region / city names, ISO country codes, or numeric Microsoft location ids). A Microsoft campaign has NO geo targeting unless it is set, and Microsoft does not require any — so if you pass none, the campaign IS CREATED and serves WORLDWIDE (Microsoft’s own default), and the returned note says so loudly. That is safe at this stage because the campaign is Paused and spends nothing; it is NOT safe to activate without telling the user, so relay the warning. Nothing is created when a location you DID name cannot be resolved (call microsoft_ads_geo_search to disambiguate, then pass the id). Pass worldwide:true to record that everywhere was deliberate and suppress the nudge. The locations are written and READ BACK inside the same rollback as the rest of the tree, so a campaign is either targeted as asked or does not exist. Everything is READ BACK from Microsoft before you are told it exists; print the returned note verbatim, and if it says the campaign cannot serve yet, say that rather than calling it a finished ad.

ParameterTypeDescription
accountIdstringMicrosoft ad account id — omit to use the brand’s single shared account
namerequiredstringcampaign name, ≤128 characters
dailyBudgetrequirednumberdaily budget in the account’s currency
budgetType"DailyBudgetStandard" | "DailyBudgetAccelerated" | "LifetimeBudgetStandard"default DailyBudgetStandard; Accelerated is Audience-campaign only
campaignTypestringdefault Search
timeZonestringMicrosoft time-zone enum — Microsoft requires one; default PacificTimeUSCanadaTijuana
locationsstring[]where the ads may serve — omit for worldwide (Microsoft’s default, warned about in the read-back), e.g. ["United States"] or ["Seattle, Washington, United States","CA"]. Resolved to Microsoft location ids BEFORE anything is created; an ambiguous or unknown one refuses the whole create and names it
excludeLocationsstring[]locations to EXCLUDE from the targeted set
locationIntent"PeopleInOrSearchingForOrViewingPages" | "PeopleIn"default PeopleInOrSearchingForOrViewingPages — someone OUTSIDE the target still sees the ad if they search for the place; PeopleIn restricts to people physically there
worldwidebooleanset true when the user DELIBERATELY wants to serve everywhere. Omitting locations already creates a worldwide campaign; this only records that it was intended, so the read-back stops nudging you to add locations
languagesstring[]campaign languages, e.g. ["English"]
adGroupobjectbuild the serving tree in the same call — WITHOUT this you get a campaign shell that can never show an ad

create_microsoft_ads_ad_group#

Add a Microsoft Advertising ad group

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. Leaving it Paused never needs confirmation. Read back from Microsoft before you are told it exists.

ParameterTypeDescription
accountIdstringMicrosoft ad account id — omit to use the brand’s single shared account
campaignIdrequiredstringthe campaign this ad group belongs to
namerequiredstringad group name, ≤256 characters
status"Active" | "Paused"default Paused
cpcBidnumberdefault max CPC in the account currency
languagestringrequired if the campaign has no language set
adobjectcreate the responsive search ad in the same call
keywordsobject[]
confirmbooleanREQUIRED true to create this Active under a LIVE campaign

create_microsoft_ads_ad#

Create a Microsoft responsive search ad

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 ad group and campaign are both Active, creating this Active starts REAL AD SPEND immediately — get an explicit yes and pass confirm:true. Read back from Microsoft, including its editorial status, before you are told it exists.

ParameterTypeDescription
accountIdstringMicrosoft ad account id — omit to use the brand’s single shared account
adGroupIdrequiredstringthe ad group this ad lives in
headlinesstring[]3–15 headlines, each ≤30 characters
descriptionsstring[]2–4 descriptions, each ≤90 characters
finalUrlsstring[]the landing page(s) — at least one is required
path1stringdisplay-URL path segment, ≤15 chars, no "/"
path2stringsecond display-URL path segment (only with path1)
status"Active" | "Paused"default Paused
confirmbooleanREQUIRED true to create this Active in a LIVE ad group

add_microsoft_ads_keywords#

Add Microsoft Advertising keywords

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 ignored by Microsoft — they inherit the campaign’s. Only the keywords Microsoft confirms on the read-back are reported as added.

ParameterTypeDescription
accountIdstringMicrosoft ad account id — omit to use the brand’s single shared account
adGroupIdrequiredstringthe ad group to add them to
keywordsrequiredobject[]the keywords
confirmbooleanREQUIRED true to add an Active keyword to a LIVE ad group

set_microsoft_ads_budget#

Set a Microsoft Advertising daily budget

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 back after the change.

ParameterTypeDescription
accountIdstringMicrosoft ad account id — omit to use the brand’s single shared account
campaignIdrequiredstringthe campaign whose budget changes
dailyBudgetrequirednumbernew daily budget in the account’s currency
confirmbooleanREQUIRED true to change the budget of a LIVE campaign

set_microsoft_ads_status#

Activate or pause a Microsoft Advertising campaign / ad group / ad

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 only these two statuses — its Deleted state is internal-only and cannot be SET — so to remove something use delete_microsoft_ads_object, which is a real delete operation, not a status. The resulting status is READ BACK from Microsoft before you are told it took — and Microsoft may report BudgetPaused / BudgetAndManualPaused / Suspended instead, which the note names explicitly.

ParameterTypeDescription
accountIdstringMicrosoft ad account id — omit to use the brand’s single shared account
level"campaign" | "adGroup" | "ad"what to change — default campaign
campaignIdstringcampaign id (level:"campaign")
adGroupIdstringad group id (level:"adGroup", or with adId for level:"ad")
adIdstringad id (level:"ad" — pass adGroupId too)
statusrequired"Active" | "Paused"Active = start spending; Paused = stop
confirmbooleanREQUIRED true to set Active (real spend)

delete_microsoft_ads_object#

Delete a Microsoft Advertising campaign / ad group / ad / keyword destructive

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: nothing is deleted, and you get back the object’s real name, its status and how many ad groups / ads / keywords go with it, read live from Microsoft — show the user exactly that. If the object has children, is Active, or has spent, confirming alone is NOT enough: you must also pass confirmName set to its exact name and confirmChildren set to the child count from that read-back, which is what proves you are deleting the object you think you are. A campaign that is paused, empty and never ran deletes on plain confirm:true. To stop delivery WITHOUT deleting, use set_microsoft_ads_status(status:"Paused") instead. The result is READ BACK from Microsoft: it says "deleted" only when the object no longer resolves, "not confirmed" if it does, and "could not tell" if the check itself failed — repeat that verbatim rather than claiming success.

ParameterTypeDescription
accountIdstringMicrosoft ad account id — omit to use the brand’s single shared account
level"campaign" | "adGroup" | "ad" | "keyword"what to delete — default campaign
campaignIdstringcampaign id (level:"campaign"; also the parent for level:"adGroup" if you know it)
adGroupIdstringad group id (level:"adGroup"; REQUIRED as the parent for level:"ad" and level:"keyword")
adIdstringad id (level:"ad" — pass adGroupId too)
keywordIdstringkeyword id (level:"keyword" — pass adGroupId too)
confirmbooleanREQUIRED true — the delete is permanent
confirmNamestringthe object’s EXACT name, required when it has children / is Active / has spent
confirmChildrennumberthe exact number of children reported by the unconfirmed call, required when it has any

ChatGPT Ads

list_openai_ads_campaigns#

List ChatGPT Ads account / campaigns / ad groups / ads read-only

Read the brand’s connected ChatGPT Ads account — the ads that appear below ChatGPT answers. Call with NO ids to get the ad account itself (name, currency, status, review state) plus its campaigns; with campaignId to list that campaign’s ad groups; with adGroupId to list that ad group’s ads, including each ad’s REVIEW status, which is what decides whether it can ever show. Statuses here are active / paused / archived. Read-only, free, zero spend risk. Needs ChatGPT Ads connected (Settings ▸ Connectors ▸ ChatGPT Ads): the user pastes an Advertiser API key from ChatGPT Ads Manager ▸ Settings — there is no OAuth and no manager account, and one key is scoped to one ad account.

ParameterTypeDescription
campaignIdstringlist this campaign’s ad groups
adGroupIdstringlist this ad group’s ads
limitnumberpage size, default 100
afterstringpagination cursor from a previous page

openai_ads_report#

ChatGPT Ads performance report read-only

Performance for ChatGPT Ads — impressions, clicks, spend, CTR, CPC, CPM. The scope follows the id you pass: none = the whole ad account, or campaignId / adGroupId / adId. granularity is hourly, daily, monthly or none (default daily); the default window is the last 30 days; segment by country or device for a breakdown, and level rolls the rows up by campaign / ad group / ad. A report with NO rows genuinely means there was NO delivery in that window — say exactly that; never present zeros as measured performance. Read-only and free, so run it FIRST after connecting: it proves the key works with zero spend risk.

ParameterTypeDescription
campaignIdstring
adGroupIdstring
adIdstring
sincestringYYYY-MM-DD
untilstringYYYY-MM-DD
granularity"hourly" | "daily" | "monthly" | "none"default daily
level"ad_account" | "campaign" | "ad_group" | "ad"roll rows up to this level
segment"product" | "country" | "device"extra group-by dimension (at most one)
limitnumber

create_openai_ads_campaign#

Build a ChatGPT Ads campaign (paused)

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, maxBid, contextHints, ad{creative}} and this builds the whole tree. THE CREATIVE IS A TEXT + IMAGE CARD AND NOTHING ELSE — title 3–50 characters, body 100 maximum, one landing page, one still image. THERE IS NO VIDEO ON THIS CHANNEL: never offer a video ad here, and if the brand only has video, pull a frame from it first. TARGETING IS SEMANTIC: context hints are natural-language descriptions of the conversations where this ad belongs (up to 2,000 per ad group). They guide matching, they are NOT exact-match keywords, and they do not guarantee delivery. OpenAI’s own guidance is BREADTH — many genuinely distinct hints and many distinct title/body angles beat one message repeated — which is exactly what plan_variations and mine_angles produce. OpenAI has no atomic multi-object write available here, so the whole tree is VALIDATED before the first write; if a level below the campaign is still rejected, the campaign is left PAUSED (spending nothing) and the note says exactly what exists — nothing is archived behind your back, because archiving is irreversible. Everything is READ BACK from OpenAI before you are told it exists: print the returned note verbatim, and if it says the campaign cannot serve yet, say that rather than calling it a finished ad.

ParameterTypeDescription
namerequiredstringcampaign name, at least 3 characters
descriptionstring
dailyBudgetnumberdaily cap in the AD ACCOUNT’S currency — minimum 1.00
lifetimeBudgetnumberlifetime cap in the account currency — minimum 1.00. Pass this and/or dailyBudget; a budget is required.
biddingType"impressions" | "clicks"default clicks (CPC). OpenAI suggests starting at a 3–5 max bid per click.
countriesstring[]2-letter country codes
locationIdsstring[]ids from openai_ads_geo_search — up to 2,500
startTimenumberunix seconds
endTimenumberunix seconds
adGroupobjectbuild the ad group (and its ad) in the same call — a campaign alone cannot serve

create_openai_ads_ad_group#

Add a ChatGPT Ads ad group

Add an ad group to an existing ChatGPT Ads campaign. Created PAUSED by default. Its context hints ARE the targeting on this platform: up to 2,000 natural-language descriptions of the conversations, topics or questions where this offering is relevant — not exact-match keywords, and no guarantee of delivery. Write many distinct ones rather than variations of the same phrase. 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. Leaving it paused never needs confirmation. The whole tree is READ BACK from OpenAI before you are told it exists.

ParameterTypeDescription
campaignIdrequiredstring
namerequiredstring
descriptionstring
maxBidrequirednumbermax bid in the account currency
billingEvent"click" | "impression"default click
contextHintsstring[]up to 2,000, deduplicated server-side
status"active" | "paused"default paused
confirmbooleanREQUIRED true to create this ACTIVE under a live campaign (real spend)

create_openai_ads_ad#

Create a ChatGPT Ads ad

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; creating it active inside a live ad group starts REAL AD SPEND, so that needs confirm:true. OpenAI REVIEWS every ad (usually a few minutes) and the ad is read back with its review status: until that says approved the ad CANNOT show, so report the review status rather than calling it live. The landing page is also checked against robots.txt for OAI-AdsBot / OAI-SearchBot blocks — a page that blocks those agents cannot run ChatGPT ads at all, and the note says so.

ParameterTypeDescription
adGroupIdrequiredstring
namestringinternal name — defaults to the title
creativerequiredobject
status"active" | "paused"default paused
confirmbooleanREQUIRED true to create this ACTIVE in a live ad group (real spend)

update_openai_ads_object#

Edit a ChatGPT Ads campaign / ad group / ad

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 and the creative are REPLACED WHOLESALE rather than merged, so send the complete list. Changing the budget, the bid or the creative of a LIVE (active) object changes what real money buys immediately — show the user the old and new values, get an explicit yes, then pass confirm:true. The object is READ BACK after the change.

ParameterTypeDescription
level"campaign" | "adGroup" | "ad"inferred from which id you pass
campaignIdstring
adGroupIdstring
adIdstring
namestring
descriptionstring
dailyBudgetnumber
lifetimeBudgetnumber
countriesstring[]
locationIdsstring[]
endTimenumber
contextHintsstring[]REPLACES the existing list
maxBidnumber
billingEvent"click" | "impression"required alongside maxBid — bidding is replaced wholesale
creativeobjectREPLACES the ad’s creative (text + image card only)
confirmbooleanREQUIRED true to change budget / bid / creative on a LIVE object

set_openai_ads_budget#

Set a ChatGPT Ads campaign budget

Change a ChatGPT Ads campaign’s budget — a daily cap, a lifetime cap, or both, in the ad account’s currency (OpenAI’s floor is 1.00). Raising it on a LIVE (active) campaign increases real spend immediately, so you MUST show the user the old and new amounts, get an explicit yes, then call with confirm:true. Lowering it or changing a paused campaign is safe. The campaign is READ BACK after the change and the note is built from that.

ParameterTypeDescription
campaignIdrequiredstring
dailyBudgetnumber
lifetimeBudgetnumber
confirmbooleanREQUIRED true when the campaign is live

set_openai_ads_status#

Activate, pause or archive a ChatGPT Ads campaign / ad group / ad

Turn a ChatGPT Ads campaign, AD GROUP or AD on (active), off (paused), or ARCHIVE it. Pass level:"campaign" + campaignId, level:"adGroup" + adGroupId, or level:"ad" + adId. ACTIVATING STARTS REAL AD SPEND — you MUST first show the user the object and its budget, get an explicit yes, then call with status:"active" and confirm:true. Pausing is always safe and stops all spend. ARCHIVING IS IRREVERSIBLE: it is this platform’s only teardown (there is no delete and no un-archive, and OpenAI’s own guidance is "only archive objects you have no further use for"), so it ALSO requires confirm:true — prefer pausing unless the user is certain. Remember an ad only serves when the ad, its ad group AND its campaign are all active and the ad has passed OpenAI’s review. The resulting status is READ BACK from OpenAI before you are told it took.

ParameterTypeDescription
level"campaign" | "adGroup" | "ad"inferred from which id you pass — default campaign
campaignIdstring
adGroupIdstring
adIdstring
statusrequired"active" | "paused" | "archived"active = start spending; paused = stop; archived = permanent
confirmbooleanREQUIRED true to activate (real spend) or to archive (irreversible)

delete_openai_ads_object#

Archive (ChatGPT Ads’ delete) a campaign / ad group / ad destructive

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" + adId. CALL IT WITHOUT confirm FIRST — nothing is archived and you get the object’s real name, status and child count read live from OpenAI; show the user exactly that. A target with children or live delivery additionally needs confirmName (its exact name) and confirmChildren (the count from that read-back). PAUSING stops all spend and keeps the object editable — offer that first whenever the user only wants delivery to stop. Archiving a campaign is not documented to cascade, so archive the children yourself if they should stop too. The result is READ BACK: it says archived only when OpenAI reports the archived status.

ParameterTypeDescription
level"campaign" | "adGroup" | "ad"what to archive — default campaign
campaignIdstring
adGroupIdstring
adIdstring
confirmbooleanREQUIRED true — archiving cannot be undone
confirmNamestringthe object’s EXACT name, required when it has children or is live
confirmChildrennumberthe exact number of children reported by the unconfirmed call, required when it has any

PINTEREST ADS

list_pinterest_ads_campaigns#

List Pinterest ad accounts / campaigns read-only

Read the brand’s connected Pinterest AD account(s). Call with NO adAccountId to list the ad accounts shared with this brand — 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). All money is in the AD ACCOUNT’S currency, which is not necessarily dollars. Pinterest’s own list default hides DRAFT and ARCHIVED objects; this asks for all four statuses so nothing is silently missing. Read-only, free. Needs Pinterest connected (Settings ▸ Connectors ▸ Pinterest) and the ad account ticked under Manage accounts.

ParameterTypeDescription
adAccountIdstringPinterest ad account id — omit to list the ad accounts shared with this brand
statuses"ACTIVE" | "PAUSED" | "ARCHIVED" | "DRAFT"[]

pinterest_ads_report#

Pinterest ads performance report read-only

Performance for a Pinterest ad account — spend, impressions, clicks, CTR, effective CPC and conversions, by campaign. Window via since/until (YYYY-MM-DD) and granularity. Pinterest keeps only 90 days and refuses ranges longer than 90 days (at HOUR granularity: 8 days back, 3-day windows) — this refuses those up front with the reason rather than letting Pinterest return an opaque error. A report with ZERO rows genuinely means nothing delivered in that window; say exactly that and never present zeros as measured performance. Read-only, free.

ParameterTypeDescription
adAccountIdstring
campaignIdsstring[]break down by campaign — omit for the whole ad account
sincestringYYYY-MM-DD, default 30 days ago
untilstringYYYY-MM-DD, default today
granularity"TOTAL" | "DAY" | "HOUR" | "WEEK" | "MONTH"default TOTAL
columnsstring[]Pinterest metric column names — omit for the standard set

create_pinterest_ads_campaign#

Build a Pinterest ad campaign (paused)

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 SERVE AN IMPRESSION: pass adGroup{name, targetingSpec, ad{pinId}} and this builds the whole tree. Pinterest has NO atomic multi-object write, so the levels are created in sequence and the campaign is ARCHIVED again if anything below it is rejected (Pinterest has no delete) — you never inherit a half-built campaign. Budgets are ordinary amounts in the ad account’s currency; the micro-currency conversion Pinterest requires is handled for you. Every ad group must target at least one place. Everything is READ BACK from Pinterest before you are told it exists; print the returned note verbatim, and if it says the campaign cannot serve yet, say that rather than calling it a finished ad.

ParameterTypeDescription
adAccountIdstring
namerequiredstringcampaign name, ≤255 characters
objectiverequired"AWARENESS" | "CONSIDERATION" | "WEB_CONVERSION" | "CATALOG_SALES" | "VIDEO_COMPLETION" | "APP_INSTALL" | "SALES" | "LEADS" | "CTV_CONSIDERATION"Pinterest requires an objective and will not guess one
dailyBudgetnumberdaily cap in the ad account’s currency
lifetimeBudgetnumberlifetime cap instead of a daily one — Pinterest then requires endTime
startTimenumberUnix timestamp in SECONDS
endTimenumberUnix timestamp in SECONDS
adGroupobjectbuild the serving tree in the same call — WITHOUT this you get a campaign shell that can never show an ad

create_pinterest_ads_ad_group#

Add a Pinterest ad group

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 the user what would begin serving, get an explicit yes, then pass confirm:true. Leaving it paused never needs confirmation. Read back from Pinterest before you are told it exists.

ParameterTypeDescription
adAccountIdstring
campaignIdrequiredstringthe campaign this ad group belongs to
namerequiredstringad group name
billableEvent"CLICKTHROUGH" | "IMPRESSION" | "VIDEO_V_50_MRC"default CLICKTHROUGH
bidnumbermax bid in the ad account’s currency — REQUIRED by Pinterest for AWARENESS/IMPRESSION, CONSIDERATION/CLICKTHROUGH and CATALOG_SALES/CLICKTHROUGH
budgetnumberad-group budget — only valid when the campaign is NOT budget-optimized (Pinterest optimizes at campaign level by default)
placementGroup"ALL" | "SEARCH" | "BROWSE" | "OTHER"
pacing"STANDARD" | "ACCELERATED"
targetingSpecobjectPinterest targeting object, e.g. {"GEO":["US"],"MINIMUM_AGE":"25"} — at least one GEO or LOCATION is REQUIRED by Pinterest
status"ACTIVE" | "PAUSED" | "DRAFT"default PAUSED
adobjectcreate the ad in the same call
confirmbooleanREQUIRED true to create this ACTIVE under a LIVE campaign

create_pinterest_ads_ad#

Create a Pinterest ad

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 (post_to_pinterest returns one). 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-back reports the review status and any rejection reason, and a REJECTED ad never serves until it is fixed.

ParameterTypeDescription
adAccountIdstring
adGroupIdrequiredstringthe ad group this ad lives in
pinIdrequiredstringthe numeric id of an existing Pin — a Pinterest ad PROMOTES a Pin, so create one with post_to_pinterest first if there is nothing to promote
creativeType"REGULAR" | "VIDEO" | "SHOPPING" | "CAROUSEL" | "MAX_VIDEO" | "COLLECTION" | "IDEA" | "SHOWCASE" | "QUIZ" | "COLLAGE" | "APP"default REGULAR
namestring
destinationUrlstringwhere the click goes
status"ACTIVE" | "PAUSED" | "DRAFT"default PAUSED
confirmbooleanREQUIRED true to create this ACTIVE in a live ad group

set_pinterest_ads_budget#

Set a Pinterest campaign budget

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 change.

ParameterTypeDescription
adAccountIdstring
campaignIdrequiredstringthe campaign whose budget changes
dailyBudgetnumber
lifetimeBudgetnumber
confirmbooleanREQUIRED true to change the budget of a LIVE campaign

set_pinterest_ads_status#

Activate, pause or archive a Pinterest campaign / ad group / ad destructive

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 an explicit yes, and call again with confirm:true. Pausing is always safe. The resulting status is READ BACK from Pinterest before you are told it took.

ParameterTypeDescription
adAccountIdstring
level"campaign" | "adGroup" | "ad"what to change — default campaign
campaignIdstring
adGroupIdstring
adIdstring
statusrequired"ACTIVE" | "PAUSED" | "ARCHIVED" | "DRAFT"ACTIVE = start spending; PAUSED = stop; ARCHIVED = retire (Pinterest’s delete)
confirmbooleanREQUIRED true for ACTIVE (real spend) or ARCHIVED (irreversible retirement)

delete_pinterest_ads_object#

Archive (Pinterest’s delete) a campaign / ad group / ad destructive

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 reporting history and simply drops out of the default list view. Pass level:"campaign" + campaignId, level:"adGroup" + adGroupId, or level:"ad" + adId. CALL IT WITHOUT confirm FIRST — nothing is archived and you get the object’s real name, status and child count read live from Pinterest; show the user exactly that. A target with children or live delivery additionally needs confirmName (its exact name) and confirmChildren (the count from that read-back). PAUSING is fully reversible — offer it first whenever the user only wants delivery to stop. Pinterest documents no cascade, so archive the children yourself if they should stop too.

ParameterTypeDescription
adAccountIdstringPinterest ad account id — omit to use the brand’s single shared account
level"campaign" | "adGroup" | "ad"what to archive — default campaign
campaignIdstring
adGroupIdstring
adIdstring
confirmbooleanREQUIRED true — archiving is not self-service reversible
confirmNamestringthe object’s EXACT name, required when it has children or is live
confirmChildrennumberthe exact number of children reported by the unconfirmed call, required when it has any

REDDIT ADS

list_reddit_ads_campaigns#

List Reddit ad accounts / campaigns read-only

Read the brand’s connected Reddit AD account(s). Call with NO adAccountId to list the ad accounts shared with this brand — 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, CAMPAIGN_PAUSED, REJECTED and so on). Read-only, free. Needs Reddit Ads connected (Settings ▸ Connectors ▸ Reddit Ads) and the ad account ticked under Manage accounts.

ParameterTypeDescription
adAccountIdstringReddit ad account id (a2_…) — omit to list the ad accounts shared with this brand

reddit_ads_report#

Reddit ads performance report read-only

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 currency is not necessarily dollars, so check the account. Reddit only accepts HOURLY window boundaries; plain YYYY-MM-DD dates are accepted here and snapped for you. A report with ZERO rows genuinely means nothing delivered in that window — say exactly that and never present zeros as measured performance. Read-only, free.

ParameterTypeDescription
adAccountIdstring
sincestringYYYY-MM-DD or full ISO timestamp, default 30 days ago
untilstringYYYY-MM-DD or full ISO timestamp, default today
fieldsstring[]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 default set; if you name one Reddit does not know, its error lists every valid value.
breakdowns"AD_ACCOUNT_ID" | "AD_GROUP_ID" | "AD_ID" | "CAMPAIGN_ID" | "COUNTRY" | "DATE" | "HOUR" | "DMA" | "METRO" | "CAROUSEL_CARD" | "GALLERY_ITEM_ID" | "GENDER" | "INTEREST" | "KEYWORD" | "PLACEMENT" | "OS_TYPE" | "ASSET_ID" | "REGION" | "COMMUNITY" | "LANGUAGE"[]up to 3 (4 if both COUNTRY and REGION). Omit for one aggregate row.
filterstringReddit filter expression to restrict rows to particular ids/values
timeZoneIdstringIANA zone, e.g. America/New_York

list_reddit_ads_profiles#

List the Reddit profiles an ad account can publish as read-only

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.

ParameterTypeDescription
adAccountIdstring

search_reddit_ads_targeting#

Resolve Reddit communities / geolocations / interests for targeting read-only

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:"geolocations" lists targetable places (pass country like US, or a city search) — targeting accepts a country code or one of the returned ids. kind:"interests" lists Reddit’s interest taxonomy — targeting wants the id ("pets_v3"). Read-only, free. Use this before create_reddit_ads_ad_group rather than inventing a community name.

ParameterTypeDescription
adAccountIdstring
kind"communities" | "geolocations" | "interests"default communities
querystringkeyword — required for communities, filters interests, searches cities for geolocations
countrystring2-letter country code, geolocations only
postalCodestring
limitnumbermax results, default 15

reddit_ads_forecast#

Forecast Reddit audience size and delivery read-only

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 estimates, not a guarantee — say so when reporting them.

ParameterTypeDescription
adAccountIdstring
budgetrequirednumberbudget in the ad account’s currency (not micro-currency — the conversion is handled)
objective"APP_INSTALLS" | "CATALOG_SALES" | "CLICKS" | "CONVERSIONS" | "IMPRESSIONS" | "LEAD_GENERATION" | "VIDEO_VIEWABLE_IMPRESSIONS"default CLICKS
goalType"DAILY_SPEND" | "LIFETIME_SPEND"
bidType"CPC" | "CPM" | "CPV" | "CPV6"
bidStrategy"BIDLESS" | "MANUAL_BIDDING" | "MAXIMIZE_VOLUME" | "TARGET_CPX"
bidAmountnumber
startTimestringISO 8601
endTimestring
targetingobjectsame shape as create_reddit_ads_ad_group targeting

reddit_ads_bid_suggestion#

Ask Reddit what to bid read-only

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.

ParameterTypeDescription
adAccountIdstring
budgetrequirednumberbudget in the ad account’s currency
objective"APP_INSTALLS" | "CATALOG_SALES" | "CLICKS" | "CONVERSIONS" | "IMPRESSIONS" | "LEAD_GENERATION" | "VIDEO_VIEWABLE_IMPRESSIONS"default CLICKS
bidType"CPC" | "CPM" | "CPV" | "CPV6"default CPC — must fit the campaign objective
bidStrategy"BIDLESS" | "MANUAL_BIDDING" | "MAXIMIZE_VOLUME" | "TARGET_CPX"
goalType"DAILY_SPEND" | "LIFETIME_SPEND"
startTimestring
endTimestring
currencystring
targetingobject

list_reddit_ads_posts#

List a Reddit profile’s ad posts read-only

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.

ParameterTypeDescription
adAccountIdstring
redditProfileIdrequiredstringthe Reddit profile id (t2_…) from list_reddit_ads_profiles
type"TEXT" | "IMAGE" | "VIDEO" | "CAROUSEL"
limitnumber

create_reddit_ads_post#

Create the Reddit post an ad will promote

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). For image/video/carousel pass media[] with a PUBLIC mediaUrl; Reddit fetches and validates it itself (minimum 140×140), and a video also needs a thumbnailUrl. The destination for a click rides on the media entry’s destinationUrl, NOT on the ad. Reddit’s call-to-action values are human-readable strings with spaces and capitals — "Learn More", "Shop Now", "Sign Up" — not SCREAMING_SNAKE; the error lists all of them. The post is published on the profile immediately, so show the user the exact headline and body first.

ParameterTypeDescription
adAccountIdstring
redditProfileIdrequiredstringthe Reddit profile id (t2_…) to publish as — from list_reddit_ads_profiles
type"TEXT" | "IMAGE" | "VIDEO" | "CAROUSEL"default TEXT
headlinerequiredstringthe post title — this is the ad’s headline
bodystringbody copy, TEXT posts
mediaobject[]one entry for IMAGE/VIDEO, up to 6 for CAROUSEL
callToActionstringapplies to every media entry that has none
thumbnailUrlstringrequired for VIDEO posts
allowCommentsbooleanReddit ads can carry a public comment thread — decide deliberately

update_reddit_ads_post#

Edit a Reddit ad post

Edit an existing Reddit ad post’s headline, body or comment setting. The post is already public, so an edit is publicly visible — show the user the exact new text first. A REDDIT AD POST CANNOT BE REMOVED THROUGH THE API AT ALL: Reddit publishes no delete endpoint for one and its update schema has no status, archived or deleted field (re-checked against Reddit’s own reference on 2026-08-05), so creating one is a one-way door and the only way to take it down is Reddit’s Ads Manager. Say that plainly rather than offering to delete it.

ParameterTypeDescription
adAccountIdstring
postIdrequiredstringthe post id (t3_…)
headlinestring
bodystring
allowCommentsboolean

create_reddit_ads_campaign#

Create a Reddit campaign

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 traffic to a website (there is no TRAFFIC), CONVERSIONS optimises toward pixel events and needs a working pixel, LEAD_GENERATION drives in-feed lead forms, IMPRESSIONS and VIDEO_VIEWABLE_IMPRESSIONS buy reach, APP_INSTALLS and CATALOG_SALES are for apps and product feeds. A campaign on its own can never serve: create an ad group under it, then an ad pointing at a post. The result is read back from Reddit.

ParameterTypeDescription
adAccountIdstringReddit ad account id (a2_…) — omit when only one is shared
namerequiredstring
objective"APP_INSTALLS" | "CATALOG_SALES" | "CLICKS" | "CONVERSIONS" | "IMPRESSIONS" | "LEAD_GENERATION" | "VIDEO_VIEWABLE_IMPRESSIONS"default CLICKS — which is what Reddit calls website traffic
spendCapCentsnumberlifetime spend ceiling for the whole campaign, in minor units of the ad account’s currency

create_reddit_ads_ad_group#

Create a Reddit ad group (targeting, budget, bidding, schedule)

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 requires more here than most platforms and refuses the create without it: 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 automatically when the ad account has exactly one). THE BID TYPE MUST FIT THE CAMPAIGN’S OBJECTIVE — a CLICKS campaign takes CPC and refuses CPM; Reddit’s error says which. Money is ordinary amounts in the ad account’s currency (micro-currency is handled for you). Resolve community names and interest ids with search_reddit_ads_targeting first, and consider reddit_ads_forecast + reddit_ads_bid_suggestion before committing. Everything is READ BACK from Reddit before you are told it exists — print the returned note verbatim.

ParameterTypeDescription
adAccountIdstring
campaignIdrequiredstringthe campaign this ad group belongs to
namerequiredstring
budgetrequirednumberbudget in the ad account’s currency, paired with goalType
goalType"DAILY_SPEND" | "LIFETIME_SPEND"default DAILY_SPEND
bidTyperequired"CPC" | "CPM" | "CPV" | "CPV6"must fit the campaign objective — CLICKS campaigns take CPC
bidStrategyrequired"BIDLESS" | "MANUAL_BIDDING" | "MAXIMIZE_VOLUME" | "TARGET_CPX"MANUAL_BIDDING needs bidAmount
bidAmountnumberbid in the ad account’s currency — required for paid bid types; ask reddit_ads_bid_suggestion
startTimerequiredstringISO 8601, e.g. 2026-08-15T00:00:00Z — Reddit rejects the create without one
endTimestring
conversionPixelIdstringonly needed when the ad account has more than one pixel
optimizationGoalstringcannot be changed later
savedAudienceIdstringreuse a saved audience instead of spelling targeting out — from list_reddit_ads_saved_audiences
targetingobject
scheduleobject[]weekly dayparting windows — omit to run all week

update_reddit_ads_ad_group#

Edit a Reddit ad group

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 is REPLACED by what you pass, not merged — send the whole set you want. This does NOT activate or pause anything; use set_reddit_ads_status for that. The result is read back from Reddit.

ParameterTypeDescription
adAccountIdstring
adGroupIdrequiredstring
namestring
budgetnumber
goalType"DAILY_SPEND" | "LIFETIME_SPEND"
bidAmountnumber
bidType"CPC" | "CPM" | "CPV" | "CPV6"
bidStrategy"BIDLESS" | "MANUAL_BIDDING" | "MAXIMIZE_VOLUME" | "TARGET_CPX"
startTimestring
endTimestring
savedAudienceIdstringpoint this ad group at a saved audience instead
targetingobjectsame shape as create_reddit_ads_ad_group — REPLACES the existing targeting
scheduleobject[]

create_reddit_ads_ad#

Create a Reddit ad

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. ALWAYS created PAUSED, and Reddit additionally has to APPROVE it, so a fresh ad reports effective status PENDING_APPROVAL — report that rather than calling it live. GOTCHA: a TEXT ("free form") post’s ad may not carry clickUrl at all — Reddit refuses it. The click destination for image and link ads lives on the POST’s media destinationUrl, not here.

ParameterTypeDescription
adAccountIdstring
adGroupIdrequiredstringthe ad group whose targeting and budget this ad runs under
namerequiredstring
postIdrequiredstringthe post to promote (t3_…) from create_reddit_ads_post or list_reddit_ads_posts
clickUrlstringleave unset for TEXT-post ads — Reddit rejects "Free form ads cannot have a click url"
redditProfileIdstringthe post author profile — required for catalog sales campaigns
eventTrackersobject[]third-party measurement URLs; only Reddit-approved providers are accepted

update_reddit_ads_ad#

Edit a Reddit ad

Rename a Reddit ad, point it at a different post, or change its click url. Does not activate or pause it — use set_reddit_ads_status. Swapping the post changes what people see, so confirm the new creative with the user first. The result is read back from Reddit.

ParameterTypeDescription
adAccountIdstring
adIdrequiredstring
namestring
postIdstringpromote a different post
clickUrlstringpass an empty string to clear it

update_reddit_ads_campaign#

Edit a Reddit campaign

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. This does NOT activate, pause or archive anything — use set_reddit_ads_status for that. The result is read back from Reddit before you are told it took.

ParameterTypeDescription
adAccountIdstring
campaignIdrequiredstring
namestring
spendCapnumberlifetime spend ceiling in the ad account’s currency
budgetnumber
goalType"DAILY_SPEND" | "LIFETIME_SPEND"
startTimestring
endTimestring

set_reddit_ads_status#

Activate, pause, archive or delete a Reddit campaign / ad group / ad destructive

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 the SAME blast-radius gate as delete_reddit_ads_object (call that one instead when you mean to remove something — it is the same code path and its unconfirmed call reports what goes with it). Remember Reddit’s three tiers all have to be ACTIVE for a single impression to serve: activating the campaign alone does nothing if its ad group and ad are still paused. The resulting status is READ BACK from Reddit.

ParameterTypeDescription
adAccountIdstring
kindrequired"campaign" | "ad_group" | "ad"
idrequiredstring
statusrequired"ACTIVE" | "PAUSED" | "ARCHIVED" | "DELETED"ACTIVE = start spending; PAUSED = stop; ARCHIVED = retire; DELETED = permanent, and blocked for 3h after any change
confirmbooleanREQUIRED true for ACTIVE (real spend), ARCHIVED and DELETED

delete_reddit_ads_object#

Delete or archive a Reddit campaign / ad group / ad destructive

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 modified in the last 3 hours, and the refusal names ARCHIVED as the immediate alternative. CALL IT WITHOUT confirm FIRST — nothing changes, and you get the object’s real name, status and how many ad groups / ads sit under it, read live from Reddit. A target with children or live delivery then needs confirmName (its exact name) and confirmChildren (the count from that read-back). Reddit does NOT document whether removing a campaign cascades to its ad groups and ads — each carries its own status — so remove the children yourself if they should go too. To stop delivery without removing, use set_reddit_ads_status(status:"PAUSED").

ParameterTypeDescription
adAccountIdstring
kindrequired"campaign" | "ad_group" | "ad"
idrequiredstring
status"DELETED" | "ARCHIVED"DELETED = permanent (default); ARCHIVED = out of service but reversible to PAUSED
confirmbooleanREQUIRED true
confirmNamestringthe object’s EXACT name, required when it has children or is live
confirmChildrennumberthe exact number of children reported by the unconfirmed call, required when it has any

Reddit Ads wave 2: measurement, audiences, lead forms, chang…

list_reddit_ads_pixels#

List Reddit conversion pixels (and whether they are firing) read-only

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 — if the account has none, the only fix is for the user to add it in Reddit’s Events Manager (ads.reddit.com ▸ Events Manager); never claim you can create one. Read-only, free.

ParameterTypeDescription
adAccountIdstringReddit ad account id (a2_…) — omit when only one is shared

send_reddit_ads_conversions#

Send conversions to Reddit (Conversions API)

Report conversions to Reddit server-side — purchases, leads, sign-ups, or your own custom events — so Reddit can attribute them to the ads that caused them and optimise delivery toward them. This is what makes a CONVERSIONS campaign work; without it Reddit optimises blind. Send events as close to real time as you can: Reddit REFUSES anything older than seven days, and deduplication against the browser pixel only works inside two days. Pass ordinary email addresses and phone numbers — they are canonicalised and SHA-256 hashed on our server before they reach Reddit, and a value you already hashed is passed through untouched. The more match keys per event (email, phone, clickId, uuid, externalId, IP + user agent) the better the attribution. Set conversionId on every event if you ALSO run the browser pixel, or the same purchase is counted twice. Costs no credits and spends no ad money — this is measurement. Needs the "adsconversions" permission: if Reddit answers 403, the connection predates it and the user must reconnect Reddit Ads.

ParameterTypeDescription
adAccountIdstring
pixelIdstringfrom list_reddit_ads_pixels — only needed when the account has more than one
testIdstringa test id from Events Manager ▸ Testing — events sent with it are visible there and NEVER counted in reporting
eventsrequiredobject[]up to 1,000 events per call

list_reddit_ads_audiences#

List Reddit custom audiences read-only

List the CUSTOM AUDIENCES (uploaded customer lists) on a Reddit ad account, with each one’s match-size range and status. Reddit will not deliver to an audience under about 1,000 matched redditors, and the reply says which ones fall short — an audience that is too small silently reaches nobody rather than erroring. Use an id here as customAudienceIds in ad-group targeting to retarget it, or as excludedCustomAudienceIds to suppress existing customers from a prospecting campaign. Read-only, free.

ParameterTypeDescription
adAccountIdstring
namestringfilter by name
limitnumberdefault 50, max 100

create_reddit_ads_audience#

Create a Reddit custom audience (customer list)

Create an empty custom audience on a Reddit ad account, then fill it with update_reddit_ads_audience_users. Reddit only supports ONE kind of audience through the API — an uploaded CUSTOMER LIST matched on hashed emails and mobile advertising ids; pixel-retargeting, engagement and lookalike audiences are built by Reddit itself in Ads Manager and cannot be created here. The audience arrives empty and stays unusable until it matches roughly 1,000 redditors, and Reddit takes up to 4 hours to show a size change and up to 36 hours to finish processing a list — so do not create, upload and then report success on reach in the same breath. Free.

ParameterTypeDescription
adAccountIdstring
namerequiredstringwhat this list is, e.g. "Purchasers – last 180 days"

update_reddit_ads_audience_users#

Add or remove people in a Reddit custom audience

Add people to, or remove people from, a Reddit custom audience. Pass ordinary email addresses and/or mobile advertising ids — each one is canonicalised the way Reddit specifies and SHA-256 hashed on our server before it is sent, so raw customer data never reaches Reddit, and an identifier you already hashed is passed through untouched. Up to 2,500 rows per call; send bigger lists as repeated calls and the audience accumulates. EVERY ROW MUST CARRY THE SAME FIELDS: Reddit’s upload is positional, so if some rows have an email and others do not, the values shift into the wrong column and match nobody — split those into separate calls instead. After Reddit accepts the upload the size does not move for up to 4 hours and processing can take 36, so never re-send the same batch because the count looks unchanged. Free.

ParameterTypeDescription
adAccountIdstring
customAudienceIdrequiredstringfrom create_reddit_ads_audience or list_reddit_ads_audiences
action"ADD" | "REMOVE"default ADD
usersrequiredobject[]up to 2,500 rows; every row must carry the same fields

delete_reddit_ads_audience#

Delete a Reddit custom audience destructive

Permanently delete a Reddit custom audience. This is one of the very few things Reddit really deletes — campaigns, ad groups and ads are only ever archived — and it cannot be undone: the uploaded list is gone and any ad group targeting it loses that audience. Confirm-gated: show the user the audience name and its size, get an explicit yes, then call again with confirm:true.

ParameterTypeDescription
adAccountIdstring
customAudienceIdrequiredstring
confirmbooleanREQUIRED true — the deletion is permanent

list_reddit_ads_saved_audiences#

List Reddit saved audiences read-only

List the SAVED AUDIENCES on a Reddit ad account — named, reusable targeting definitions (communities, interests, geos, devices and so on) that an ad group can point at instead of repeating the whole block. The reply says how many live ad groups each one is attached to, which is what makes editing one a decision rather than a formality. Read-only, free.

ParameterTypeDescription
adAccountIdstring
limitnumberdefault 50, max 100

create_reddit_ads_saved_audience#

Create a reusable Reddit saved audience

Save a targeting definition under a name so every ad group can reuse it — define "our people" once, then pass savedAudienceId when creating ad groups instead of retyping communities and interests each time, and one later edit re-targets every ad group using it. Takes the same targeting block as create_reddit_ads_ad_group, so resolve community names and interest ids with search_reddit_ads_targeting first. Creates targeting only: no budget, no spend. Free.

ParameterTypeDescription
adAccountIdstring
namerequiredstring
targetingrequiredobjectsame shape as create_reddit_ads_ad_group targeting — an empty block is refused, because a saved audience IS its targeting

update_reddit_ads_saved_audience#

Edit a Reddit saved audience

Rename a Reddit saved audience or replace its targeting. Targeting is REPLACED, never merged — send the whole set you want. Editing one that live ad groups already use re-targets all of them immediately, so say how many are affected and get a yes before changing targeting on a running account. The result is read back from Reddit.

ParameterTypeDescription
adAccountIdstring
savedAudienceIdrequiredstring
namestring
targetingobjectREPLACES the existing targeting

delete_reddit_ads_saved_audience#

Delete a Reddit saved audience destructive

Delete a Reddit saved audience — the named, reusable targeting block ad groups point at. Reddit publishes NO delete verb for one (its whole Ads API has four, and this is not among them), so removal is its `status` field set to DELETED. THE BLAST RADIUS IS THE AD GROUPS USING IT: every live ad group pointing at this audience loses that targeting definition the moment it goes, and Reddit’s own `active_ad_groups_count` is what says how many. CALL IT WITHOUT confirm FIRST — nothing is deleted and you get its real name and that count; show the user exactly that, and if any ad group uses it you must then pass confirmName (its exact name) and confirmChildren (the count). The result is READ BACK from Reddit.

ParameterTypeDescription
adAccountIdstring
savedAudienceIdrequiredstring
confirmbooleanREQUIRED true
confirmNamestringthe audience’s EXACT name, required when live ad groups use it
confirmChildrennumberthe exact number of live ad groups reported by the unconfirmed call

list_reddit_ads_lead_forms#

List Reddit lead generation forms read-only

List the lead generation forms on a Reddit ad account, with the fields each one asks for. Reddit publishes NO endpoint for reading the leads a form has collected — the user downloads those from Reddit’s Ads Manager. Say that plainly if asked for the leads themselves; do not imply they can be fetched. Read-only, free.

ParameterTypeDescription
adAccountIdstring
limitnumberdefault 50, max 100

create_reddit_ads_lead_form#

Create a Reddit lead generation form

Create a lead generation form — the in-feed form redditors fill in without leaving Reddit, used by LEAD_GENERATION campaigns. Reddit requires a link to a real privacy policy on every form. Ask for the FEWEST fields that make a lead useful: every extra question costs completions. KNOW THE LIMIT BEFORE YOU PROMISE ANYTHING: Reddit exposes no way to attach a form to an ad through the API — there is no lead-form field on an ad, an ad group or a post — so the user picks this form in Reddit’s Ads Manager when building the creative, and downloads its leads from there. There is also no update and no delete, so get the questions right the first time. Free.

ParameterTypeDescription
adAccountIdstring
namerequiredstringinternal name — redditors do not see it
promptrequiredstringthe line shown above the form telling people what they are signing up for
privacyLinkrequiredstringfull https:// URL to your privacy policy — Reddit requires it
questionsrequiredobject[]at least one

reddit_ads_history#

Reddit ad account changelog read-only

Read the CHANGELOG for a Reddit ad account — what was changed, from what to what, by which member, and when. This is the tool for "performance fell off a cliff on Tuesday, what changed?" and for auditing what an agent or a teammate actually did. Call it with nothing but the ad account to get every change; narrow it with a date window, change types (BUDGET, BID, STATUS, TARGETING…) or specific campaign / ad group / ad ids. An empty result genuinely means nothing was changed in that window — say that, do not read it as missing data. Read-only, free.

ParameterTypeDescription
adAccountIdstring
sincestringYYYY-MM-DD or full ISO timestamp
untilstringYYYY-MM-DD or full ISO timestamp
changeTypes"AD_ACCOUNT" | "AD" | "AD_GROUP" | "AUDIENCE" | "BID" | "BUDGET" | "CAMPAIGN" | "STATUS" | "TARGETING"[]
entityType"AD" | "AD_GROUP" | "CAMPAIGN"required when you pass entityIds
entityIdsstring[]restrict to these objects
includeChildEntitiesbooleanalso return changes to what lives under those objects
memberIdsstring[]restrict to changes made by these Reddit members
limitnumberdefault 50, max 200

LINKEDIN COMPANY PAGES + ADS

list_linkedin_pages#

List the LinkedIn company Pages this account administers read-only

List the LinkedIn COMPANY PAGES the connected account administers — id, name and the role held on each. ALWAYS call this before post_to_linkedin_page when there is more than one Page: publishing to the wrong company Page is a public mistake and Hermoso never chooses for the user. If it comes back empty, the account holds no Page admin role, or LinkedIn has not granted this app the organization scopes — say that plainly rather than guessing an id. Read-only, free.

No parameters.

post_to_linkedin_page#

Publish to a LinkedIn company Page

Publish a post to one of the user’s LinkedIn COMPANY PAGES — text, plus optionally an image, a video, or a 2–20 image CAROUSEL (LinkedIn calls it a MultiImage post; pass the slides in order as imageUrls[]). The media need not be a Hermoso render — it must be Hermoso-HOSTED because we upload the bytes to LinkedIn ourselves, and upload_file turns ANY file the user already has into such a URL. ORGANIC CAROUSELS ARE COMPANY-PAGE ONLY — a personal profile cannot publish one and is refused by name, so send a deck here rather than to post_to_linkedin. This is a DIFFERENT thing from post_to_linkedin, which publishes to the person’s own profile: pick the one the user actually asked for and never substitute. organizationId comes from list_linkedin_pages; omit it only when the account administers exactly one Page. This PUBLISHES immediately and PUBLICLY — ALWAYS show the user the exact text and get an explicit yes BEFORE calling. LinkedIn does NOT allow the image or video of a published post to be swapped afterwards, so get the visual right first (the copy can still be edited with manage_linkedin_post).

ParameterTypeDescription
hookstringWHAT ANGLE THIS POST IS BUILT ON — the single most valuable field here, and the only moment it can ever be recorded. post_performance groups on it to answer "which hooks work", and it needs 5 posts sharing ONE hook before it will call anything a winner, so REUSE THE SAME WORDING across a campaign instead of rephrasing it every time. Best of all, pass a hook id from list_hooks (e.g. "direct_callout", "mid_problem", "before_after") — those fold onto a stable key however they are spelled, so a whole brand accumulates evidence on one row. Your own wording is fine too; it just only groups when you repeat it exactly. Omitting it means this post can never vote on which hook works.
subjectstringWHAT THIS POST IS ABOUT — the product, feature, offer or theme (e.g. "winter coat", "free trial", "founder story"). The second grouping axis in post_performance. Same rule as hook: reuse the exact wording so posts about one subject land in one group.
organizationIdstringnumeric Page id from list_linkedin_pages
textrequiredstringthe post text
imageUrlstringa Hermoso-hosted image URL — a render (list_library), or ANY image of the user’s own passed through upload_file first. An arbitrary external host is refused.
videoUrlstringa Hermoso-hosted video URL — a render, or the user’s own footage via upload_file. LinkedIn processes it before publishing, which takes a minute.
imageUrlsstring[]CAROUSEL — an ORDERED list of image (and, where the channel allows, video) URLs published as ONE post the viewer swipes through. THIS IS NOT “post several” — it is a single post with several slides, which is what a multi-slide creative (a listicle, a “1/6 · SWIPE” deck) actually needs; publishing only its first slide tells the viewer to swipe at something that cannot. The ORDER is the product. Limits per channel: Instagram 2–10 (images, videos or a mix), Threads 2–20 (mix allowed), Facebook 2+ (Meta publishes no documented maximum; Hermoso caps the upload fan-out at 30 and says so), LinkedIn company Pages 2–20 (images only), Pinterest 2–5 (images only), TikTok up to 35. One url here is simply an ordinary single post. Anything a channel cannot do is REFUSED with the real reason — nothing is ever quietly downgraded to one slide.
idempotencyKeystringSAFE RETRIES. Publishing can take minutes (a video upload, a carousel of ten slides) and a transport can time out while the post SUCCEEDS — retrying blind is how the same thing gets posted twice. Pass any stable string here and a repeat of the SAME publish returns the ORIGINAL post id instead of posting again (24h). You do not have to: an identical publish is auto-recognised for 10 minutes anyway. If a call times out or errors ambiguously, CALL AGAIN WITH THE SAME KEY — that is the safe move, and it will either report the original post or publish it for the first time. It never posts twice.
allowDuplicatebooleanpost it even though an identical post was just made or attempted. Only pass this when the user genuinely wants the same thing posted twice, or when you have LOOKED at the account and confirmed a timed-out attempt did not land.
altTextstring
titlestringvideo title
visibility"PUBLIC" | "CONNECTIONS"default PUBLIC

manage_linkedin_post#

Edit or delete a LinkedIn post destructive

Edit or delete a published LinkedIn post — personal profile or company Page. Pass postUrn, the full urn returned when it was published. action:"edit" changes ONLY THE COPY: LinkedIn does not allow the image or video of a published post to be replaced, so a new visual means a NEW post — tell the user that instead of promising a swap. action:"delete" is immediate and public and requires confirm:true.

ParameterTypeDescription
postUrnrequiredstringthe full LinkedIn post urn returned by publishing
actionrequired"edit" | "delete"
textstringthe new copy, for action:"edit"
confirmbooleanREQUIRED true to delete

linkedin_page_analytics#

Organic performance of a LinkedIn company Page read-only

ORGANIC performance for one of the brand’s LinkedIn COMPANY PAGES: total followers, followers gained (organic vs paid) across the window, Page views (all / unique / desktop / mobile), and the impressions, unique impressions, clicks, likes, comments, shares and engagement rate of the Page’s posts. This is what answers “is our LinkedIn actually working” and “did that post land”. It is NOT linkedin_ads_report — that covers PAID campaigns; LinkedIn excludes sponsored activity from these figures entirely. Pass postUrns (the urn:li:share:… / urn:li:ugcPost:… that post_to_linkedin_page returned) for PER-POST numbers; LinkedIn forbids a date range together with named posts, so that switches to lifetime-per-post. Only Pages the user ticked in Manage accounts are readable — a Page the account merely administers is refused, by design. LinkedIn keeps 12 months, follower figures run about 2 days behind, and it OMITS posts with no recorded activity rather than returning zeros: report an absent post or an unavailable section as MISSING data, never as zero. Read-only, 0 credits. Needs LinkedIn connected with the organization scopes.

ParameterTypeDescription
organizationIdstringnumeric Page id from list_linkedin_pages — omit only when exactly one Page is shared with this brand
startDatestringYYYY-MM-DD, default 28 days ago (LinkedIn keeps 12 months)
endDatestringYYYY-MM-DD, default today
postUrnsstring[]urn:li:share:… / urn:li:ugcPost:… — switches to per-post lifetime numbers instead of the Page total

list_linkedin_ads_campaigns#

List LinkedIn ad accounts / campaigns read-only

Read the LinkedIn ad accounts this connection can reach, and — with adAccountId — that account’s campaign groups and campaigns: name, status, objective, budgets, and LinkedIn’s own servingStatuses, which explain WHY something is not delivering (billing hold, start-date hold, parent-status hold). LinkedIn’s Advertising API is an approval-gated product, and on its Development tier each ad account must ALSO be mapped to the app in LinkedIn’s Developer Portal — so if nothing is reachable, say that rather than implying the user has no ad account. Read-only, free.

ParameterTypeDescription
adAccountIdstringLinkedIn ad account id — omit to list the reachable accounts
campaignIdstringalso return the CREATIVES (the actual ads) under this campaign, each with its intendedStatus, whether it isServing, and LinkedIn’s own servingHoldReasons
campaignIdsstring[]same, for several campaigns at once
statuses"ACTIVE" | "PAUSED" | "ARCHIVED" | "DRAFT"[]

linkedin_ads_report#

LinkedIn ads performance report read-only

LinkedIn ad performance — impressions, clicks, cost, website conversions, leads and social actions — pivoted by CAMPAIGN (default), CAMPAIGN_GROUP, CREATIVE, ACCOUNT, CONVERSION, PLACEMENT_NAME, IMPRESSION_DEVICE_TYPE, SERVING_LOCATION… or by AUDIENCE DEMOGRAPHICS: MEMBER_COMPANY_SIZE, MEMBER_INDUSTRY, MEMBER_SENIORITY, MEMBER_JOB_TITLE, MEMBER_JOB_FUNCTION, MEMBER_COUNTRY_V2, MEMBER_REGION_V2, MEMBER_COMPANY. The MEMBER_* pivots are what LinkedIn is uniquely good at — job title, seniority and company size are targeting dimensions no other platform reports — and LinkedIn allows exactly ONE pivot per report, so ask for them one at a time and join the answers yourself. An unknown pivot or granularity is refused BY NAME rather than forwarded. On a demographic pivot LinkedIn returns only the top 100 values, DROPS any value under 3 events (so the rows will not sum to the campaign total) and lags 12–24 hours behind the performance numbers — the note says so, every time. Window via since/until (YYYY-MM-DD). ZERO rows genuinely means no delivery in that window; say exactly that and never present zeros as measured performance. A LinkedIn TEST ad account NEVER returns analytics, and the note says so when that is what you are looking at. Read-only, free.

ParameterTypeDescription
adAccountIdstring
campaignIdsstring[]
sincestringYYYY-MM-DD, default 30 days ago
untilstringYYYY-MM-DD
pivotstringCAMPAIGN (default), CAMPAIGN_GROUP, CREATIVE, ACCOUNT, CONVERSION, PLACEMENT_NAME, IMPRESSION_DEVICE_TYPE, SERVING_LOCATION, or one MEMBER_* demographic pivot — an unknown value is refused with the full list
granularity"ALL" | "DAILY" | "MONTHLY" | "YEARLY"default ALL
fieldsstring[]metric names — omit for the standard set (LinkedIn returns ONLY impressions and clicks if none are named)

LINKEDIN PLANNING READS

linkedin_audience_count#

How many LinkedIn members a targeting spec reaches read-only

HOW MANY LINKEDIN MEMBERS a targeting spec reaches, before any budget is committed — the cheapest sanity check there is on a B2B audience, and it needs no ad account. Pass locations plus optional include:{titles, industries, seniorities, staffCountRanges, jobFunctions, skills, …}; search_linkedin_ads_targeting resolves any of those names to the URNs LinkedIn demands, free. THE CRITICAL THING TO SAY WHEN REPORTING: a returned total of 0 means the audience is UNDER 300 PEOPLE, not that it is empty — LinkedIn suppresses any count below 300 to protect member privacy, and 300 is also the minimum audience a campaign may run against, so a 0 means this targeting is too narrow to advertise to. The figure is a rounded approximation, so quote it as an estimate and never as a headcount. Read-only, 0 credits.

ParameterTypeDescription
locationsstring[]geo URNs or bare geo ids, e.g. ["103644278"] for the United States
includeobjectmore facets ANDed onto locations, e.g. {titles:[…], industries:[…], seniorities:[…], staffCountRanges:[…]}
targetingCriteriaobjectLinkedIn’s raw targeting object — overrides locations/include

linkedin_bid_pricing#

LinkedIn suggested bid and budget range read-only

LinkedIn’s OWN suggested bid and daily-budget range for a specific audience — the suggested bid with a low/mid/high range, the hard bid limits, and the minimum, default and maximum daily budget, all in the ad account’s currency. Use it before proposing a number to a user instead of guessing what LinkedIn costs, and pair it with linkedin_audience_count to answer "can we afford this audience?" in one go. Below LinkedIn’s minimum bid it says delivery "may be poor" for Sponsored Update campaigns and is impossible for every other format. These are ESTIMATES for this audience, not prices, and nothing is committed until a campaign is activated with set_linkedin_ads_status(confirm:true). Read-only, 0 credits.

ParameterTypeDescription
adAccountIdstring
locationsstring[]
includeobject
targetingCriteriaobject
campaignType"TEXT_AD" | "SPONSORED_UPDATES" | "SPONSORED_INMAILS"default SPONSORED_UPDATES
bidType"CPM" | "CPC" | "CPV"default CPM
matchType"EXACT" | "AUDIENCE_EXPANDED"default EXACT
objectiveTypestringoptional — LinkedIn prices some objective/optimization combinations and not others
currencystring
dailyBudgetnumber
countryCodestring

create_linkedin_ads_campaign_group#

Create a LinkedIn campaign group (draft)

Create a LinkedIn CAMPAIGN GROUP — the container LinkedIn has required every campaign to live inside since 2020. Created DRAFT, which is LinkedIn’s own structural safety net: it REFUSES to hold an ACTIVE campaign inside a DRAFT group, so while the group is a draft nothing beneath it can serve whatever its own status says. Creating it ACTIVE removes that protection and therefore requires confirm:true. LinkedIn REQUIRES a run schedule on a campaign group: it starts today unless you pass startDate, and if you set totalBudget you MUST also pass endDate — that pairing is LinkedIn’s own rule and it is refused here before anything is created. Read back from LinkedIn before you are told it exists.

ParameterTypeDescription
adAccountIdstring
namerequiredstringcampaign group name
totalBudgetnumberoptional group-level total budget, in the ad account’s currency
currencyCodestringmust match the ad account’s currency or LinkedIn refuses it
status"DRAFT" | "ACTIVE" | "PAUSED" | "ARCHIVED"default DRAFT
startDatestringYYYY-MM-DD — when campaigns under this group may start; defaults to today
endDatestringYYYY-MM-DD — REQUIRED by LinkedIn whenever totalBudget is set
runScheduleobjectLinkedIn raw runSchedule object, passed through; startDate/endDate are easier
confirmbooleanREQUIRED true to create it ACTIVE

create_linkedin_ads_campaign#

Create a LinkedIn campaign (draft)

Create a LinkedIn campaign inside an existing campaign group. Created DRAFT — it spends NOTHING until activated with set_linkedin_ads_status(confirm:true) — and a campaign on its own carries no creative, so it cannot serve an impression. Budget amounts are in the ad account’s currency; tell the user that LinkedIn may spend UP TO 150% of a daily budget on a high-opportunity day before they pick a number. Two LinkedIn behaviours to repeat rather than hide: on manual, target-cost or cost-cap bidding a unitCost of 0 means the campaign never delivers, and LinkedIn DEFERS some validation on DRAFT objects, so a clean create can still fail at activation — never promise it will run. TARGETING IS MANDATORY on LinkedIn — a campaign with no audience is refused outright — so pass locations (and optionally include/exclude facets like titles, industries, seniorities or staffCountRanges), or a raw targetingCriteria. Resolve every targeting value with search_linkedin_ads_targeting first: they are opaque URNs and MUST NOT be invented. LinkedIn’s own enums for type, objectiveType and costType are passed straight through, and LinkedIn’s refusal is surfaced verbatim if one is wrong. Read back before you are told it exists.

ParameterTypeDescription
adAccountIdstring
campaignGroupIdrequiredstringthe campaign group this campaign lives in — LinkedIn requires one
namerequiredstring
typestringLinkedIn campaign type, e.g. SPONSORED_UPDATES
objectiveTypestringLinkedIn objective, e.g. WEBSITE_VISIT
costTypestringCPM / CPC / CPV
dailyBudgetnumber
totalBudgetnumber
unitCostnumberthe bid
currencyCodestring
localeobject
countrystringcampaign locale country, default US — this is the ad UI language market, NOT geo targeting. NEVER derive it from where the ad should run: targeting Canada still uses the US/en locale (LinkedIn refuses en_CA). Leave it alone unless the user explicitly asks for a different interface language.
languagestringcampaign locale language, default en
locationsstring[]REQUIRED unless targetingCriteria is given — geo URNs or bare geo ids from search_linkedin_ads_targeting
includeobjectfurther targeting facets ANDed onto locations, e.g. {titles:[…], industries:[…], seniorities:[…], staffCountRanges:[…]}
excludeobjectfacets to exclude, same shape
excludeLocationsstring[]
targetingCriteriaobjectLinkedIn’s raw targeting object — passed through and overrides locations/include/exclude
startDatestringYYYY-MM-DD; defaults to today
endDatestringYYYY-MM-DD; omit for an open-ended run
runScheduleobject
organizationIdstringthe LinkedIn company Page this campaign advertises — LinkedIn REQUIRES it for Sponsored Content, Dynamic and Lead Gen campaigns
formatstring
optimizationTargetTypestring
audienceExpansionEnabledboolean
offsiteDeliveryEnabledbooleanalso serve on the LinkedIn Audience Network; default false
politicalIntent"POLITICAL" | "NOT_POLITICAL" | "NOT_DECLARED"
status"DRAFT" | "ACTIVE" | "PAUSED" | "ARCHIVED"default DRAFT
confirmbooleanREQUIRED true to create it ACTIVE under a LIVE campaign group

set_linkedin_ads_budget#

Set a LinkedIn campaign budget

Change a LinkedIn campaign’s daily and/or total budget. On a LIVE (ACTIVE) campaign this changes real spend immediately — and LinkedIn can spend up to 150% of a daily budget on a high-opportunity day — so show the user the new amount, get an explicit yes, then call with confirm:true. The currency must match the ad account’s. Read back after the change.

ParameterTypeDescription
adAccountIdstring
campaignIdrequiredstring
dailyBudgetnumber
totalBudgetnumber
currencyCodestring
confirmbooleanREQUIRED true to change the budget of a LIVE campaign

set_linkedin_ads_status#

Activate or pause a LinkedIn campaign group / campaign / ad

Turn a LinkedIn campaign group, campaign or CREATIVE (the ad itself) on (ACTIVE) or off (PAUSED). Pass level:"campaign" + campaignId, level:"campaignGroup" + campaignGroupId, or level:"creative" + creativeId. All three tiers must be ACTIVE for an ad to serve — activating only the campaign leaves a DRAFT creative sitting there showing nothing. ACTIVATING STARTS REAL AD SPEND — you MUST first show the user the campaign and its budget, get an explicit yes, then call with status:"ACTIVE" and confirm:true. Pausing is always safe. The resulting status is READ BACK from LinkedIn along with its servingStatuses before you are told it took: LinkedIn defers validation on drafts, so activation is exactly where a hidden problem surfaces, and the note reports what LinkedIn actually says rather than what was requested.

ParameterTypeDescription
adAccountIdstring
level"campaign" | "campaignGroup" | "creative"default campaign
campaignIdstring
campaignGroupIdstring
creativeIdstring
statusrequired"ACTIVE" | "PAUSED" | "ARCHIVED" | "DRAFT"
confirmbooleanREQUIRED true to set ACTIVE (real spend)

search_linkedin_ads_targeting#

Find LinkedIn targeting URNs read-only

Look up LinkedIn TARGETING entities by name and get their URNs — locations, job titles, industries, seniorities, company sizes, skills, job functions, interests, employers, degrees, fields of study, member behaviours. LinkedIn’s targeting values are opaque URNs (urn:li:geo:103644278 is the United States) with no guessable form, so ALWAYS resolve an audience here before passing it to create_linkedin_ads_campaign, and NEVER invent a URN — a made-up one either 400s or, worse, targets somebody else. If nothing matches, say so plainly. Read-only, free.

ParameterTypeDescription
adAccountIdstring
facetstringfacet name, default "locations" — e.g. locations, titles, industries, seniorities, staffCountRanges, skills, jobFunctions, interests, employers, degrees, fieldsOfStudy, memberBehaviors
queryrequiredstringthe name to search for, e.g. "United States", "Software Engineer", "Marketing"
languagestringdefault en
countrystringdefault US

create_linkedin_ads_creative#

Create a LinkedIn ad (creative, draft)

Create the AD ITSELF on LinkedIn — a CREATIVE inside an existing campaign. A LinkedIn campaign holds no copy and no visual, so until this runs the campaign CANNOT show an impression no matter what its status says; say that rather than calling a campaign "live". Two ways in: pass postUrn to sponsor a post that already exists (LinkedIn’s "boost this post"), or pass text and/or imageUrl / videoUrl to author a DIRECT SPONSORED CONTENT post — a real post by a company Page the user administers that is NEVER shown on the Page’s feed and exists only as an ad. Created DRAFT: it spends nothing until you activate it with set_linkedin_ads_status(level:"creative", status:"ACTIVE", confirm:true). Creating it ACTIVE under an already-live campaign starts REAL AD SPEND on the very next auction and therefore requires confirm:true. The whole tree — group, campaign and every creative — is read back from LinkedIn before you are told anything exists.

ParameterTypeDescription
adAccountIdstring
campaignIdrequiredstringthe campaign this ad belongs to
namestringthe creative’s name in Campaign Manager
postUrnstringsponsor an EXISTING post — urn:li:share:… / urn:li:ugcPost:… (what post_to_linkedin_page returned)
organizationIdstringthe company Page that authors the Direct Sponsored Content post; omit only when the connection administers exactly one Page
textstringthe ad copy
imageUrlstringa Hermoso-hosted image to attach — a render, or the user’s OWN creative put through upload_file first (an arbitrary external host is refused)
videoUrlstringa Hermoso-hosted video to attach — a render, or the user’s own footage via upload_file
titlestring
altTextstring
allowReshareboolean
intendedStatus"DRAFT" | "ACTIVE" | "PAUSED" | "ARCHIVED"default DRAFT
confirmbooleanREQUIRED true to create it ACTIVE under a LIVE campaign (real spend)

delete_linkedin_ads_object#

Delete a LinkedIn campaign group / campaign / ad destructive

Delete a LinkedIn campaign group, campaign or creative (level:"creative" + creativeId). LinkedIn HARD-deletes only DRAFT objects; anything that has ever run is moved to PENDING_DELETION instead — it stops serving and its reporting history is retained. The returned note says which of the two actually happened, and you must repeat that rather than claiming a clean delete. Irreversible either way, so it requires confirm:true.

ParameterTypeDescription
adAccountIdstring
level"campaign" | "campaignGroup" | "creative"default campaign
campaignIdstring
campaignGroupIdstring
creativeIdstring
confirmrequiredbooleanREQUIRED true — this is irreversible

update_meta_object#

Edit a Meta campaign / ad set / ad

Update an EXISTING campaign, ad set, or ad — rename, change its daily budget, retarget (ad sets), or change status (PAUSED / ACTIVE / ARCHIVED). Pass objectId (from list_meta_ads) + adAccountId. Setting something ACTIVE can start REAL AD SPEND — show the user what will run + its budget, get a yes, then pass confirm:true. Pausing / renaming / archiving is always safe.

ParameterTypeDescription
objectIdrequiredstringthe campaign / ad set / ad id (from list_meta_ads)
adAccountIdrequiredstringad account id (for auth + scope)
namestringnew name
status"ACTIVE" | "PAUSED" | "ARCHIVED"ACTIVE starts spend (needs confirm:true); PAUSED / ARCHIVED are safe
dailyBudgetUsdnumbernew daily budget in USD (1–10000; ad-set or campaign level)
targetingobjectreplacement targeting spec (ad sets) — a Meta targeting object
confirmbooleanREQUIRED true ONLY to set status ACTIVE (real spend)

delete_meta_object#

Delete a Meta campaign / ad set / ad destructive

PERMANENTLY delete a campaign, ad set, or ad. Pass objectId (from list_meta_ads) + adAccountId. DELETING A CAMPAIGN ALSO DELETES EVERY AD SET AND AD UNDER IT, and deleting an ad set deletes its ads — one id, the whole tree. Call it WITHOUT confirm first: it reports what the object is, its name, and how many children go with it. Show the user that, get an unambiguous yes, then call again with confirm:true plus confirmChildren set to the number it reported (only needed when there is at least one child). To stop delivery without deleting anything, use update_meta_object(status:"PAUSED") instead — that is reversible and this is not.

ParameterTypeDescription
objectIdrequiredstringthe campaign / ad set / ad id to delete
adAccountIdrequiredstringad account id (for auth + scope)
confirmbooleanREQUIRED true — deletion is permanent
confirmChildrennumberthe number of child ad sets + ads this delete also destroys, as reported by the unconfirmed call — required whenever that is above zero

manage_meta_post#

Edit or delete a published post destructive

Edit the text of, or delete, a post you published with post_to_meta. target:"facebook" → edit the message (action:"edit", message:…) OR delete (action:"delete"); target:"threads" → delete only (Threads has no edit API); Instagram posts can’t be edited or deleted via the API. Deleting is permanent — confirm with the user, then pass confirm:true.

ParameterTypeDescription
postIdrequiredstringthe post id returned by post_to_meta
actionrequired"edit" | "delete"edit the text (FB only) or delete the post
target"facebook" | "threads" | "instagram"default facebook
messagestringthe new post text (action:"edit" on facebook)
confirmbooleanREQUIRED true to delete (permanent)