Build and run paid campaigns across twelve ad platforms. Everything is created paused.
MERCHANT SETTINGS, THE WRITE HALF
manage_merchant_settings#
Change Merchant Center settings destructive
FIX THE MERCHANT CENTER SETTINGS that stop an account serving (merchant_account_status names them). Pick section + action. shipping: get · upsert (add or update services by name; services you do not mention are KEPT) · remove (serviceNames) · replace (exactly this list). A service is {name, countries, currency, rate, freeOver?, handlingDays:[min,max], transitDays:[min,max], active?} or Google's own Service shape; delivery days are required, never guessed. returns: list · create (policy {countries, returnPolicyUri, days or policyType, itemConditions, returnMethods, processRefundDays, returnLabelSource, label?}) · delete (returnPolicyId). Google has no edit for a return policy. business_info: get · update (address, customerService {uri,email,phone}, koreanBusinessRegistrationNumber; merged field by field, null clears). homepage: get · claim (re-checks verification; overwrite:true takes it from another account) · unclaim · set_uri (removes the claim). programs: list · enable · disable (program e.g. free-listings, shopping-ads). Overwriting or removing a shipping service, replacing existing business info, deleting a return policy, unclaiming, changing a claimed homepage, overwrite:true and disabling an enabled program each answer with what would change and need confirm:true. Every write is read back from Google. Free.
| Parameter | Type | Description |
|---|
merchantCenterIdrequired | string | from list_merchant_accounts |
sectionrequired | "shipping" | "returns" | "business_info" | "homepage" | "programs" | shipping, returns, business_info, homepage or programs; the Merchant Center area to act on |
action | "get" | "upsert" | "remove" | "replace" | "list" | "create" | "delete" | "update" | "claim" | "unclaim" | "set_uri" | "enable" | "disable" | defaults to the section's read (get/list) |
services | object[] | shipping upsert/replace |
serviceNames | string[] | shipping remove |
warehouses | object[] | shipping: Google Warehouse objects; omitted = existing ones kept |
policy | object | returns create |
returnPolicyId | string | returns delete, from list |
address | object | business_info: regionCode, street or addressLines, locality, administrativeArea, postalCode |
customerService | object | business_info: uri, email, phone (E.164) |
koreanBusinessRegistrationNumber | string | Korean business registration number; business_info update only |
uri | string | homepage set_uri |
overwrite | boolean | homepage claim: take the claim from another account |
program | string | programs enable/disable |
confirm | boolean | true ONLY after the user approved the change the first call described |
merchant_quota#
Merchant Center quota and limits read-only
Merchant Center API quota and account limits, which is the difference between "the API refused" and "you are out of daily quota". Two different things in one answer: quota groups are how many API CALLS each method group has left today, and account limits are how many PRODUCTS the account may hold per destination. Google resets the daily quota at 12:00 PM MIDDAY UTC, not at midnight. If one half of the read fails it comes back null with a warning, which means unknown rather than zero. Read-only, free.
| Parameter | Type | Description |
|---|
merchantCenterIdrequired | string | from list_merchant_accounts |
manage_merchant_notifications#
Merchant Center push notifications destructive
PUSH INSTEAD OF POLLING for Merchant Center product status. Google POSTs to a URL the moment a product's status changes — a disapproval is otherwise found whenever someone happens to run list_merchant_issues, which for a feed of thousands means money burning unnoticed. action:'list' shows the subscriptions, 'create' adds one, 'delete' removes one. BE HONEST ABOUT WHERE IT GOES: callBackUri must be an HTTPS endpoint THE MERCHANT runs and can decode — Hermoso does not receive these notifications, so this only helps someone with a server on the other end. 0 credits.
| Parameter | Type | Description |
|---|
merchantCenterIdrequired | string | from list_merchant_accounts |
action | "list" | "create" | "delete" | defaults to 'list' |
callBackUri | string | HTTPS endpoint Google will POST to (create). Not a Hermoso URL and not localhost |
registeredEvent | "PRODUCT_STATUS_CHANGE" | "ACCOUNT_SERVICE_CHANGE" | defaults to PRODUCT_STATUS_CHANGE, the one that fires on a disapproval |
allManagedAccounts | boolean | subscribe for every managed account instead of just this one — mutually exclusive with targetAccount |
targetAccount | string | accounts/{id} to receive notifications for; defaults to this account |
subscriptionId | string | for action:'delete', from action:'list' |
manage_merchant_conversion_source#
Merchant Center conversion sources destructive
WHERE MERCHANT CENTER GETS ITS CONVERSION DATA FROM, which is what free-listing and Shopping performance reporting is built on — a merchant with no conversion source sees clicks and no outcomes, and nothing in Merchant Center says why. Two kinds, and they are different jobs: a MERCHANT CENTER DESTINATION is a Google tag target, and creating one returns a destination id (MC-…) that is the only place that id exists, so it must be read back to the user because it is what the Google tag has to send conversions to. A GOOGLE ANALYTICS LINK pulls conversions from a GA4 property instead, needs the connected Google account to be an ADMIN on that property, and Google marks it IMMUTABLE — the property cannot be changed afterwards, only deleted and re-created. action:'list' shows every source including archived ones, 'create' adds one, 'update' changes a destination's name, currency or attribution, 'delete' ARCHIVES one (confirm-gated) and 'undelete' restores it until the expiry Google reports on the archived row. Attribution lookback is 7, 30 or 40 days and nothing else. Free.
| Parameter | Type | Description |
|---|
merchantCenterIdrequired | string | from list_merchant_accounts |
action | "list" | "create" | "update" | "delete" | "undelete" | defaults to 'list' |
kind | "merchant_center_destination" | "google_analytics_link" | for action:'create' — which kind to make. Passing propertyId implies the Google Analytics one |
displayName | string | the name that identifies a tag destination in the Merchant Center UI, e.g. "Website purchases". Required to create one; may also name an existing source for update/delete |
currencyCode | string | three-letter ISO 4217 code the conversions are reported in, e.g. USD. Required when creating a tag destination — it decides what every number reported through it means |
attributionModel | string | CROSS_CHANNEL_LAST_CLICK (default) | ADS_PREFERRED_LAST_CLICK | CROSS_CHANNEL_DATA_DRIVEN | CROSS_CHANNEL_FIRST_CLICK | CROSS_CHANNEL_LINEAR | CROSS_CHANNEL_POSITION_BASED | CROSS_CHANNEL_TIME_DECAY |
attributionLookbackWindowDays | number | 7, 30 or 40 — Google publishes no other value. Defaults to 30 |
conversionTypes | string[] | names of the conversion types events may be classified as. IMMUTABLE once set, and Google returns it on no read, so it can never be confirmed afterwards — leave it off and Google creates a standard "purchase" type |
propertyId | string | NUMERIC GA4 property id for a Google Analytics link (from list_analytics_properties), not a G- measurement id |
conversionSourceId | string | for update, delete and undelete — from action:'list' |
showDeleted | boolean | include archived sources in a list |
confirm | boolean | must be true to actually archive |
list_merchant_data_sources#
List Merchant Center product feeds read-only
List the data sources (feeds) on a Merchant Center account and say which of them can actually take a product write. Do this BEFORE creating or deleting a product: writes go into a data source, and Google only accepts them into an API-input product feed — a file feed, the feed Merchant Center's own UI creates, and an autofeed all list here and all REFUSE writes, so picking the first row would pick a feed that cannot be written to. Needs merchantCenterId from list_merchant_accounts. Read-only and free.
| Parameter | Type | Description |
|---|
merchantCenterIdrequired | string | from list_merchant_accounts |
create_merchant_data_source#
Create a Merchant Center product feed
Create an API product feed on a Merchant Center account — the container upsert_merchant_product writes into. Most accounts have none until one is made: a store set up through the Merchant Center UI has a UI feed, which is read-only. feedLabel and contentLanguage must be set TOGETHER or not at all and BOTH ARE IMMUTABLE — leave them off and the feed accepts products for any market and language, which is the safer default because a wrong feed label can never be corrected. Omit destinations to inherit wherever the account already sells. Creating a feed costs nothing and cannot spend.
| Parameter | Type | Description |
|---|
merchantCenterIdrequired | string | from list_merchant_accounts |
displayNamerequired | string | the name the merchant will see in Merchant Center |
feedLabel | string | market bucket, e.g. "US" — max 20 chars, A-Z/0-9/dashes. IMMUTABLE, and must be set together with contentLanguage |
contentLanguage | string | two-letter ISO 639-1, e.g. "en". IMMUTABLE, and must be set together with feedLabel |
countries | string[] | CLDR territory codes the items may be shown in |
destinations | string[] | omit to inherit the account's own program participation (SHOPPING_ADS, FREE_LISTINGS, …) |
upsert_merchant_product#
Create or replace a Merchant Center product destructive
Create or replace a product in a Merchant Center feed — this is how a merchant's catalogue gets populated, and it is what Shopping ads, free listings and retail Performance Max actually serve. It is an UPSERT and a WHOLE-ROW write: calling it again for the same offerId REPLACES the product rather than patching it, so send every field you want kept each time. You do not have to find a feed first — omit dataSource and it resolves the account's one writable feed, creates one if there is none, and REFUSES by name if there are several rather than putting the product in a market the campaigns may not target. offerId, contentLanguage and feedLabel together ARE the product's identity and are all immutable; contentLanguage and feedLabel are taken from the feed when the feed declares them. For the product to serve at all Google needs title, description, link, imageLink, availability and price — a row missing any of them is stored and then disapproved, and the reply says so rather than letting you believe it published. The reply reports what GOOGLE STORED, not what was sent. Free: a feed edit cannot spend.
| Parameter | Type | Description |
|---|
merchantCenterIdrequired | string | from list_merchant_accounts |
offerIdrequired | string | your own unique id for the product — IMMUTABLE, and what you update or delete it by later |
dataSource | string | the feed to write into, from list_merchant_data_sources. Omit to resolve the account's one writable feed (created if there is none, refused by name if there are several) |
dataSourceName | string | display name for a feed created on the fly |
contentLanguage | string | two-letter, e.g. "en" — taken from the feed when the feed declares one |
feedLabel | string | market bucket, e.g. "US" — taken from the feed when the feed declares one |
title | string | product title; Google needs it for the product to serve |
description | string | product description; Google needs it for the product to serve |
link | string | the product page URL on the merchant's store |
imageLink | string | main product image URL (https); Google needs it for the product to serve |
additionalImageLinks | string[] | extra product image URLs, as a list |
availability | string | in_stock | out_of_stock | preorder | backorder |
condition | string | new | refurbished | used |
price | object | price object: {value in major units or amountMicros, currencyCode} |
salePrice | object | sale price, same shape as price |
brand | string | brand name |
gtin | string | GTIN barcode (UPC, EAN or similar) |
mpn | string | manufacturer part number |
identifierExists | boolean | false for a product with no GTIN/MPN/brand, such as a service or a bundle |
googleProductCategory | string | Google product taxonomy category |
productTypes | string[] | merchant-defined category labels for the product, as a list |
color | string | product colour |
size | string | product size |
material | string | product material |
ageGroup | string | newborn, infant, toddler, kids or adult |
gender | string | male, female or unisex |
itemGroupId | string | shared id that groups the variants of one product, e.g. colours or sizes |
attributes | object | any other product attribute by its Merchant API name — there are 145 and only the common ones are named above |
customAttributes | object[] | extra attributes as a list of {name, value} objects |
delete_merchant_product#
Delete a Merchant Center product destructive
Delete a product from a Merchant Center feed. CONFIRM-GATED: without confirm:true nothing is deleted and it reports the real product it WOULD delete — title, price and availability read back from Google — because the id you were given proves nothing about what is actually there. It is recoverable: re-inserting the same offerId re-creates the product. Needs the same offerId + contentLanguage + feedLabel that identify the product (contentLanguage and feedLabel come from the feed when it declares them). Free.
| Parameter | Type | Description |
|---|
merchantCenterIdrequired | string | from list_merchant_accounts |
offerIdrequired | string | your offer id of the product to delete |
dataSource | string | the feed it lives in, from list_merchant_data_sources |
contentLanguage | string | two-letter language code of the product; taken from the feed when it declares one |
feedLabel | string | market label of the product, e.g. US; taken from the feed when it declares one |
confirm | boolean | must be true to actually delete; without it nothing is deleted and the real product is reported back |
update_merchant_product#
Update fields on a Merchant Center product destructive
Change SOME fields on a product already in a Merchant Center feed — the everyday operation, because price and availability move daily. This is a PATCH: only the attributes you name are touched and everything else on the product survives, which is the difference from upsert_merchant_product (a whole-row write that wipes anything you leave out). It refuses if the product does not exist rather than quietly creating a half-populated one, and it refuses an empty change rather than sending an update mask with nothing behind it. The reply reports every field Google now holds, so you can see what survived. Free.
| Parameter | Type | Description |
|---|
merchantCenterIdrequired | string | from list_merchant_accounts |
offerIdrequired | string | the product to change |
dataSource | string | the feed it lives in, from list_merchant_data_sources |
contentLanguage | string | two-letter language code; taken from the feed when it declares one |
feedLabel | string | market label, e.g. US; taken from the feed when it declares one |
title | string | new product title; omit to leave unchanged |
description | string | new product description; omit to leave unchanged |
link | string | product page URL on the merchant's store |
imageLink | string | main product image URL (https) |
additionalImageLinks | string[] | extra image URLs, sent as the whole list |
availability | string | in_stock | out_of_stock | preorder | backorder |
condition | string | new | refurbished | used |
price | object | new price: {value in major units or amountMicros, currencyCode} |
salePrice | object | new sale price, same shape as price |
brand | string | brand name |
gtin | string | GTIN barcode (UPC, EAN or similar) |
mpn | string | manufacturer part number |
identifierExists | boolean | false for a product with no GTIN, MPN or brand, such as a service or bundle |
googleProductCategory | string | Google product taxonomy category |
productTypes | string[] | merchant-defined category labels, as a list |
color | string | product colour |
size | string | product size |
material | string | product material |
ageGroup | string | newborn, infant, toddler, kids or adult |
gender | string | male, female or unisex |
itemGroupId | string | shared id that groups the variants of one product |
attributes | object | any other product attribute by its Merchant API name |
delete_merchant_data_source#
Delete a Merchant Center product feed destructive
Delete a data source (feed) from a Merchant Center account. CONFIRM-GATED and the heavier of the two deletes: without confirm:true nothing is deleted and it reports the feed plus HOW MANY PRODUCTS would be destroyed with it, counted live from Google. This is NOT recoverable the way a product delete is — the products would have to be re-inserted into a new feed, and feedLabel/contentLanguage are immutable so a replacement may not be identical. Free.
| Parameter | Type | Description |
|---|
merchantCenterIdrequired | string | from list_merchant_accounts |
dataSourcerequired | string | the feed to delete, from list_merchant_data_sources |
confirm | boolean | must be true to actually delete; without it nothing is deleted and the product count that would go with it is reported |
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).
| Parameter | Type | Description |
|---|
customerId | string | 10-digit account id (dashes ok) — omit to list accessible accounts |
status | "ENABLED" | "PAUSED" | "REMOVED" | filter campaigns by status |
datePreset | string | metrics window preset (default LAST_30_DAYS) |
since | string | start date YYYY-MM-DD (with until) |
until | string | end date YYYY-MM-DD |
metrics | boolean | include performance metrics (default true) |
loginCustomerId | string | manager (MCC) id — only if reaching a client account through a manager |
limit | number | max campaigns (1–500, default 100) |
google_ads_report#
Google Ads GAQL report read-only
Run a GAQL (Google Ads Query Language) report for detailed performance breakdowns — ad groups, ads, keywords, search terms, demographics, geo. Pass customerId + a GAQL query (SELECT … FROM <resource> WHERE segments.date DURING LAST_30_DAYS). Allowed FROM resources: campaign, ad_group, ad_group_ad, keyword_view, campaign_budget, campaign_criterion, ad_group_criterion, conversion_action, campaign_conversion_goal, search_term_view, age_range_view, gender_view, geographic_view, asset, change_event and every other documented GAQL report resource (the refusal names the full list if one is missing). Campaign network settings (Search partners / Display) are readable here as campaign.network_settings.* and changed with set_google_ads_networks. cost_micros is micros — divide by 1,000,000 for the account currency. Read-only, free.
| Parameter | Type | Description |
|---|
customerId | string | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
queryrequired | string | GAQL, e.g. "SELECT ad_group.name, metrics.clicks, metrics.cost_micros FROM ad_group WHERE segments.date DURING LAST_7_DAYS" |
loginCustomerId | string | manager id if operating through an MCC |
GOOGLE ADS CHANGE HISTORY
google_ads_change_history#
What changed on a Google Ads account, and when read-only
WHAT CHANGED ON THE ACCOUNT, AND WHEN — the answer to "performance fell off a cliff on Tuesday, what happened?", and the Google twin of reddit_ads_history. source:"change_event" (default) is FIELD-LEVEL over the last 30 days: the change time, who made it, from which client (web UI, API, scripts, bulk upload, automated rule), whether it was a CREATE / UPDATE / REMOVE, and exactly which fields moved — with detail:true it also carries the old and new resource snapshots. source:"change_status" reaches 90 days and is the ONLY one that catches GOOGLE ADS EDITOR and criterion-level edits: Google documents change_event as NEVER returning Editor changes, so an Editor-managed account looks completely untouched there. CHECK BOTH BEFORE TELLING ANYONE NOTHING CHANGED. An unknown source is refused by name; the 30/90-day windows and Google’s own 10,000-row cap are enforced here with the reason instead of surfacing as an unreadable Google error, and a change takes up to three minutes to appear. Neither resource carries any metric or segment, so this says what changed, never what it cost. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
customerId | string | 10-digit account id — omit to use the brand’s selected default account |
source | "change_event" | "change_status" | default change_event (30 days, field-level). change_status is 90 days and is the only one that sees Google Ads Editor. |
since | string | YYYY-MM-DD, default 14 days ago |
until | string | YYYY-MM-DD, default today |
limit | number | rows, max 10000 — Google’s own ceiling, and the clamp is reported |
detail | boolean | change_event only — include the old/new resource snapshots |
loginCustomerId | string | manager (MCC) account id when acting through a manager account; omit otherwise |
create_google_ads_campaign#
Build a Google Ads campaign (paused) destructive
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.
| Parameter | Type | Description |
|---|
customerId | string | 10-digit account id (from list_google_ads_campaigns) — omit to use the brand’s selected default account |
namerequired | string | campaign name |
dailyBudgetUsd | number | daily budget USD (1–100000) — creates a budget inline; required unless budgetResourceName is given |
budgetResourceName | string | reuse an existing budget instead of creating one |
channelType | "SEARCH" | "DISPLAY" | default SEARCH |
searchPartners | boolean | SEARCH only — also serve on Google search partners (default false) |
bidding | object | how the campaign bids — default MANUAL_CPC |
locations | string[] | location NAMES to target, e.g. ["United States"] or ["Toronto","Vancouver"]. WITHOUT this the campaign runs WORLDWIDE — the most expensive default in Google Ads |
excludedLocations | string[] | location names to EXCLUDE |
languages | string[] | ISO language codes, e.g. ["en","fr"] |
startDate | string | YYYY-MM-DD |
endDate | string | YYYY-MM-DD |
adGroup | object | build the serving tree in the same atomic call — WITHOUT this you get a campaign shell that can never show an ad |
finalUrlSuffix | string | UTMs appended to every click, e.g. "utm_source=google&utm_medium=cpc". Omit for our default (utm_source/medium/campaign + {campaignid}/{creative}); "" for none |
trackingUrlTemplate | string | click-tracking redirect, e.g. "{lpurl}?src=x" — only if you use a tracker |
urlCustomParameters | object | up to 8 custom parameters, used in URLs as {_key} |
containsEuPoliticalAds | boolean | EU Political Advertising Regulation declaration. Google REQUIRES one on every campaign. Default false (a normal commercial ad) — set true ONLY for genuine EU political advertising |
dryRun | boolean | validate the WHOLE tree against Google without creating anything. Nothing is written and no budget is consumed |
loginCustomerId | string | manager id if operating through an MCC |
create_google_ads_ad_group#
Add an ad group to a Google Ads campaign destructive
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.
| Parameter | Type | Description |
|---|
customerId | string | omit to use the brand’s selected default account |
campaignIdrequired | string | the campaign this ad group belongs to |
namerequired | string | ad group name |
cpcBidUsd | number | max CPC for this ad group — omit to inherit the campaign bidding |
status | "ENABLED" | "PAUSED" | default PAUSED |
ad | object | build the ad in the same atomic call |
keywords | object[] | a SEARCH ad group with no keywords never shows |
confirm | boolean | set true ONLY after the user explicitly approved starting spend — required when the parent campaign is already LIVE (ENABLED) and this object would serve immediately |
dryRun | boolean | true = validate with Google and create nothing |
loginCustomerId | string | manager (MCC) account id when acting through a manager account; omit otherwise |
create_google_ads_ad#
Create a Google Ads ad destructive
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.
| Parameter | Type | Description |
|---|
customerId | string | omit to use the brand’s selected default account |
adGroupIdrequired | string | the ad group this ad lives in |
finalUrls | string[] | the landing page(s) — at least one is required |
headlines | string | 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). |
descriptions | string | object[] | SEARCH: 2–4 descriptions, each ≤90 characters. DISPLAY: 1–5. Pass a plain string, or {text, pin} to pin it. |
path1 | string | SEARCH only — display-URL path segment, ≤15 chars |
path2 | string | SEARCH only — second display-URL path segment, ≤15 chars |
longHeadline | string | DISPLAY only — ≤90 characters |
businessName | string | DISPLAY only — ≤25 characters |
marketingImages | string[] | DISPLAY only — landscape 1.91:1 asset resource names from upload_google_ads_asset |
squareMarketingImages | string[] | DISPLAY only — square 1:1 asset resource names |
logoImages | string[] | DISPLAY only — logo asset resource names |
status | "ENABLED" | "PAUSED" | default PAUSED |
confirm | boolean | set true ONLY after the user explicitly approved starting spend — required when the parent campaign is already LIVE (ENABLED) and this object would serve immediately |
dryRun | boolean | true = validate with Google and create nothing |
loginCustomerId | string | manager (MCC) account id when acting through a manager account; omit otherwise |
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.
| Parameter | Type | Description |
|---|
customerId | string | omit to use the brand’s selected default account |
adGroupIdrequired | string | the ad group to add them to |
keywordsrequired | object[] | the keywords to add |
confirm | boolean | set true ONLY after the user explicitly approved starting spend — required when the parent campaign is already LIVE (ENABLED) and this object would serve immediately |
dryRun | boolean | true = validate with Google and create nothing |
loginCustomerId | string | manager (MCC) account id when acting through a manager account; omit otherwise |
set_google_ads_targeting#
Add Google Ads location & language targeting destructive
ADD locations and languages to an existing Google Ads campaign. THIS ADDS; IT DOES NOT REPLACE — Google campaign criteria are a list, this call only ever creates entries, and there is no remove operation here. So a campaign already targeting the United States that you "change to Canada" ends up targeting BOTH and still spending in the US; the read-back names every pre-existing location and language it kept, and you MUST relay that rather than reporting the new total as the answer. Removing targeting is done in Google Ads (Campaign > Settings > Locations). Pass locations by NAME ("United States", "California", "Toronto") — they are resolved to Google's geo target ids for you; excludedLocations adds a NEGATIVE criterion (the reliable way to stop serving somewhere from here); 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.
| Parameter | Type | Description |
|---|
customerId | string | omit to use the brand’s selected default account |
campaignIdrequired | string | the campaign to target |
locations | string[] | location NAMES to target |
excludedLocations | string[] | location NAMES to exclude |
languages | string[] | ISO language codes, e.g. ["en","es"] |
countryCode | string | 2-letter hint to disambiguate a city name, e.g. CA for "London" |
confirm | boolean | REQUIRED true to change a LIVE (ENABLED) campaign |
dryRun | boolean | true = validate with Google and change nothing |
loginCustomerId | string | manager (MCC) account id when acting through a manager account; omit otherwise |
set_google_ads_bidding#
Set a Google Ads bidding strategy destructive
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.
| Parameter | Type | Description |
|---|
customerId | string | omit to use the brand’s selected default account |
campaignIdrequired | string | the campaign to change |
strategyrequired | "MANUAL_CPC" | "MAXIMIZE_CLICKS" | "MAXIMIZE_CONVERSIONS" | "MAXIMIZE_CONVERSION_VALUE" | "TARGET_CPA" | "TARGET_ROAS" | the bidding strategy |
targetCpaUsd | number | REQUIRED for TARGET_CPA |
targetRoas | number | REQUIRED for TARGET_ROAS — e.g. 4 = $4 revenue per $1 spent |
maxCpcUsd | number | MAXIMIZE_CLICKS — the max CPC ceiling; REQUIRED when switching an existing campaign to it |
enhancedCpc | boolean | DO NOT SET true — Google retired Enhanced CPC for new campaigns and answers OPERATION_NOT_PERMITTED_FOR_CONTEXT (measured live 2026-08-05); Hermoso refuses it up front with the reason. Use MAXIMIZE_CONVERSIONS / TARGET_CPA instead. |
confirm | boolean | REQUIRED true to change a LIVE (ENABLED) campaign |
dryRun | boolean | true = validate with Google and change nothing |
loginCustomerId | string | manager (MCC) account id when acting through a manager account; omit otherwise |
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.
| Parameter | Type | Description |
|---|
queryrequired | string | one location name, or several comma-separated (up to 25) |
countryCode | string | 2-letter hint, e.g. CA to disambiguate "London" |
loginCustomerId | string | manager (MCC) account id when acting through a manager account; omit otherwise |
set_google_ads_budget#
Set a Google Ads campaign budget destructive
Create a new daily budget, or change an existing budget’s daily amount (pass budgetResourceName). Raising the budget on a LIVE (ENABLED) campaign increases real spend immediately — you MUST show the user the new daily amount, get an explicit yes, then call with confirm:true. Creating a budget or lowering one on a paused campaign is safe.
| Parameter | Type | Description |
|---|
customerId | string | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
dailyBudgetUsdrequired | number | daily budget in USD (1–100000) |
budgetResourceName | string | existing budget to UPDATE — omit to CREATE a new budget |
name | string | name for a newly created budget |
confirm | boolean | REQUIRED true to raise the budget of a LIVE campaign |
loginCustomerId | string | manager id if operating through an MCC |
set_google_ads_status#
Enable, pause or remove a Google Ads campaign / ad group / ad destructive
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.
| Parameter | Type | Description |
|---|
customerId | string | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
level | "campaign" | "adGroup" | "ad" | what to change — default campaign |
campaignId | string | campaign id (level:"campaign") |
adGroupId | string | ad group id (level:"adGroup", or with adId for level:"ad") |
adId | string | ad id (level:"ad" — pass adGroupId too) |
campaignResourceName | string | full resource name, e.g. customers/{cid}/campaigns/{id} |
statusrequired | "ENABLED" | "PAUSED" | "REMOVED" | ENABLED = start spending; PAUSED = stop; REMOVED = permanent |
confirm | boolean | REQUIRED true to ENABLE (real spend) or to REMOVE (permanent) |
confirmName | string | when archiving or removing: its EXACT name, as the unconfirmed call names it. Required when it has children or is live |
confirmChildren | number | when archiving or removing: the exact number of children the unconfirmed call reported. Required when it has any |
loginCustomerId | string | manager id if operating through an MCC |
manage_google_ads_budget_networks#
Google Ads waterfall priorities and network spend caps destructive
Google Ads WATERFALL AND NETWORK CAPS: which network a budget spends on first and how much the others may take. action 'read' (default), 'set' (settings) or 'clear', on the ACCOUNT DEFAULT or, with budgetId, on one campaign budget, which then wins. Every channel a campaign type supports must be in a tier and caps only go on tier2; Google says which if not. A budget's read says whether it can use these and why not. 'set' and 'clear' move real spend on the next auction: show the user, get a yes, then confirm:true; dryRun:true validates without changing anything.
| Parameter | Type | Description |
|---|
action | "read" | "set" | "clear" | default 'read' |
customerId | string | 10-digit account id (dashes ok); omit to use the brand’s selected default account |
budgetId | string | numeric campaign budget id; omit for the account default |
settings | object[] | for set: one entry per campaign type |
confirm | boolean | REQUIRED true for set and clear, after the user approved |
dryRun | boolean | validate with Google, write nothing |
loginCustomerId | string | manager id if operating through an MCC |
google_ads_local_services_eligibility#
Check Google Local Services eligibility read-only
Ask Google whether a Business Profile listing can run LOCAL SERVICES ads on this Google Ads account: eligible or not, why not, and which lead types and categories it qualifies for. listingId is the numeric id at the end of a location name from list_business_locations. Read-only.
| Parameter | Type | Description |
|---|
listingIdrequired | string | numeric Business Profile location id |
customerId | string | 10-digit account id (dashes ok); omit to use the brand’s selected default account |
loginCustomerId | string | manager id if operating through an MCC |
set_google_ads_networks#
Change where a Google Ads campaign serves (Search partners / Display) destructive
Change WHERE an existing Google Ads campaign serves: Google Search, Search partners (target_search_network) and the Display Network (target_content_network), each true/false. The classic use is turning Search partners OFF on a Search campaign, or Display off. Current settings are read first and a no-op says so. On a LIVE (ENABLED) campaign this moves real spend on the next auction — show the user the before -> after, get an explicit yes, then call again with confirm:true. dryRun:true validates with Google and writes nothing. The result is READ BACK from Google before you are told it took; Google’s own rules (a Display campaign cannot take Google Search on, a Search campaign keeps Google Search on) are relayed by name.
| Parameter | Type | Description |
|---|
customerId | string | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
campaignIdrequired | string | the campaign to change |
googleSearch | boolean | serve on Google Search (target_google_search) |
searchPartners | boolean | serve on Google search partner sites (target_search_network) |
display | boolean | serve on the Google Display Network (target_content_network) |
confirm | boolean | REQUIRED true to change a LIVE (ENABLED) campaign |
dryRun | boolean | validate with Google, write nothing |
loginCustomerId | string | manager id if operating through an MCC |
set_google_ads_schedule_and_bids#
Google Ads ad schedule + device / location / hour bid adjustments destructive
WHEN a Google Ads campaign runs and how much more or less it bids by device, location and hour. Pass only campaignId to read the current schedule and adjustments. schedule:[{day: MONDAY…SUNDAY | WEEKDAYS | WEEKENDS | EVERYDAY, start:"09:00", end:"17:00", bidModifier?}] REPLACES the schedule (scheduleMode:"add" appends; clearSchedule:true runs it all day again); account time zone, quarter hours, end may be 24:00. devices {mobile, desktop, tablet, connectedTv} and adGroupDevices (with adGroupId) take MULTIPLIERS: 1.2 = +20%, 0.8 = -20%, 0 = never show on that device. locations:[{geoTargetId, bidModifier}] adjusts locations the campaign already targets. On a LIVE campaign a change that can raise spend (more hours, a higher multiplier) needs confirm:true after the user approves; lowering never does. dryRun:true validates only. Read back from Google.
| Parameter | Type | Description |
|---|
campaignIdrequired | string | the campaign |
adGroupId | string | for adGroupDevices |
schedule | object[] | ad schedule slots; replaces the current schedule unless scheduleMode is add |
scheduleMode | "replace" | "add" | replace (default) or add to the current schedule |
clearSchedule | boolean | remove the schedule so it runs all day |
devices | object | campaign-level device multipliers |
adGroupDevices | object | ad-group device multipliers (needs adGroupId) |
locations | object[] | bid adjustments for locations the campaign already targets |
customerId | string | omit to use the selected default account |
confirm | boolean | true only after the user approved a change to a LIVE campaign or a delete |
dryRun | boolean | validate with Google, change nothing |
loginCustomerId | string | manager id if operating through an MCC |
manage_google_ads_negative_list#
Google Ads shared negative keyword lists destructive
Shared NEGATIVE KEYWORD LISTS: one list of blocked searches reused across Google Ads campaigns, or applied to the whole account. action: list (with sharedSetId shows its keywords) | create (name, keywords, campaignIds or accountLevel:true) | add (keywords) | remove_keywords (keywords or criterionIds) | attach / detach (campaignIds) | delete. keywords are "text" or {text, matchType: BROAD|PHRASE|EXACT}. Creating, adding and attaching only block ads, so they never need confirm. Removing keywords, detaching or deleting lets blocked searches show ads again: confirm:true on a live campaign, and always for delete. One ad group's own negatives: add_google_ads_keywords with negative:true.
| Parameter | Type | Description |
|---|
action | "list" | "create" | "add" | "remove_keywords" | "attach" | "detach" | "delete" | default list |
sharedSetId | string | the list (from action list) |
name | string | create |
keywords | string | object[] | keywords to block, as text strings or {text, matchType BROAD, PHRASE or EXACT} objects |
criterionIds | string[] | remove_keywords by id |
campaignIds | string[] | campaign ids to attach to or detach from the shared list |
accountLevel | boolean | create: apply to every campaign on the account |
customerId | string | omit to use the selected default account |
confirm | boolean | true only after the user approved a change to a LIVE campaign or a delete |
dryRun | boolean | validate with Google, change nothing |
loginCustomerId | string | manager id if operating through an MCC |
manage_google_ads_labels#
Google Ads labels on campaigns, ad groups and ads destructive
Google Ads LABELS for organising campaigns, ad groups and ads ("Made by Hermoso", "Q4 test"). action: list | create (name, color #RRGGBB, description, and targets to label at once) | update (name, color, description) | apply / remove (campaignIds, adGroupIds, ads:[{adGroupId, adId}]) | delete (confirm:true). Labels never change delivery or spend. Report by label with google_ads_report over campaign_label / ad_group_label / ad_group_ad_label.
| Parameter | Type | Description |
|---|
action | "list" | "create" | "update" | "apply" | "remove" | "delete" | default list |
labelId | string | the label (from action list) |
name | string | label name, for create or update |
color | string | #RRGGBB |
description | string | up to 200 characters |
campaignIds | string[] | campaign ids to apply or remove the label on |
adGroupIds | string[] | ad group ids to apply or remove the label on |
ads | object[] | ads to label, as a list of {adGroupId, adId} objects |
customerId | string | omit to use the selected default account |
confirm | boolean | true only after the user approved a change to a LIVE campaign or a delete |
dryRun | boolean | validate with Google, change nothing |
loginCustomerId | string | manager id if operating through an MCC |
manage_google_ads_bid_strategy#
Google Ads portfolio bid strategies destructive
PORTFOLIO bid strategies: one shared Google Ads strategy and target driving several campaigns. action: list | create (name, strategy, its targets, optional campaignIds to attach) | update (targets or name; the type cannot change) | attach (campaignIds) | delete (only once no campaign uses it; confirm:true). Amounts are in the account currency. Changing targets of, or attaching, a LIVE campaign changes how it bids now: confirm:true after the user approves. Performance Max cannot use a portfolio. To move a campaign OFF a portfolio, call set_google_ads_bidding on it.
| Parameter | Type | Description |
|---|
action | "list" | "create" | "update" | "attach" | "delete" | default list |
biddingStrategyId | string | the strategy (from action list) |
name | string | strategy name, for create or update |
strategy | "MAXIMIZE_CLICKS" | "MAXIMIZE_CONVERSIONS" | "MAXIMIZE_CONVERSION_VALUE" | "TARGET_CPA" | "TARGET_ROAS" | "TARGET_IMPRESSION_SHARE" | create |
targetCpaUsd | number | cost per conversion |
targetRoas | number | 4 = 4x revenue per unit spent |
maxCpcUsd | number | max CPC ceiling (required for TARGET_IMPRESSION_SHARE) |
minCpcUsd | number | min CPC floor |
impressionSharePercent | number | 1-100 |
impressionShareLocation | "ANYWHERE_ON_PAGE" | "TOP_OF_PAGE" | "ABSOLUTE_TOP_OF_PAGE" | where to aim impression share: ANYWHERE_ON_PAGE, TOP_OF_PAGE or ABSOLUTE_TOP_OF_PAGE |
campaignIds | string[] | campaign ids to attach to the strategy |
allowNoConversionTracking | boolean | skip the conversion-tracking check |
customerId | string | omit to use the selected default account |
confirm | boolean | true only after the user approved a change to a LIVE campaign or a delete |
dryRun | boolean | validate with Google, change nothing |
loginCustomerId | string | manager id if operating through an MCC |
set_google_ads_conversion_goals#
Google Ads conversion goals (what smart bidding optimises for) destructive
Which conversion goals Google Ads smart bidding optimises toward, the usual cause of "it optimises for the wrong thing". With no goals it reads: the account goals, or with campaignId that campaign's goals and whether it follows the account. goals:[{category (PURCHASE, SIGNUP, SUBMIT_LEAD_FORM, PAGE_VIEW...), origin (default WEBSITE), biddable}] sets them account-wide, or for one campaign with campaignId (it then stops following account changes). useAccountGoals:true puts a campaign back on the account goals. A goal exists only where a conversion action of that category exists. Changing goals of LIVE campaigns needs confirm:true after the user approves. Read back from Google.
| Parameter | Type | Description |
|---|
campaignId | string | omit for the account goals |
goals | object[] | conversion goals to set, as a list of {category, origin, biddable}; omit to read the current goals |
useAccountGoals | boolean | put the campaign back on the account goals |
customerId | string | omit to use the selected default account |
confirm | boolean | true only after the user approved a change to a LIVE campaign or a delete |
dryRun | boolean | validate with Google, change nothing |
loginCustomerId | string | manager 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").
| Parameter | Type | Description |
|---|
customerId | string | 10-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 |
campaignId | string | campaign id (level:"campaign") |
adGroupId | string | ad group id (level:"adGroup"; REQUIRED as the parent for "ad" and "keyword") |
adId | string | ad id (level:"ad" — pass adGroupId too) |
keywordId | string | keyword criterion id (level:"keyword" — pass adGroupId too) |
conversionActionId | string | conversion action id (level:"conversionAction") |
resourceName | string | full resource name — REQUIRED for campaignAsset / adGroupAsset, accepted for any level |
confirm | boolean | REQUIRED true — REMOVED is permanent |
confirmName | string | the object’s EXACT name, required when it has children / is live / has spent |
confirmChildren | number | the exact number of children reported by the unconfirmed call, required when it has any |
loginCustomerId | string | manager 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. For a PLAYABLE (App campaigns), pass playableUrl: an HTML5 zip with <!DOCTYPE html>, an ad.orientation meta tag and no outside files except Google's own (exitapi.js, Google Fonts); add appAdId to put it on an App ad. Returns the asset resource name. Pass customerId (from list_google_ads_campaigns).
| Parameter | Type | Description |
|---|
customerId | string | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
imageUrl | string | a Hermoso-hosted image URL for an IMAGE asset (≤5MB) — a Hermoso render, or the user’s OWN creative put through upload_file first. An arbitrary external/CDN URL is refused. |
youtubeVideoId | string | a YouTube video id for a VIDEO asset (post_to_youtube first) |
playableUrl | string | PLAYABLE: link to an HTML5 zip (any public https URL or upload_file). Checked against Google's rules, then Google's own free validation, then created as a MEDIA_BUNDLE asset |
appAdId | string | also add the playable to this App ad (App campaigns for installs only) |
assetResourceName | string | with appAdId: add a playable already in the asset library instead of uploading one |
name | string | asset name |
loginCustomerId | string | manager 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.
| Parameter | Type | Description |
|---|
customerId | string | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
namerequired | string | what 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. Google Ads v25’s IN_APP_AD_REVENUE is not offered: Google refuses it on every type this tool creates, and it comes only from a linked Firebase app (FIREBASE_ANDROID/IOS_APP_AD_IMPRESSION), which list_google_ads_conversion_actions reads like any other |
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 |
defaultValueUsd | number | what one conversion is worth — required in practice for TARGET_ROAS |
defaultCurrencyCode | string | 3-letter ISO code, e.g. USD |
alwaysUseDefaultValue | boolean | ignore any value sent with the conversion and always use the default |
clickThroughLookbackDays | number | 1–90 days |
viewThroughLookbackDays | number | 1–30 days |
includeInConversionsMetric | boolean | default true — false makes smart bidding IGNORE it |
primaryForGoal | boolean | default true — whether this action is biddable for its category |
dryRun | boolean | validate against Google and create NOTHING |
loginCustomerId | string | manager 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.
| Parameter | Type | Description |
|---|
customerId | string | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
includeRemoved | boolean | also list REMOVED conversion actions |
loginCustomerId | string | manager id if operating through an MCC |
add_google_ads_assets#
Add sitelinks, callouts, call, promotion, price, lead-form and brand assets to Google Ads
Add ASSETS (extensions) to a Google Ads campaign, ad group or the whole account (level:"customer") — and ATTACH them, which is what makes them show. Pass assetType plus assets[]: SITELINK {linkText ≤25, finalUrl, description1/2}; CALLOUT {calloutText ≤25}; STRUCTURED_SNIPPET {header, values[] ≥3}; CALL {phoneNumber, countryCode}; PROMOTION {promotionTarget ≤20, percentOff OR moneyAmountOff, finalUrl, promotionCode?, occasion?, startDate?, endDate?}; PRICE {priceType, offerings[3–8] of {header ≤25, description ≤25, price, unit?, finalUrl}}; LEAD_FORM {businessName, headline, description, privacyPolicyUrl, callToActionType, callToActionDescription, finalUrl, fields?}; BUSINESS_NAME {businessName ≤25}; BUSINESS_LOGO {image: a square Hermoso image}. Or link existing ones with assetResourceNames[]. Edit, pause or detach later with manage_google_ads_assets. (Google does not let apps attach Search image extensions; images go through Performance Max, Demand Gen and display ads.) 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.
| Parameter | Type | Description |
|---|
customerId | string | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
assetTyperequired | "SITELINK" | "CALLOUT" | "STRUCTURED_SNIPPET" | "CALL" | "PROMOTION" | "PRICE" | "LEAD_FORM" | "BUSINESS_NAME" | "BUSINESS_LOGO" | what kind of asset to create and attach |
level | "campaign" | "adGroup" | "customer" | where to attach it — follows the id you pass; customer = every campaign on the account |
campaignId | string | campaign id (level:"campaign") |
adGroupId | string | ad group id (level:"adGroup") |
assets | object[] | the assets to CREATE and attach |
assetResourceNames | string[] | attach assets that ALREADY exist instead of creating new ones |
status | "ENABLED" | "PAUSED" | the LINK status — default ENABLED |
confirm | boolean | set true ONLY after the user approved changing what a LIVE campaign shows |
dryRun | boolean | validate against Google and create NOTHING |
loginCustomerId | string | manager id if operating through an MCC |
google_ads_recommendations#
Google Ads recommendations + optimization score read-only
Google Ads’ own advice for the account: the optimization score (account and per campaign), every open recommendation with what applying it would change (budget $X -> $Y, a keyword, a bid target, a Google-written ad) and Google’s weekly impact estimate, each tagged spend (changes what the account buys) or serves (changes what an ad shows), plus which types Google is auto-applying by itself. An empty list means Google has no advice yet, never that the account is optimal. Ids go stale fast, so act on a fresh read. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
customerId | string | 10-digit account id, omit for the brand’s default account |
campaignId | string | only recommendations for this campaign |
types | string[] | recommendation types, e.g. CAMPAIGN_BUDGET, KEYWORD; an unknown one is refused by name |
includeDismissed | boolean | also show dismissed ones |
loginCustomerId | string | manager id if operating through an MCC |
manage_google_ads_recommendations#
Apply / dismiss Google Ads recommendations, or set auto-apply destructive
Act on Google Ads recommendations by id. action:"apply" makes the change live immediately with no undo. Any spend-type recommendation (budget, bidding, keywords, network or audience expansion, new campaigns) or one touching a LIVE campaign is refused until the user says yes: call once without confirm, show the returned list, then call again with confirm:true and the confirmCount it named. The count is re-checked against a fresh read, and if any id is no longer on offer NOTHING is applied. action:"dismiss" only takes advice off the list and needs no confirm. action:"auto_apply" with autoApply:{TYPE:true|false} lets Google apply that type by itself; turning one ON needs confirm:true, OFF never does. dryRun validates and changes nothing. 0 credits.
| Parameter | Type | Description |
|---|
customerId | string | 10-digit account id, omit for the brand’s default account |
actionrequired | "apply" | "dismiss" | "auto_apply" | apply (live now; spend types need confirm), dismiss (remove from list), or auto_apply (set auto-apply per type) |
recommendationIds | string[] | apply/dismiss: ids from google_ads_recommendations, max 100; there is no "apply everything" |
autoApply | object | auto_apply: {RECOMMENDATION_TYPE: true|false} |
confirm | boolean | the user saw the list and said yes |
confirmCount | number | apply: echo the count the unconfirmed call reported |
dryRun | boolean | validate and change nothing |
loginCustomerId | string | manager id if operating through an MCC |
preview_google_ads_ad#
Shareable Google Ads preview link read-only
A shareable Google preview link (viewable with no Google Ads login, good for client approval) for a Performance Max asset group, or for a YouTube video ad. Google cannot preview Search or Display ads (RSA/RDA), and that is refused by name for free. Links expire and cannot be embedded in an iframe. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
customerId | string | 10-digit account id, omit for the brand’s default account |
assetGroupIds | string[] | Performance Max asset group ids |
adId | string | a YouTube video ad id |
adGroupId | string | its ad group id |
loginCustomerId | string | manager id if operating through an MCC |
google_ads_policy_issues#
Why Google rejected or limited an ad read-only
WHY Google rejected or limited something: every DISAPPROVED or APPROVED_LIMITED ad, asset (headlines, images, sitelinks, Performance Max assets) and keyword on the account or one campaign, with Google’s policy topic, its effect, the evidence (offending text, a broken landing page and its HTTP code, restricted countries) and appeal state. includeApproved lists everything. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
customerId | string | 10-digit account id, omit for the brand’s default account |
campaignId | string | only this campaign |
includeApproved | boolean | list approved and pending items too |
loginCustomerId | string | manager id if operating through an MCC |
manage_google_ads_assets#
List, edit, pause or detach Google Ads assets destructive
The assets on a Google Ads campaign, ad group or the whole account (level:"customer"): action "list" (every attached asset with its review status and any policy problem), "update" (edit a sitelink/callout/snippet/call/promotion/price/lead-form asset — pass assetId, assetType and asset{} in add_google_ads_assets' shape), "pause", "enable" or "unlink" (pass assetId + fieldType from list). Google never deletes an asset; unlink stops it showing. Editing or re-enabling on a LIVE campaign needs confirm:true after the user agrees.
| Parameter | Type | Description |
|---|
customerId | string | 10-digit account id; omit to use the brand's selected default account |
action | "list" | "update" | "pause" | "enable" | "unlink" | default list |
level | "campaign" | "adGroup" | "customer" | campaign, adGroup or customer; follows the id you pass |
campaignId | string | campaign id, when level is campaign |
adGroupId | string | ad group id, when level is adGroup |
assetId | string | asset id, from the list action |
fieldType | string | the link field type from list, e.g. CALL, SITELINK |
assetType | string | update — the asset kind |
asset | object | update — the new content |
confirm | boolean | true only after the user agreed to change a LIVE campaign's assets |
dryRun | boolean | true = validate with Google and change nothing |
loginCustomerId | string | manager (MCC) account id when acting through a manager account; omit otherwise |
set_google_ads_tracking#
Set UTMs / tracking on a Google Ads campaign, ad group or ad destructive
Set the FINAL URL SUFFIX (the UTMs added to every click), tracking template and custom parameters on a Google Ads campaign, ad group or ad; a lower level overrides a higher one. "" clears a field; utmDefault:true applies our standard UTMs (utm_source=google, utm_medium, utm_campaign=<name>, utm_id={campaignid}, utm_content={creative}). New campaigns already get those unless you say otherwise. Read back from Google.
| Parameter | Type | Description |
|---|
customerId | string | 10-digit account id; omit to use the brand's selected default account |
campaignId | string | campaign to set tracking on; use with level campaign |
adGroupId | string | ad group to set tracking on; use with level adGroup |
adId | string | ad to set tracking on; use with level ad |
level | "campaign" | "adGroup" | "ad" | follows the id you pass |
finalUrlSuffix | string | UTM string appended to every click, e.g. utm_source=google&utm_medium=cpc; empty string clears it |
trackingUrlTemplate | string | click-tracking redirect template, e.g. {lpurl}?src=x; empty string clears it |
urlCustomParameters | object | up to 8 custom URL parameters as a key/value object, used in URLs as {_key} |
utmDefault | boolean | true = apply the standard UTMs (utm_source, utm_medium, utm_campaign, utm_id, utm_content) |
confirm | boolean | needed to change the tracking redirect on a LIVE campaign |
dryRun | boolean | true = validate with Google and change nothing |
loginCustomerId | string | manager (MCC) account id when acting through a manager account; omit otherwise |
manage_google_ads_audience#
Find or build Google Ads audiences destructive
Audiences for Google Ads. action "search" finds affinity + in-market interests, remarketing lists, custom segments and saved audiences (query by name). "create_remarketing_list": people who visited pages matching urlRules [{operator CONTAINS|EQUALS|STARTS_WITH|ENDS_WITH, value}] in the last membershipDays (needs the Google tag on the site). "create_custom_segment": people searching keywords[] or into sites like urls[] (type AUTO or SEARCH). "create_audience": a reusable audience (userLists, interests, customSegments ids + ages like "25-34", genders) — what Performance Max signals and Demand Gen targeting take. "remove" (userList or customSegment, confirm:true). Attach with set_google_ads_audiences.
| Parameter | Type | Description |
|---|
customerId | string | Google Ads account id, 10 digits (dashes ok); omit for the brand's selected default account |
action | "search" | "create_remarketing_list" | "create_custom_segment" | "create_audience" | "remove" | default search |
query | string | search text matched against audience and interest names (action search) |
name | string | audience or list name, unique on the account (create actions) |
description | string | optional description (create actions) |
urlRules | object[] | visitor rules for create_remarketing_list: [{operator, value}]; visitors to pages matching any rule |
excludeUrlRules | object[] | same shape as urlRules; visitors to matching pages are left out of the list |
membershipDays | number | 1–540, default 30 |
type | "AUTO" | "SEARCH" | custom segment type: AUTO (Google decides, default) or SEARCH (people who searched the keywords) |
keywords | string[] | search phrases for a custom segment, 10 words or fewer each (create_custom_segment) |
urls | string[] | full http(s) URLs of sites like yours for a custom segment (create_custom_segment) |
apps | string[] | app identifiers for a custom segment (create_custom_segment) |
userLists | string[] | user list ids to include in an audience (create_audience), from action search |
interests | string[] | interest ids (affinity or in-market) to include in an audience, from action search |
customSegments | string[] | custom segment ids to include in an audience, from action search |
ages | string[] | age ranges to include: 18-24, 25-34, 35-44, 45-54, 55-64, 65+ or unknown |
genders | "male" | "female" | "unknown"[] | genders to include: male, female or unknown |
kind | "userList" | "customSegment" | remove |
id | string | remove |
confirm | boolean | true to actually remove the list or segment (action remove only) |
dryRun | boolean | true = validate with Google and create or remove nothing |
loginCustomerId | string | manager (MCC) account id, when the account is managed through one |
set_google_ads_audiences#
Target or exclude audiences on Google Ads destructive
Who a Google Ads campaign reaches. adGroupId (Search/Display): userLists, interests (affinity/in-market ids), customSegments (Display only), mode "observation" (report, do not narrow — default on Search) or "targeting" (narrow to them); Demand Gen ad groups take audiences[] ids. assetGroupId (Performance Max): audiences[] as signals and searchThemes[]. campaignId: exclusions only. exclude{userLists, interests, customSegments}, excludeAges ["18-24"…"65+","unknown"], excludeGenders. remove[] takes criterion resource names from a call with no changes (which lists what is set). Narrowing a LIVE ad group needs confirm:true.
| Parameter | Type | Description |
|---|
customerId | string | Google Ads account id, 10 digits (dashes ok); omit for the brand's default account |
adGroupId | string | ad group to target (Search, Display or Demand Gen); one of adGroupId, assetGroupId, campaignId |
assetGroupId | string | Performance Max asset group id; audiences become signals there |
campaignId | string | campaign id; exclusions only |
userLists | string[] | user list ids to target on the ad group |
interests | string[] | interest ids (affinity or in-market) to target on the ad group |
customSegments | string[] | custom segment ids to target; Display ad groups only, Search refuses them |
audiences | string[] | audience ids from manage_google_ads_audience (create_audience) for Demand Gen or Performance Max |
searchThemes | string[] | search theme phrases for a Performance Max asset group, up to 50, 10 words or fewer each |
mode | "observation" | "targeting" | observation (report only, default on Search) or targeting (narrow delivery to these audiences) |
exclude | object | audiences to exclude from the ad group or campaign: {userLists, interests, customSegments} ids |
excludeAges | string[] | age ranges to exclude: 18-24, 25-34, 35-44, 45-54, 55-64, 65+ or unknown |
excludeGenders | string[] | genders to exclude: male, female or unknown |
remove | string[] | criterion resource names to remove; a call with no other changes lists the current ones |
confirm | boolean | true to approve a change that alters who a live ad group reaches |
dryRun | boolean | true = validate with Google and change nothing |
loginCustomerId | string | manager (MCC) account id, when the account is managed through one |
upload_google_ads_video#
Upload a video straight into Google Ads destructive
Upload a Hermoso video (render id, Library id, attachment://N or Hermoso link) straight into Google Ads — no YouTube channel needed: it goes UNLISTED to the Google-managed channel of the ad account (or to your own brand channel with channelId, where PUBLIC is allowed). Waits for YouTube to process it, then returns the video asset ready for create_google_ads_demand_gen_campaign or Performance Max. AI-generated renders are disclosed as such. action "status" finishes a slow one (uploadId), "list" shows uploads, "remove" deletes one (confirm:true). Free.
| Parameter | Type | Description |
|---|
customerId | string | Google Ads account id, 10 digits (dashes ok); omit for the brand's default account |
action | "upload" | "status" | "list" | "remove" | default upload |
video | string | Hermoso render id, Library id, attachment://N or Hermoso video link |
title | string | required, ≤100 chars |
description | string | YouTube description for the uploaded video (optional) |
channelId | string | your own YouTube channel (UC…), else Google's |
privacy | "UNLISTED" | "PUBLIC" | UNLISTED (default, the only option on Google's channel) or PUBLIC (own channel only, with channelId) |
aiGenerated | boolean | Google Ads generative-content disclosure. OMIT IT and Hermoso decides from provenance: a Hermoso render is declared, a video that came through upload_file (the user’s own footage) is NOT. true/false overrides |
uploadId | string | upload id from the upload reply or action list; required for status and remove |
waitSeconds | number | seconds to wait for YouTube to finish processing, max 240; default 90 on upload, 0 on status |
confirm | boolean | true to remove the upload (action remove); every ad using the video stops showing it |
dryRun | boolean | true = build and return the request without uploading any bytes |
loginCustomerId | string | manager (MCC) account id, when the account is managed through one |
create_google_ads_demand_gen_campaign#
Create a Google Demand Gen video campaign (YouTube Shorts, in-feed, in-stream) destructive
Build a Google DEMAND GEN campaign — video ads on YouTube Shorts, in-feed and in-stream (plus Discover, Gmail, Display, Maps if you pick them) — as ONE atomic, PAUSED build: budget, campaign, ad group with channel controls, locations/languages, optional audiences, and a video ad. ad needs videoAssets (from upload_google_ads_video) or youtubeVideoIds, a square logo (logoImage or logoAssets), finalUrl, businessName ≤25, 1–5 headlines, longHeadlines, descriptions. Google sets a minimum daily budget (it names it if you go under). Read back; turn it on with set_google_ads_status(confirm:true) only after the user approves.
| Parameter | Type | Description |
|---|
customerId | string | Google Ads account id, 10 digits (dashes ok); omit for the brand's default account |
namerequired | string | campaign name |
dailyBudgetUsd | number | daily budget in USD, 1 to 100000; required unless budgetResourceName is given |
budgetResourceName | string | existing campaign budget resource name to reuse instead of creating a new budget |
bidding | object | bidding object {strategy, targetCpaUsd, targetRoas, maxCpcUsd}; strategy defaults to MAXIMIZE_CONVERSIONS |
channels | "youtubeShorts" | "youtubeInFeed" | "youtubeInStream" | "discover" | "gmail" | "display" | "maps"[] | omit for all of Google's channels |
locations | string[] | place names to target, resolved by Google, e.g. United States |
excludedLocations | string[] | place names to exclude, resolved by Google |
languages | string[] | ISO language codes to target, e.g. ["en"] |
countryCode | string | 2-letter country code that disambiguates a place name, e.g. CA for Toronto |
audiences | string[] | audience ids from manage_google_ads_audience |
adrequired | object | video ad: {videoAssets or youtubeVideoIds, logo, finalUrl, businessName, headlines, longHeadlines, descriptions} |
finalUrlSuffix | string | UTM suffix appended to clicks; omit for Hermoso's default, empty string for none |
trackingUrlTemplate | string | click-tracking redirect template, e.g. {lpurl}?src=x; only if you use a tracker |
urlCustomParameters | object | up to 8 custom URL parameters, used in URLs as {_key} |
startDate | string | campaign start date, YYYY-MM-DD |
endDate | string | campaign end date, YYYY-MM-DD |
containsEuPoliticalAds | boolean | true only for genuine EU political advertising |
dryRun | boolean | true = validate with Google and create nothing |
loginCustomerId | string | manager (MCC) account id, when the account is managed through one |
create_google_ads_performance_max_campaign#
Create a Google Ads Performance Max campaign destructive
Build a PERFORMANCE MAX campaign — Google’s cross-surface campaign type (Search, YouTube, Display, Discover, Gmail, Maps). ALWAYS created PAUSED; it spends NOTHING until you enable it with set_google_ads_status(confirm:true). PMax has NO manual bidding and NO keywords: it bids only on conversions, so the account MUST already have a conversion action — check with list_google_ads_conversion_actions, 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: pass merchantCenterId (from list_merchant_accounts) to make it a Shopping-feed Performance Max — it then advertises the WHOLE feed (one root listing group); feedLabel narrows it to a single feed. Partitioning the feed by brand/category/custom label is NOT built and is refused by name, so a caller can never believe they narrowed it when they did not.
| Parameter | Type | Description |
|---|
customerId | string | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
namerequired | string | campaign name |
dailyBudgetUsd | number | daily budget in USD (1–100000) — or pass budgetResourceName |
budgetResourceName | string | an existing budget to reuse |
bidding | object | bidding {strategy, targetCpaUsd, targetRoas}; no manual bidding, strategy defaults to MAXIMIZE_CONVERSIONS |
locations | string[] | place NAMES ("United States", "Toronto") — resolved for you |
excludedLocations | string[] | places to block |
languages | string[] | ISO codes, e.g. ["en"] |
countryCode | string | 2-letter hint to disambiguate a city name |
startDate | string | YYYY-MM-DD |
endDate | string | YYYY-MM-DD |
finalUrlSuffix | string | UTMs appended to every click, e.g. "utm_source=google&utm_medium=cpc". Omit for our default (utm_source/medium/campaign + {campaignid}/{creative}); "" for none |
trackingUrlTemplate | string | click-tracking redirect, e.g. "{lpurl}?src=x" — only if you use a tracker |
urlCustomParameters | object | up to 8 custom parameters, used in URLs as {_key} |
containsEuPoliticalAds | boolean | true ONLY for genuine EU political advertising |
merchantCenterId | string | makes this a RETAIL (Shopping-feed) Performance Max — the Merchant Center account whose products it advertises, from list_merchant_accounts. It advertises the WHOLE feed; partitioning by brand/category is not built and is refused by name |
feedLabel | string | optional, retail only: narrow to ONE Merchant Center feed by its feed label. Omit to use products from all feeds |
brandGuidelinesEnabled | boolean | default TRUE, matching Google’s own default since v21: businessName + logos are linked to the CAMPAIGN. Pass false only for the pre-v21 layout, where they sit on the asset group instead |
assetGrouprequired | object | the creative — Google requires every field above before a PMax campaign can serve |
dryRun | boolean | validate the whole tree against Google and create NOTHING |
loginCustomerId | string | manager id if operating through an MCC |
google_ads_keyword_ideas#
Keyword ideas with real Google search volume read-only
Google’s own KEYWORD PLANNER: real keyword ideas with average monthly search volume, competition level and top-of-page bid estimates, so keyword choices are measured instead of guessed. Seed it with keywords[] (terms you already have), url (one landing page to mine) or site (a whole domain — the fastest way to size a competitor). Narrow by locations (place NAMES, resolved for you) and language. Results come back sorted by monthly volume. Use this BEFORE add_google_ads_keywords or create_google_ads_campaign so the ad group targets terms people actually search, and quote the volumes when you propose them. Read-only, free, spends nothing and creates nothing.
| Parameter | Type | Description |
|---|
customerId | string | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
keywords | string[] | up to 20 seed terms |
url | string | one page to mine for ideas |
site | string | a whole domain to mine, e.g. example.com |
locations | string[] | place NAMES, e.g. ["United States"] |
language | string | ISO code, e.g. "en" |
countryCode | string | 2-letter hint to disambiguate a city name |
network | "GOOGLE_SEARCH" | "GOOGLE_SEARCH_AND_PARTNERS" | default GOOGLE_SEARCH |
limit | number | how many ideas to return (1–200, default 50) |
loginCustomerId | string | manager id if operating through an MCC |
GOOGLE ANALYTICS
list_analytics_properties#
List the GA4 properties shared with this profile read-only
The GA4 properties SHARED WITH THIS PROFILE, with their numeric property ids, display names and the Analytics account each sits under. CALL THIS FIRST — every other Analytics tool needs a PROPERTY ID, which is NUMERIC (e.g. 123456789) and is NOT the "G-XXXXXXX" Measurement ID people usually know from their tracking snippet; the API accepts the numeric id and nothing else. Resolve the property yourself from this list instead of asking the user to go and find one, and only ask when two names are genuinely ambiguous. THIS IS NOT EVERY PROPERTY THE GOOGLE ACCOUNT CAN SEE, deliberately: Analytics access is handed out freely, so one login often has Viewer on many different clients' properties, and the user ticks which ones belong to THIS profile. Only ticked properties can be reported on, and any other one is refused by name. An empty list means nothing is ticked yet — say so and point the user at Settings > Connectors > Google Analytics > Manage accounts (or call list_connector_accounts / set_connector_accounts with provider "google_analytics"); never name or guess a property. Read-only, 0 credits. Needs Google Analytics connected (Settings > Connectors > Google Analytics).
No parameters.
analytics_report#
Run a GA4 report read-only
WHAT HAPPENED ON THE SITE — the GA4 Data API report, and the tool that answers "where is our traffic coming from", "which campaign converted", "which landing page is working". Pass metrics by name (activeUsers, sessions, screenPageViews, conversions, totalRevenue, engagementRate, bounceRate, averageSessionDuration …) and dimensions to break them down by (sessionDefaultChannelGroup, sessionSource / sessionMedium / sessionCampaignName, landingPage, pagePath, country, deviceCategory, date, eventName …). GA4 publishes hundreds of both and each property ships its OWN custom dimensions, so names are forwarded as given rather than validated against a copied list — use list_analytics_definitions to see a property's custom ones. THE PROPERTY IS A NUMERIC ID (e.g. 123456789), NOT the G-XXXXXXX Measurement ID — run list_analytics_properties to resolve it. Dates default to the last 28 days and accept either YYYY-MM-DD or GA4's relative forms ("28daysAgo", "yesterday", "today"). PERIOD OVER PERIOD IN ONE CALL: compare:"previous_period" (or "previous_year") adds the earlier period and returns per-period totals plus `change` (absolute + %) computed from GA4's own totals — quote it, never redo the maths; or pass up to 4 named dateRanges. comparisons put segments side by side; totals:true adds a total row. Rows come back as flat named objects, and a sampled result says so. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
propertyrequired | string | the NUMERIC GA4 property id from list_analytics_properties (e.g. "123456789") — never the G-XXXXXXX Measurement ID, and it must be one SHARED with this profile |
metrics | string[] | GA4 metric names — default ["activeUsers","sessions","screenPageViews"] |
dimensions | string[] | GA4 dimension names to break the metrics down by — omit for a single total row |
startDate | string | YYYY-MM-DD or a GA4 relative date like "28daysAgo" (default 28daysAgo) |
endDate | string | YYYY-MM-DD or "today" (default today) |
limit | number | rows to return, 1–250000 (default 50). 250,000 is GOOGLE's per-request maximum, not ours — asking for more is silently capped there. |
offset | number | skip this many rows — how you page past `limit`. Use the nextOffset the previous call returns. |
orderByMetric | string | sort by this metric — must be one of the metrics requested |
orderDesc | boolean | default true (largest first) when orderByMetric is set |
dimensionFilter | object | NARROW THE REPORT — a GA4 FilterExpression, exactly one of andGroup | orGroup | notExpression | filter. Without it a report is the WHOLE property. Example: {"filter":{"fieldName":"sessionSource","stringFilter":{"matchType":"EXACT","value":"google"}}}; combine with {"andGroup":{"expressions":[…]}}. METRICS CANNOT BE USED HERE — use metricFilter. |
metricFilter | object | Filter the AGGREGATED rows, GA4's having-clause — same FilterExpression shape. Example: {"filter":{"fieldName":"sessions","numericFilter":{"operation":"GREATER_THAN","value":{"int64Value":"30"}}}}. DIMENSIONS CANNOT BE USED HERE — use dimensionFilter. |
compare | "previous_period" | "previous_year" | also read the period before startDate–endDate (or the same dates a year earlier) and return `change` |
dateRanges | object[] | up to 4 periods instead of startDate/endDate; rows gain a dateRange column; with two, change = the FIRST relative to the SECOND |
totals | boolean | add TOTAL rows |
aggregations | "TOTAL" | "MINIMUM" | "MAXIMUM" | "COUNT"[] | extra summary rows over the result: TOTAL, MINIMUM, MAXIMUM or COUNT |
comparisons | object[] | segments side by side: [{name, dimensionFilter}] or [{comparison:"comparisons/123"}]; adds a comparison column |
orderBy | object[] | [{dimension:"date"}] or [{metric:"sessions", desc:true}] |
keepEmptyRows | boolean | true = keep rows whose metrics are all zero |
currencyCode | string | ISO 4217, e.g. EUR — default the property currency |
analytics_realtime#
Who is on the site right now (GA4 realtime) read-only
WHO IS ON THE SITE RIGHT NOW — GA4's realtime report, covering roughly the last 30 minutes. Use it to see a launch, a post or a campaign landing in real time. IT IS A DIFFERENT REPORT WITH A DIFFERENT METRIC SET, NOT a shortcut for analytics_report: realtime accepts activeUsers, screenPageViews, conversions and eventCount, and REJECTS `sessions` outright — asking for sessions here returns an error, not a zero. Realtime dimensions are also narrower (unifiedScreenName, country, deviceCategory, platform, eventName …) and there is no date range at all. The property is the NUMERIC id from list_analytics_properties, never the G-XXXXXXX Measurement ID. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
propertyrequired | string | the NUMERIC GA4 property id from list_analytics_properties — it must be one SHARED with this profile, and any other is refused by name |
metrics | string[] | realtime metric names — default ["activeUsers"]. NOT the same set as analytics_report; `sessions` is not a realtime metric |
dimensions | string[] | realtime dimension names, e.g. ["country"] or ["unifiedScreenName"] |
limit | number | rows, 1–1000 (default 50) |
list_analytics_definitions#
List a GA4 property’s key events and custom dimensions read-only
What a GA4 property already MEASURES — its key events (what GA4 counts as a conversion) and its custom dimensions, with each dimension's parameter name and scope. Two reasons to call it: to learn a property's own custom dimension names before using them in analytics_report, and to CHECK BEFORE CREATING — a custom dimension can never be deleted, only archived, and a property is capped at 50 event-scoped ones, so creating a duplicate permanently burns a slot. The property is the NUMERIC id from list_analytics_properties. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
propertyrequired | string | the NUMERIC GA4 property id from list_analytics_properties — it must be one SHARED with this profile, and any other is refused by name |
create_analytics_key_event#
Mark a GA4 event as a key event (conversion)
Mark an event GA4 ALREADY COLLECTS as a KEY EVENT — what GA4 calls a conversion since the 2024 rename (the resource is keyEvents; the old conversionEvents spelling is deprecated). This is what makes an event countable as a result in reports and importable into Google Ads as a conversion. THE EVENT NAME MUST MATCH AN EVENT THE SITE ALREADY SENDS ("purchase", "generate_lead", "sign_up", or a custom one) — marking an event that is never fired creates a key event that can never fire, so check list_analytics_definitions or an analytics_report broken down by eventName first. countingMethod ONCE_PER_EVENT counts EVERY occurrence; ONCE_PER_SESSION counts the event at most ONCE PER SESSION, however many times it fires in that session. Those are the only two GA4 accepts — there is no per-USER counting method, and asking for one is refused rather than quietly counted per event. The result is READ BACK from Google, never echoed from the request. The property is the NUMERIC id from list_analytics_properties. Needs the Google Analytics connection to have edit access on the property.
| Parameter | Type | Description |
|---|
propertyrequired | string | the NUMERIC GA4 property id from list_analytics_properties — it must be one SHARED with this profile, and any other is refused by name |
eventNamerequired | string | an event GA4 already collects, e.g. "purchase" / "generate_lead" / "sign_up" |
countingMethod | "ONCE_PER_EVENT" | "ONCE_PER_SESSION" | default ONCE_PER_EVENT; ONCE_PER_SESSION counts at most once per session (GA4 publishes no per-user method) |
create_analytics_custom_dimension#
Create a GA4 custom dimension destructive
Register an event parameter the site ALREADY SENDS as a custom dimension, so reports can break down by it (plan tier, content category, logged-in state…). TWO THINGS TO TELL THE USER BEFORE CALLING: a GA4 custom dimension CANNOT BE DELETED — only archived — and a property is capped at 50 EVENT-scoped dimensions, so a duplicate or a typo permanently burns one of them. Call list_analytics_definitions first to see what already exists. GET THE TWO NAMES THE RIGHT WAY ROUND: parameterName is the event parameter GA4 is already collecting (e.g. "customer_tier") and displayName is only the label shown in reports — swapping them silently produces a dimension that always reads "(not set)". Registering a dimension is NOT retroactive: it only collects from the moment it is created. The property is the NUMERIC id from list_analytics_properties. Needs the Google Analytics connection to have edit access on the property.
| Parameter | Type | Description |
|---|
propertyrequired | string | the NUMERIC GA4 property id from list_analytics_properties — it must be one SHARED with this profile, and any other is refused by name |
parameterNamerequired | string | the event parameter GA4 already collects, e.g. "customer_tier" — NOT the report label |
displayName | string | the label shown in GA4 reports — defaults to parameterName |
scope | "EVENT" | "USER" | "ITEM" | default EVENT. The 50-dimension cap applies to EVENT scope |
description | string | optional description, ≤150 characters |
list_analytics_metadata#
What a GA4 property can be asked read-only
THE VOCABULARY OF ONE PROPERTY — every dimension and metric analytics_report will accept on it, INCLUDING that property's own custom dimensions, each with its api name and its human label. Use this instead of guessing an api name: GA4 publishes hundreds and they are not memorable (sessions broken down by landing page is `landingPage`, revenue is `totalRevenue`, the channel grouping is `sessionDefaultChannelGroup`), and a wrong name is an error mid-conversation rather than a suggestion. analytics_report deliberately forwards names AS GIVEN — it never validates against a copied list, because that list would go stale and start refusing names Google accepts — so THIS is where a name is checked. PASS `search` almost always: unfiltered this returns several hundred rows, and a search matches both the api name and the label ("revenue", "campaign", "device"). Read-only, 0 credits.
| Parameter | Type | Description |
|---|
propertyrequired | string | the NUMERIC GA4 property id from list_analytics_properties — it must be one SHARED with this profile, and any other is refused by name |
search | string | substring filter over api name AND human label, e.g. "revenue", "campaign", "landing" — strongly recommended, since the unfiltered vocabulary is several hundred entries |
archive_analytics_custom_dimension#
Archive a GA4 custom dimension destructive
THE ONLY WAY TO RETIRE A CUSTOM DIMENSION, and it is ONE-WAY. GA4 publishes no delete and no un-archive for custom dimensions anywhere in its API — archiving is permanent through every programmatic surface — so this is how a typo'd or duplicate dimension is cleared, and it also frees the slot it was holding against the 50-event-scoped cap. Reports lose the ability to break down by it. CALLED WITHOUT `confirm` IT ARCHIVES NOTHING and instead reports what the dimension actually is, read back from Google — check that against what the user asked for before confirming, because naming the right dimension is the only thing `confirm` cannot prove. Identify it by its parameterName (the event parameter), which list_analytics_definitions lists. Needs edit access on the property.
| Parameter | Type | Description |
|---|
propertyrequired | string | the NUMERIC GA4 property id from list_analytics_properties |
parameterNamerequired | string | the event parameter of the dimension to archive, e.g. "customer_tier" — from list_analytics_definitions, NOT the report label |
confirm | boolean | must be true to actually archive. Without it nothing changes and the dimension is described back to you |
delete_analytics_key_event#
Stop counting a GA4 event as a conversion destructive
REMOVE A KEY EVENT — the reverse of create_analytics_key_event, and note the ASYMMETRY with custom dimensions: a key event really can be DELETED, where a custom dimension can only be archived. Nothing is destroyed — GA4 keeps collecting the underlying event and keeps all of its history, this only stops it counting as a conversion, and it can be marked again at any time. Use it for an event marked as a conversion by mistake, or one that should no longer be optimised toward. BE AWARE IT REACHES FURTHER THAN GA4: anything importing this conversion — Google Ads smart bidding in particular — stops receiving it, which changes how campaigns bid. Called without `confirm` it deletes nothing and describes the key event back to you. Needs edit access on the property.
| Parameter | Type | Description |
|---|
propertyrequired | string | the NUMERIC GA4 property id from list_analytics_properties |
eventNamerequired | string | the key event to stop counting, e.g. "sign_up" — from list_analytics_definitions |
confirm | boolean | must be true to actually delete. Without it nothing changes and the key event is described back to you |
create_analytics_custom_metric#
Register a GA4 custom metric
Register an event parameter the site ALREADY SENDS as a GA4 custom METRIC, so reports can SUM or AVERAGE it. This is the other half of the pair: a custom DIMENSION makes a value something you can group BY, a custom METRIC makes it something you can TOTAL — 'order_value' as a dimension gives you a list of prices, as a metric it gives you revenue. TELL THE USER TWO THINGS BEFORE CALLING: a GA4 custom metric CANNOT be deleted, only archived, and a property is capped at 50 (125 on Analytics 360) — a duplicate or a typo permanently burns a slot, so run list_analytics_definitions first, which lists metrics beside dimensions. measurementUnit IS REQUIRED and is never guessed for you: STANDARD for a plain count or quantity, CURRENCY for money, SECONDS/MINUTES/HOURS for durations. CURRENCY additionally REQUIRES restrictedMetricType (REVENUE_DATA for money coming in, COST_DATA for money going out) and every other unit REFUSES it — both directions are refused before the call is made, because Google's own error names neither field. METRICS ARE EVENT-SCOPED ONLY, unlike dimensions, which take EVENT | USER | ITEM. Not retroactive: it collects from creation onward. The result is READ BACK from Google, never echoed from the request. Needs edit access on the property. 0 credits.
| Parameter | Type | Description |
|---|
propertyrequired | string | NUMERIC GA4 property id from list_analytics_properties — never the G-XXXXXXX Measurement ID |
parameterNamerequired | string | the event parameter GA4 already collects, e.g. 'order_value' — NOT the report label. Letters, digits and underscores, starting with a letter, max 40 characters |
displayName | string | the label shown in reports; defaults to parameterName, max 82 characters |
measurementUnitrequired | "STANDARD" | "CURRENCY" | "FEET" | "METERS" | "KILOMETERS" | "MILES" | "MILLISECONDS" | "SECONDS" | "MINUTES" | "HOURS" | REQUIRED — Google marks it Required and there is no safe default |
restrictedMetricType | "COST_DATA" | "REVENUE_DATA"[] | REQUIRED when measurementUnit is CURRENCY, and refused for every other unit |
scope | "EVENT" | EVENT is the only scope GA4 accepts for a metric, and it is immutable |
description | string | ≤150 characters |
archive_analytics_custom_metric#
Archive a GA4 custom metric (one-way) destructive
THE ONLY WAY TO RETIRE A GA4 CUSTOM METRIC, and it is ONE-WAY: GA4 publishes no delete and no un-archive for custom metrics anywhere in its API, exactly as for dimensions, so archiving is permanent through every programmatic surface. It frees the slot against the 50-per-property cap, and reports lose the ability to sum or average it. WITHOUT `confirm` IT ARCHIVES NOTHING and instead describes the metric read back from Google — check that against what the user asked for before confirming, because naming the RIGHT metric is the one thing confirm cannot prove. Identify it by parameterName (from list_analytics_definitions), not the report label. A custom DIMENSION of the same name is a DIFFERENT resource and needs archive_analytics_custom_dimension. Needs edit access. 0 credits.
| Parameter | Type | Description |
|---|
propertyrequired | string | NUMERIC GA4 property id |
parameterNamerequired | string | the event parameter of the metric to archive, e.g. 'order_value' |
confirm | boolean | must be true to actually archive — without it nothing is archived and the metric is described back to you |
check_analytics_compatibility#
Check a GA4 report will work, before running it read-only
FREE PRE-FLIGHT FOR analytics_report — ask Google whether these dimensions and metrics can appear in ONE report together, BEFORE running the report that would fail. Use it whenever a report combines fields you have not combined before, and especially when one of them is a custom dimension: GA4 has real pairs that are each perfectly valid alone and cannot be queried together, and `analytics_report` deliberately forwards names AS GIVEN (GA4 publishes hundreds of dimensions and every property ships its own custom ones, so validating against a copied list would go stale and start REFUSING names Google accepts). Pass EXACTLY what you would pass to analytics_report — same metrics, same dimensions, same filters — and it returns which fields are INCOMPATIBLE so you can drop one and run the real report once. IT DOES NOT CHECK THAT A NAME EXISTS: an unknown api name is refused by Google outright, and list_analytics_metadata is what resolves names. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
propertyrequired | string | NUMERIC GA4 property id |
metrics | string[] | the same metrics you would send to analytics_report (defaults to the same three it defaults to) |
dimensions | string[] | the same dimensions you would send to analytics_report |
dimensionFilter | object | the same GA4 FilterExpression you would send to analytics_report |
metricFilter | object | the same GA4 FilterExpression you would send to analytics_report |
compatibilityFilter | "COMPATIBILITY_UNSPECIFIED" | "COMPATIBLE" | "INCOMPATIBLE" | narrow the answer — COMPATIBLE returns only the fields that CAN be added |
PRODUCT ANALYTICS: PostHog · Mixpanel · Amplitude
list_posthog_projects#
List the PostHog projects this key can see read-only
The PostHog projects the connected personal API key can see, and which ONE this profile is pointed at. Only the ACTIVE project is readable. That is deliberate: PostHog's API otherwise falls back to "the last project you visited in the UI", which would make every answer depend on the user's browsing history, so Hermoso pins a project at connect time instead of relying on their implicit default. To move this profile to a different project, the user reconnects PostHog under Settings > Connectors > PostHog, or with connect_connector, with that project id. Read-only, 0 credits.
No parameters.
posthog_query#
Ask PostHog a question in HogQL read-only
ASK POSTHOG A QUESTION IN HogQL — their SQL dialect over the `events`, `persons` and `sessions` tables. Use it for per-user funnels, retention and custom breakdowns, e.g. `SELECT properties.$current_url, count() FROM events WHERE event = '$pageview' AND timestamp > now() - INTERVAL 7 DAY GROUP BY 1 ORDER BY 2 DESC`. IT IS NOT AN EXPORTER AND MUST NEVER BE LOOPED — PostHog's own policy, verbatim: "Third-party connectors must use batch exports, not /query. Connectors built on /query are not supported and will be rate-limited or rejected." Ask ONE bounded question. Do not paginate it, do not schedule it, and do not call it repeatedly to assemble a whole table; if the user genuinely needs bulk data, tell them to set up a PostHog batch export. Every query is capped at 1000 rows, and OFFSET is refused outright because PostHog returns HTTP 400 for it on API keys — use keyset pagination on `timestamp` (events) or `id` (persons) if a second page is truly needed. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
queryrequired | string | a HogQL (ClickHouse-flavoured SQL) query — one bounded question, never a page of an export |
limit | number | row cap, 1–1000 (default 1000). A LIMIT already in the query is honoured and clamped to the same ceiling |
posthog_insight#
Read a saved PostHog insight (funnel / retention / trend) read-only
Read back an insight the user already BUILT in their PostHog UI — funnels, retention curves, trends and paths. Call with NO id to LIST the saved insights with their names and ids; call with insightId for that insight's definition and computed result. THIS IS THE RIGHT WAY TO ANSWER A FUNNEL OR RETENTION QUESTION when the report already exists, and the reason is a documentation fact rather than a preference: PostHog's typed query kinds (FunnelsQuery, RetentionQuery, PathsQuery) are undocumented — their own docs say those "are mostly used to power PostHog internally and are not useful for you" and publish no request payload, no field table and no example for any of them, so building on them would be a private API that can change without notice. The two supported routes are HogQL (posthog_query) and a saved insight (this). Read-only, 0 credits.
| Parameter | Type | Description |
|---|
insightId | string | omit to list the saved insights and their ids |
refresh | boolean | default true (recompute if stale); false = return the cached result even if stale |
list_posthog_session_recordings#
List PostHog session replays read-only
SESSION REPLAYS. Lists recent recordings with who they belong to, how long they ran, how many clicks / keypresses / CONSOLE ERRORS each had, the URL they started on, and a link that opens the replay in PostHog. USE THE SIGNALS RATHER THAN JUST LISTING THEM: a session with console errors and a high keypress count on a checkout page is a bug report with a video attached, and that is the insight worth surfacing. THE RAW REPLAY IS NOT AVAILABLE OVER THE API — PostHog's words: "This endpoint does not provide the raw JSON of the replays. To get the raw JSON, you need to click Export as JSON in the replay options menu in-app." So describe and link; never promise a download and never claim to have watched one. Links need the user's own PostHog login. A publicly shareable link is minted in the PostHog UI and deliberately not by an agent, because it publishes a real person's session and PostHog themselves say they "make no guarantees about sensitive information contained in the recording". Read-only, 0 credits.
| Parameter | Type | Description |
|---|
limit | number | 1–100, default 20 |
posthog_persons#
Look up people in PostHog read-only
Look up PEOPLE in PostHog by distinct id, email or a free-text search, with their properties. Use it to resolve a specific customer, then use their distinct id inside posthog_query to see exactly what that one human did. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
distinctId | string | PostHog distinct id |
email | string | exact email match |
search | string | free-text search across person properties |
limit | number | 1–100, default 20 |
mixpanel_insights#
Read a saved Mixpanel report read-only
READ BACK A SAVED MIXPANEL REPORT by its bookmark id — and this is the PREFERRED Mixpanel lane, not a fallback. Mixpanel has put BOTH its Segmentation and its Funnels query APIs in maintenance mode and recommends in their place that the report is built in the Mixpanel UI and read programmatically through Insights, which is exactly what this does. THE BOOKMARK ID MUST COME FROM THE USER: Mixpanel publishes no endpoint that lists saved reports, so ask them to open the report in Mixpanel and copy the id out of its URL (the part after "report-"). Remember Mixpanel allows only 60 QUERIES PER HOUR across its entire Query API — the tightest budget of any connector here — so reuse an answer rather than re-asking. READ BACK dateRange, do not assume the window: a saved report's date range is configured in Mixpanel's own UI and not by this call, and it comes back stamped with the PROJECT's UTC offset — the only place this connector can observe that timezone at all. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
bookmarkIdrequired | string | the saved report's bookmark id, from its URL in Mixpanel |
mixpanel_retention#
Mixpanel retention cohorts read-only
MIXPANEL RETENTION — how many of the people who did a first thing came back and did another, cohorted by day / week / month. This endpoint is FULLY SUPPORTED, unlike segmentation and funnels, which Mixpanel has put in maintenance mode — so it is the one typed Mixpanel report to reach for first. retentionType "birth" cohorts people by their FIRST occurrence of bornEvent (new-user retention, and Mixpanel's DEFAULT); "compounded" counts anyone active. Warning: MIXPANEL REQUIRES bornEvent WHENEVER retentionType IS "birth", AND BIRTH IS THE DEFAULT — so a call with neither is refused HERE, for free, rather than spending one of the sixty hourly queries on their 400; use list_mixpanel_events first to name a real event. TWO FILTERS, NOT ONE: bornWhere filters who ENTERS the cohort, where filters the RETURNING event. And interval is the WIDTH of each bucket while intervalCount is HOW MANY of them — different knobs. Dates are YYYY-MM-DD and BOTH ENDS ARE INCLUSIVE, resolved in the PROJECT's timezone (UTC unless its owner changed it) rather than in yours. 60 queries/hour across the whole Query API (5 concurrent) — the tightest budget of any connector here, so widen a range rather than looping over days. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
fromDate | string | YYYY-MM-DD (default 30 days ago) |
toDate | string | YYYY-MM-DD (default today) |
retentionType | "birth" | "compounded" | "birth" = cohort by first occurrence of bornEvent; "compounded" = anyone active |
bornEvent | string | the event that puts someone into the cohort — REQUIRED by Mixpanel when retentionType is "birth", which is the default |
event | string | the returning event; omitted means any event |
unit | "day" | "week" | "month" | the interval unit (default day) |
interval | number | the WIDTH of each bucket, in DAYS. Mixpanel cannot take this together with "unit" — it answers HTTP 500 to the pair, so pass one or the other (default: interval 1 day) |
intervalCount | number | HOW MANY buckets to return (default 1). A "0th" bucket is always included for events inside the first interval |
bornWhere | string | a Mixpanel segmentation expression filtering who ENTERS the cohort (the born event) |
where | string | a Mixpanel segmentation expression filtering the RETURNING event |
on | string | a property expression to segment the returning event on — this is what breaks a retention curve down by e.g. plan or platform |
limit | number | top N segmentation values; does nothing unless "on" is set |
unboundedRetention | boolean | accumulate right-to-left, so day N means "retained on day N or any day after" |
mixpanel_segmentation#
Mixpanel event segmentation (maintenance mode) read-only
One Mixpanel event over time, optionally broken down by a property (`on`) and filtered (`where`). Warning: MIXPANEL HAS PUT THIS ENDPOINT IN MAINTENANCE MODE — their words: "We recommend discontinuing new use of this endpoint. To break down and filter event data, build an Insights report in-app and query it programmatically with the Insights Query API." It still answers today, which is why it is offered rather than withheld, but SAY SO when you use it and prefer mixpanel_insights whenever the user can build the report. It takes ONE event name and not an array (the /events endpoints take an array; this one does not — an easy and silent mistake). 60 queries/hour across the whole Query API. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
eventrequired | string | a SINGLE event name — not an array |
fromDate | string | YYYY-MM-DD (default 30 days ago) |
toDate | string | YYYY-MM-DD (default today) |
on | string | a segmentation expression to break down by, e.g. properties["$browser"] |
where | string | a segmentation expression to filter by |
unit | "minute" | "hour" | "day" | "month" | NOTE: Mixpanel offers no "week" on segmentation, unlike retention |
interval | number | the number of days each bucket covers — Mixpanel offers this in lieu of "unit" when "type" is not "general" |
type | "general" | "unique" | "average" | count type: general (event count, default), unique (distinct users) or average |
limit | number | top N property values; Mixpanel defaults to 60, max 10000, and it does nothing unless "on" is set |
mixpanel_funnel#
Read a saved Mixpanel funnel (maintenance mode) read-only
A saved Mixpanel funnel. Call with NO id to LIST the saved funnels and their ids; call with funnelId to read its conversion data. Warning: MIXPANEL HAS PUT THE FUNNELS QUERY API IN MAINTENANCE MODE — their words: "We recommend discontinuing new use of this endpoint. To get funnel data, build a Funnels report in-app and query it programmatically with the Insights Query API." So prefer mixpanel_insights with that report's bookmark id; this is offered because it still answers and because it is the only way to LIST a project's funnels. `length` IS BOUNDED AT 90 DAYS, WHICH IS NOT THE NUMBER 90: it counts lengthUnits, so 90 days is 2160 hours or 129600 minutes, and an over-long window is REFUSED BY NAME rather than trimmed — silently shortening it would answer a different question with nothing to show that it had happened. Omit both and Mixpanel uses whatever the funnel was saved with in its own UI, which is usually what you want. Dates are YYYY-MM-DD and BOTH ENDS ARE INCLUSIVE, resolved in the PROJECT's timezone (UTC unless its owner changed it) rather than in yours. 60 queries/hour across the whole Query API (5 concurrent) — the tightest budget of any connector here, so widen a range rather than looping over days. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
funnelId | string | omit to list the saved funnels and their ids |
fromDate | string | YYYY-MM-DD (default 30 days ago) |
toDate | string | YYYY-MM-DD (default today) |
length | number | the conversion window, counted in lengthUnits — the TOTAL may not exceed 90 days. Omit to use the funnel own saved value |
lengthUnit | "second" | "minute" | "hour" | "day" | the unit "length" is counted in (Mixpanel offers no week or month here). Omit to use the funnel own saved value |
unit | "day" | "week" | "month" | the bucket the results are grouped into |
interval | number | the number of days each bucket covers — an alternative to "unit" (default 1) |
on | string | a property expression to break the funnel down by |
where | string | a segmentation expression to filter by |
limit | number | top N property values; Mixpanel defaults to 255, max 10000, and it does nothing unless "on" is set |
list_mixpanel_events#
List Mixpanel events (or one event’s properties) read-only
The EVENT VOCABULARY of the Mixpanel project — the most common event names over THE LAST 31 DAYS, so a query names something the project actually records rather than a guess. Start here before any other Mixpanel call. Pass `event` to list THAT event's top property names instead; the two are different questions with different answers, and a failure to read properties is reported AS a failure and never as "this event has no properties". Pass window "today" for Mixpanel's separate today-only endpoint, which carries counts and the percent change from yesterday — that is a DIFFERENT and far narrower question, and on a quiet project (or simply early in the project's own timezone day) it legitimately returns nothing while the project still records dozens of event types, so NEVER read an empty today answer as "this project has no events". 60 queries/hour across the whole Query API (5 concurrent) — the tightest budget of any connector here, so widen a range rather than looping over days. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
event | string | pass an event name to list ITS properties instead of listing events |
window | "vocabulary" | "today" | "vocabulary" (default) = the most common events over the last 31 days, names only. "today" = today only, with counts and the change from yesterday — a much narrower question |
type | "general" | "unique" | "average" | count type for the event amounts: general (default), unique or average |
limit | number | Mixpanel own defaults are 255 for the vocabulary, 100 for today and 10 for properties |
amplitude_segmentation#
Amplitude event segmentation read-only
AMPLITUDE EVENT SEGMENTATION — an event over time, with filters and group-bys. metric is uniques / totals / pct_dau / average / histogram / sums / value_avg / formula (and "formula" additionally REQUIRES `formula`, or Amplitude refuses it). Dates take YYYY-MM-DD or YYYYMMDD. AMPLITUDE'S RATE LIMIT IS COST-BASED, not per-request: cost = days × conditions × query type, 1000 per 5-minute window, so one very wide query can exhaust the budget on its own — the fix for a 429 here is to NARROW the range or the conditions, not to retry. AN EMPTY RESULT IS A VALID ANSWER, not a failure: a new or low-traffic Amplitude project genuinely has no data, and reporting that as a broken connection sends the user to fix something that is working. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
events | object[] | one or two event specs: a plain name, or {eventType, filters:[{subprop_type,subprop_key,subprop_op,subprop_value}], groupBy:[…]} |
event | string | shorthand for a single event name |
metric | "uniques" | "totals" | "pct_dau" | "average" | "histogram" | "sums" | "value_avg" | "formula" | default uniques |
formula | string | required when metric is "formula" |
start | string | YYYYMMDD or YYYY-MM-DD (default 30 days ago) |
end | string | YYYYMMDD or YYYY-MM-DD (default today) |
interval | number | -300000 realtime, -3600000 hourly, 1 daily, 7 weekly, 30 monthly |
groupBy | string | user property or event property to break the series down by, e.g. gp:utm_campaign or country |
groupBy2 | string | a SECOND group-by (Amplitude's g2), e.g. groupBy gp:utm_campaign + groupBy2 country |
segments | object[] | FILTER to only these users (Amplitude's segment `s`): [{prop:'gp:utm_campaign', op:'is', values:['summer']}]. op is one of is, is not, contains, does not contain, less, less or equal, greater, greater or equal, set is, set is not. Custom user properties are gp:<name>; built-ins are country, city, region, platform, os, device_type, version, language, paying; prop userdata_cohort + a cohort id filters to a behavioural cohort. A "who performed" condition is {type:'event', event_type, op:'>=', value:1, time_type:'rolling', time_value:30} (did it at least once in the last 30 days); symbol operators only. |
limit | number | ≤1000 |
amplitude_funnel#
Amplitude funnel (conversion + drop-off) read-only
AMPLITUDE FUNNELS: conversion and drop-off across ordered steps. Pass `events` as an ORDERED array of at least two steps. mode is ordered / unordered / sequential. conversionWindowSeconds is how long a user has to complete the funnel and DEFAULTS TO 2,592,000 SECONDS (30 DAYS) — ALWAYS state which window a conversion rate was measured over, because a 30-day window makes a funnel look dramatically healthier than a same-session one and the difference is invisible in the number itself. At most ONE group-by: segmentation allows two, funnels do not, and a second is refused by name rather than silently dropped. Returns step-by-step and cumulative conversion plus median and average transition times. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
eventsrequired | object[] | ordered steps — plain event names, or {eventType, filters:[…]} objects |
mode | "ordered" | "unordered" | "sequential" | default ordered |
conversionWindowSeconds | number | Amplitude default is 2592000 (30 days) — say which window you used |
start | string | YYYYMMDD or YYYY-MM-DD (default 30 days ago) |
end | string | YYYYMMDD or YYYY-MM-DD (default today) |
groupBy | string | at most ONE — a second is refused |
users | "active" | "new" | 'new' = first-time users only (the acquisition funnel); Amplitude's default is active |
segments | object[] | FILTER to only these users (Amplitude's segment `s`): [{prop:'gp:utm_campaign', op:'is', values:['summer']}]. op is one of is, is not, contains, does not contain, less, less or equal, greater, greater or equal, set is, set is not. Custom user properties are gp:<name>; built-ins are country, city, region, platform, os, device_type, version, language, paying; prop userdata_cohort + a cohort id filters to a behavioural cohort. A "who performed" condition is {type:'event', event_type, op:'>=', value:1, time_type:'rolling', time_value:30} (did it at least once in the last 30 days); symbol operators only. |
amplitude_retention#
Amplitude retention read-only
AMPLITUDE RETENTION — of the people who did startEvent, how many came back and did returnEvent. The two magic values are "_new" (first-time users) and "_active" (any active user), which is what makes "new-user retention" a single call. retentionMode is n-day / bracket / rolling, and "bracket" additionally REQUIRES `brackets` — it is refused without them rather than silently switched to another mode, because a retention curve computed under a different definition than the one asked for is a wrong answer that looks right. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
startEvent | string | "_new" (default) or an event name |
returnEvent | string | "_active" (default) or an event name |
start | string | YYYYMMDD or YYYY-MM-DD (default 30 days ago) |
end | string | YYYYMMDD or YYYY-MM-DD (default today) |
retentionMode | "n-day" | "bracket" | "rolling" | n-day, bracket (needs brackets) or rolling |
brackets | number[] | required when retentionMode is "bracket" |
interval | number | 1 daily, 7 weekly, 30 monthly |
groupBy | string | ONE property to group by |
segments | object[] | FILTER to only these users (Amplitude's segment `s`): [{prop:'gp:utm_campaign', op:'is', values:['summer']}]. op is one of is, is not, contains, does not contain, less, less or equal, greater, greater or equal, set is, set is not. Custom user properties are gp:<name>; built-ins are country, city, region, platform, os, device_type, version, language, paying; prop userdata_cohort + a cohort id filters to a behavioural cohort. A "who performed" condition is {type:'event', event_type, op:'>=', value:1, time_type:'rolling', time_value:30} (did it at least once in the last 30 days); symbol operators only. |
amplitude_active_users#
Amplitude active / new users read-only
Amplitude ACTIVE or NEW user counts over a date range — the top-line "is the product growing" number, and the cheapest Amplitude call to run first after connecting because it proves the credential with no setup. metric is "active" or "new"; interval 1 daily, 7 weekly, 30 monthly. An empty series on a new project is the CORRECT answer and means the connection works and there is no data yet — never report it as a failure. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
start | string | YYYYMMDD or YYYY-MM-DD (default 30 days ago) |
end | string | YYYYMMDD or YYYY-MM-DD (default today) |
metric | "active" | "new" | default active |
interval | number | 1 daily, 7 weekly, 30 monthly |
groupBy | string | ONE property to group by |
segments | object[] | FILTER to only these users (Amplitude's segment `s`): [{prop:'gp:utm_campaign', op:'is', values:['summer']}]. op is one of is, is not, contains, does not contain, less, less or equal, greater, greater or equal, set is, set is not. Custom user properties are gp:<name>; built-ins are country, city, region, platform, os, device_type, version, language, paying; prop userdata_cohort + a cohort id filters to a behavioural cohort. A "who performed" condition is {type:'event', event_type, op:'>=', value:1, time_type:'rolling', time_value:30} (did it at least once in the last 30 days); symbol operators only. |
amplitude_user_activity#
One person’s Amplitude activity stream read-only
ONE PERSON'S ACTIVITY STREAM in Amplitude. Pass a user id, device id or user-id PREFIX and it SEARCHES; pass a numeric Amplitude ID and it returns that user's recent events with their properties. Two steps, because Amplitude splits them across two endpoints: search resolves a human-typed identifier to an Amplitude ID, and only that id reads an activity stream. NOTE these two endpoints sit on a DIFFERENT rate limit from the rest of Amplitude — flat counts (10 concurrent, 360 queries/hour) rather than the cost model — so they do not consume the segmentation budget and are not protected by it either. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
userrequired | string | a numeric Amplitude ID to read activity, or a user id / device id / user-id prefix to search |
search | boolean | force a search even when the value is numeric |
limit | number | maximum events to return, up to 1000 |
list_amplitude_events#
Amplitude taxonomy (declared events and properties) read-only
The Amplitude TAXONOMY — the project's declared events, event properties, user properties or group properties, so a query names something real instead of a guess. category is event (default) / category / event-property / user-property / group-property. IF AMPLITUDE REFUSES THIS, REPORT THEIR REFUSAL AND MOVE ON. The Taxonomy API is widely believed to require an enterprise/Govern entitlement, but Amplitude documents no such gating on either the taxonomy page or the Dashboard API page (both checked), so a 403 here is surfaced as AMPLITUDE'S OWN message and is never presented as a Hermoso plan rule, as a broken connection, or as a fact about the user's plan that we cannot actually know. Every other Amplitude tool is unaffected by that refusal. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
category | "category" | "event" | "event-property" | "user-property" | "group-property" | default "event" |
amplitude_chart#
Read a saved Amplitude chart read-only
READ BACK A SAVED AMPLITUDE CHART by its id — the chart the user already built in Amplitude's own UI, with whatever segments, filters and date range they configured there. This is the saved-chart read for Amplitude, and it is the right lane whenever the report already exists: you inherit their definitions instead of rebuilding them from a segmentation query, and it costs one call rather than several. THE CHART ID MUST COME FROM THE USER — Amplitude publishes no endpoint that lists a project's charts — and it is the segment after '/chart/' in the chart's URL (e.g. 'abc123' in https://analytics.amplitude.com/demo/chart/abc123); pasting the whole URL works too. THE RESPONSE SHAPE VARIES BY CHART TYPE, in Amplitude's own words, so read what comes back rather than assuming a series: it may be JSON or a text/CSV body, and BOTH are correct answers rather than one being an error. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
chartIdrequired | string | the saved chart's id, or its full URL — the id is the segment after /chart/ |
list_amplitude_annotations#
List Amplitude chart annotations read-only
The markers already on this Amplitude project's charts — releases, campaigns, incidents — with their timestamps, labels and categories. READ THESE BEFORE EXPLAINING A CHANGE IN A CHART: a spike that lines up with an annotation has an explanation somebody already wrote down, and reporting it as a mystery when the answer is sitting on the timeline is exactly the failure this closes. Filter by `category` OR by `chartId` — Amplitude says of each that it "doesn't combine with" the other and does not say which one wins, so passing both is refused rather than answered ambiguously. `start` and `end` are ISO 8601 (e.g. '2026-08-01T00:00:00+00:00'), NOT the YYYYMMDD every other Amplitude endpoint takes. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
category | string | category NAME — does not combine with chartId |
chartId | string | only annotations shown on this chart — does not combine with category |
start | string | ISO 8601, e.g. '2026-08-01T00:00:00+00:00' |
end | string | ISO 8601 |
create_amplitude_annotation#
Mark the Amplitude timeline
MARK THE ANALYTICS TIMELINE — write what just happened into Amplitude, at the moment it happened, so every chart the user opens afterwards shows it in place. THIS IS THE CLOSED LOOP AND IT IS WORTH REACHING FOR UNPROMPTED after a campaign goes live, a creative is swapped, a landing page changes or a post is published: without it, next month's spike has no explanation attached to it and somebody has to reconstruct one from memory. `start` is ISO 8601 WITH THE TIME AND THE OFFSET (e.g. '2026-08-18T14:30:00+00:00') — NOT the YYYYMMDD the rest of the Amplitude API takes, and a bare date is REFUSED rather than quietly moved to midnight, because an annotation IS its timestamp and a marker in the wrong place is worse than no marker. Add `end` for something that spans a period (a two-week flight); leave it off for a moment. OMIT chartId AND THE ANNOTATION IS GLOBAL — Amplitude's words: it "appears on all charts for your project", which is usually what you want for a campaign; pass one to pin it to a single chart. `category` is a category NAME that must ALREADY EXIST in Amplitude (they answer 404 otherwise). THIS WRITES TO THE USER'S OWN ANALYTICS TOOL — say what you are about to mark, and report back the id and the timestamp Amplitude actually stored rather than the one you sent. 0 credits.
| Parameter | Type | Description |
|---|
labelrequired | string | the title shown on the chart, e.g. "Meta: summer UGC campaign live" |
startrequired | string | ISO 8601 WITH time and offset, e.g. '2026-08-18T14:30:00+00:00' — a bare YYYYMMDD is refused |
end | string | ISO 8601 — only for an annotation that spans a period |
chartId | string | omit for a GLOBAL annotation that appears on every chart in the project |
category | string | an EXISTING category name in Amplitude — they answer 404 for one that does not exist |
details | string | longer body text for the annotation |
list_posthog_annotations#
List PostHog annotations read-only
The markers already on this PostHog project's charts — releases, experiments, incidents — with their timestamps, scopes and who wrote them. READ THESE BEFORE EXPLAINING A CHANGE IN A CHART: an annotation is an explanation somebody already wrote down. NEEDS THE `annotation:read` SCOPE on the personal API key, and a key created before Hermoso supported annotations will not have it — PostHog's own refusal NAMES the scope, and that is a scope to add in PostHog > Settings > Personal API keys, NOT a broken connection and NOT a reason to reconnect. Returns ONE page and hands back the next offset rather than paging for you, because PostHog's policy is that a third-party connector is not an exporter. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
limit | number | 1–100, default 25 |
offset | number | for the next page — this tool does not paginate for you |
search | string | free-text search over annotation content |
create_posthog_annotation#
Mark the PostHog timeline
MARK THE POSTHOG TIMELINE — write what just happened into PostHog at the moment it happened, so every chart afterwards shows it in place. The same closed loop as create_amplitude_annotation, and worth reaching for unprompted after a campaign goes live, a creative is swapped or a landing page changes. `dateMarker` is what POSITIONS the marker on the x-axis (ISO 8601); omit it and NOW is used, which is right for "this just went live" — and the timestamp PostHog actually STORED is reported back rather than the one sent. `scope` is project (the default — shows on everything) / organization / dashboard / dashboard_item, and `dashboard` and `dashboard_item` each need their id or the annotation attaches to nothing and appears nowhere. PostHog also publishes a `recording` scope which THEY THEMSELVES REJECT, so it is refused by name here rather than sent. `creationType` USR means a person wrote it, GIT means a bot or a deployment did. NEEDS THE `annotation:write` SCOPE on the personal API key — a key made before Hermoso supported annotations will not have it; PostHog names it in their refusal, and it is added in PostHog > Settings > Personal API keys without reconnecting. THIS WRITES TO THE USER'S OWN ANALYTICS TOOL. 0 credits.
| Parameter | Type | Description |
|---|
contentrequired | string | the text shown on the chart, ≤8192 characters |
dateMarker | string | ISO 8601 — defaults to now, which is right for "this just went live" |
scope | "project" | "organization" | "dashboard" | "dashboard_item" | default project. PostHog's `recording` scope is deprecated and rejected by PostHog |
creationType | "USR" | "GIT" | USR = a person wrote it, GIT = a bot or deployment did |
dashboardId | number | REQUIRED when scope is "dashboard" |
insightId | number | REQUIRED when scope is "dashboard_item" — the numeric id of the insight |
list_mixpanel_annotations#
List Mixpanel annotations read-only
The markers already on this Mixpanel project's charts (launches, campaigns, incidents) with their date, text, tags and who wrote them. READ THESE BEFORE EXPLAINING A CHANGE IN A CHART: an annotation is an explanation somebody already wrote down. fromDate / toDate (YYYY-MM-DD, both inclusive) narrow the window. Read-only, 0 credits. Needs Mixpanel connected.
| Parameter | Type | Description |
|---|
fromDate | string | YYYY-MM-DD, inclusive |
toDate | string | YYYY-MM-DD, inclusive |
create_mixpanel_annotation#
Mark the Mixpanel timeline
MARK THE MIXPANEL TIMELINE: write what just happened into Mixpanel at the moment it happened, so every chart afterwards shows it in place. The same closed loop as create_posthog_annotation and create_amplitude_annotation, worth reaching for unprompted after a campaign goes live, a creative is swapped or a landing page changes. `date` is "YYYY-MM-DD HH:mm:ss" (Mixpanel's own format, charted in the PROJECT's timezone), "YYYY-MM-DD", or an ISO timestamp with its offset (written in the project's timezone so the mark sits at that exact instant, read back to prove it); omit it for NOW. tags are numeric annotation-tag ids. The stored row is read back. Mixpanel only lets a service account with at least the ANALYST role write annotations; a lower role is refused by name, and that is a role change in Mixpanel, not a reconnect. 0 credits. Needs Mixpanel connected.
| Parameter | Type | Description |
|---|
descriptionrequired | string | the text shown on the chart, e.g. "Meta: summer UGC campaign went live" |
date | string | "YYYY-MM-DD HH:mm:ss", "YYYY-MM-DD" or ISO with offset; default now |
tags | number[] | annotation-tag ids |
update_mixpanel_annotation#
Edit a Mixpanel annotation destructive
Change the text or tags of a Mixpanel annotation (annotationId from list_mixpanel_annotations). Mixpanel cannot move an annotation to a new date; to fix a wrong date, delete it and create a new one. The edited row is read back. Needs the ANALYST role on the service account. 0 credits. Needs Mixpanel connected.
| Parameter | Type | Description |
|---|
annotationIdrequired | number | from list_mixpanel_annotations |
description | string | new annotation text; empty text is refused |
tags | number[] | annotation-tag ids |
delete_mixpanel_annotation#
Delete a Mixpanel annotation destructive
Permanently delete a Mixpanel annotation from every chart in the project. WITHOUT confirm:true nothing is deleted and you get the exact annotation that would go, to show the user; call again with confirm:true only after they say yes. The delete is verified by reading it back. Needs the ANALYST role on the service account. 0 credits. Needs Mixpanel connected.
| Parameter | Type | Description |
|---|
annotationIdrequired | number | from list_mixpanel_annotations |
confirm | boolean | true only after the user approved deleting this exact annotation |
list_posthog_surveys#
PostHog surveys read-only
PostHog SURVEYS. Without surveyId: the project's surveys (name, type, running / complete / draft / archived, dates, questions); filter by status or search. With surveyId: that survey's questions (with their ids, for posthog_survey_responses) plus its stats: shown / dismissed / sent counts, response and dismissal rates, and per-question distributions and rating averages. NEEDS THE OPTIONAL survey:read SCOPE on the PostHog personal API key: a key without it connects fine and works for everything else, and this tool then says exactly which box to tick in PostHog > Settings > Personal API keys. That is never a reconnect. Read-only, 0 credits. Needs PostHog connected.
| Parameter | Type | Description |
|---|
surveyId | string | one survey: its questions and stats |
status | "running" | "complete" | "draft" | only surveys in this state |
search | string | match survey names containing this text |
archived | boolean | PostHog archived filter: true or false; omit to leave it unfiltered |
limit | number | 1-100, default 25 |
offset | number | skip this many surveys; use the nextOffset from the previous reply |
dateFrom | string | stats window start, ISO |
dateTo | string | stats window end, ISO |
posthog_survey_responses#
PostHog survey responses read-only
THE ANSWERS people gave a PostHog survey, with the QUESTION TEXT resolved beside each answer: who (distinct_id), when, country, device and the page they were on. The customers' own words: the best raw input for ad hooks, angles and objections. questionId narrows to one question; with scoreLte / scoreGte on a rating question it pulls e.g. NPS detractors (scoreLte 6) or promoters (scoreGte 9). since / until are ISO timestamps. One page (limit 1-500, default 100) and the next offset when there is more. NEEDS THE OPTIONAL survey:read SCOPE on the PostHog personal API key (plus query:read, which every connection already has); without it this tool names the box to tick in PostHog, never a reconnect. Read-only, 0 credits. Needs PostHog connected.
| Parameter | Type | Description |
|---|
surveyIdrequired | string | from list_posthog_surveys |
questionId | string | one question id from the survey (see list_posthog_surveys with surveyId) |
scoreLte | number | needs questionId; e.g. 6 for NPS detractors |
scoreGte | number | needs questionId; e.g. 9 for NPS promoters |
since | string | ISO timestamp |
until | string | ISO timestamp |
excludeArchived | boolean | true = leave out responses to archived questions |
limit | number | 1-500, default 100 |
offset | number | skip this many responses; use the nextOffset from the previous reply |
list_mixpanel_cohorts#
List saved Mixpanel cohorts read-only
THE SAVED COHORTS in the Mixpanel project — each with its id, name, description and CURRENT MEMBER COUNT. Two uses, and both matter for advertising: the counts alone answer "how big is our converted audience", and the id is what mixpanel_profiles needs to read the PEOPLE in one. A cohort hidden in Mixpanel's UI is still listed and flagged rather than dropped, because hidden is a display choice and it is still queryable by id. ONE CALL, and the cheapest thing on this connector — worth calling before mixpanel_profiles rather than guessing an id. Remember Mixpanel allows only 60 QUERIES PER HOUR across its entire Query API (5 concurrent), the tightest budget of any connector here, and this shares it with every other Mixpanel tool. Read-only, 0 credits.
No parameters.
mixpanel_profiles#
Read the people in a Mixpanel cohort read-only
THE PEOPLE in Mixpanel, with their profile properties. Pass `cohortId` (from list_mixpanel_cohorts) for "who is in the Converted cohort and what do we know about them", or `where` for an ad-hoc filter over profile properties, or `distinctId` for one person. PASS `outputProperties` ALMOST ALWAYS: it cuts the payload to the properties you actually need, and Mixpanel says it "can drastically reduce the amount of data returned" and "can speed up queries significantly". RETURNS ONE PAGE AND DOES NOT PAGINATE FOR YOU — it hands back page, pageSize, total and a sessionId, and Mixpanel REQUIRES that sessionId alongside any page above 0, so a second page is a deliberate second call. DO NOT LOOP IT: Mixpanel allows 60 queries per hour across its ENTIRE Query API and each page spends one of them. REPORT `total` (the whole cohort) SEPARATELY FROM `count` (this page) — quoting only the page size understates the audience, which is the number a targeting decision gets made on. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
cohortId | string | a numeric cohort id from list_mixpanel_cohorts |
where | string | a Mixpanel segmentation expression over profile properties, e.g. properties["$city"] == "London" |
distinctId | string | one person |
distinctIds | string[] | several people by distinct id |
outputProperties | string[] | only return these properties — strongly recommended, it is much faster and much smaller |
dataGroupId | string | group key — for GROUP profiles rather than user profiles |
page | number | pages start at ZERO; any page above 0 REQUIRES sessionId |
sessionId | string | from the previous page — required for page > 0 |
includeAllUsers | boolean | only applies alongside cohortId; false = only distinct ids that actually have a profile |
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. 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
keywords | string[] | seed terms to expand from |
url | string | a landing page for Microsoft to mine ideas from |
categoryId | number | Microsoft keyword category id to mine ideas from |
locationIdsrequired | string[] | REQUIRED — Microsoft location ids (microsoft_ads_geo_search resolves names to ids, free) |
language | string | default English |
network | "OwnedAndOperatedAndSyndicatedSearch" | "OwnedAndOperatedOnly" | "SyndicatedSearchOnly" | search network the volume is measured on: Microsoft-owned, partner-syndicated or both |
competition | string[] | filter to Low | Medium | High |
minSearchVolume | number | only ideas with at least this many monthly searches |
maxSearchVolume | number | only ideas with at most this many monthly searches |
attributes | string[] | which idea attributes to return — omit for all |
expandIdeas | boolean | false = 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
keywordsrequired | string[] | the keywords to estimate |
maxCpcrequired | number | REQUIRED — the max CPC bid to estimate at, in the account currency |
matchType | "Exact" | "Phrase" | "Broad" | default Exact |
locationIdsrequired | string[] | REQUIRED — Microsoft location ids |
language | string | language to estimate for, e.g. English |
network | string | search network to estimate for; same values as microsoft_ads_keyword_ideas network |
dailyBudget | number | daily budget in account currency the estimate assumes |
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.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
campaignId | string | omit for the whole account |
microsoft_ads_geo_search#
Find Microsoft Advertising location ids read-only
Resolve country / region / city names to the Microsoft Advertising location ids that create_microsoft_ads_campaign needs. Read-only, free, 0 credits. Use it when a location ask is ambiguous ("Springfield") — this returns EVERY candidate with its id so the USER can pick, and you never guess between two places. Accepts names, ISO country codes ("CA"), or numeric location ids. Pass `query` as ONE ask (a plain string) or SEVERAL (an array of strings) — a comma is part of a place's name ("Seattle, Washington, United States"), never a separator. Postal codes and neighbourhoods are not name-searchable — pass their numeric location id straight through; the campaign read-back reports the name Microsoft resolves for it.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
queryrequired | string | string[] | ONE location ask as a plain string, or several as an array of strings — names, ISO country codes, or numeric Microsoft location ids. A comma belongs to a name ("Seattle, Washington, United States") and is NOT a separator: pass several places as several array items, never one comma-joined string. |
create_microsoft_ads_campaign#
Build a Microsoft Advertising campaign (paused) destructive
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, 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
namerequired | string | campaign name, ≤128 characters |
dailyBudgetrequired | number | daily budget in the account’s currency |
budgetType | "DailyBudgetStandard" | "DailyBudgetAccelerated" | "LifetimeBudgetStandard" | default DailyBudgetStandard; Accelerated is Audience-campaign only |
campaignType | string | Search (default), Shopping, DynamicSearchAds, Audience or PerformanceMax. Search (and DynamicSearchAds, which Microsoft now builds AS a Search campaign) take the full tree below; Shopping, Audience and PerformanceMax are created as a campaign on its own (finish it in Microsoft Advertising), and passing `adGroup` with one of those is refused before anything is created. PER-TYPE SETTINGS: Shopping needs storeId (a Microsoft Merchant Center store; with exactly one store on the account it is used and named in the reply, with none the create is refused in plain words). DynamicSearchAds needs dynamicSearchAds{domainName, language}. PerformanceMax needs a language (none given = All, said in the reply) and takes biddingScheme/targetCpa/targetRoas, finalUrlExpansionOptOut and an optional storeId for a retail campaign. |
storeId | string | Shopping (required) or retail Performance Max (optional): the Microsoft Merchant Center store id. Checked against the account’s own stores before anything is created. |
shoppingPriority | integer | Shopping only: 0 (Low, the default), 1 or 2 — decides which campaign serves when several share a store. Performance Max is always 4, set for you. |
salesCountryCode | string | Shopping: two-letter country to filter the store catalog to, e.g. "US". Not with feedLabel. |
feedLabel | string | Shopping: advertise every product with this feed label (≤20 characters). Not with salesCountryCode. |
localInventoryAds | boolean | Shopping: enable local inventory ads (only where Microsoft has enabled the feature for the account) |
dynamicSearchAds | object | Dynamic search ads: required for campaignType DynamicSearchAds, optional on Search. Microsoft retired the DynamicSearchAds TYPE, so the campaign is a Search campaign carrying this setting. |
biddingScheme | "EnhancedCpc" | "MaxClicks" | "MaxConversions" | "MaxConversionValue" | "TargetImpressionShare" | "TargetCpa" | "TargetRoas" | bid strategy. Search takes any; Audience EnhancedCpc/MaxConversions; Shopping EnhancedCpc; Performance Max MaxConversions/MaxConversionValue. TargetCpa/TargetRoas are sent as MaxConversions/MaxConversionValue with the target (Microsoft converts them silently since 2025-08-04). Omitted = Microsoft’s default, read back in the reply. Change it later with set_microsoft_ads_bidding |
maxCpc | number | bid cap for MaxClicks / MaxConversions / MaxConversionValue / TargetImpressionShare (not Performance Max) |
targetImpressionShare | number | 1–100, with biddingScheme TargetImpressionShare |
targetAdPosition | "FirstPage" | "MainLine" | "MainLine1" | with TargetImpressionShare; MainLine1 = the very top |
targetCpa | number | Performance Max with MaxConversions: target cost per acquisition, account currency |
targetRoas | number | Performance Max with MaxConversionValue: target return on ad spend as a ratio, e.g. 4 = 400% |
finalUrlExpansionOptOut | boolean | Performance Max: true = serve only the final URLs you give; default false lets Microsoft use the whole domain |
timeZone | string | Microsoft time-zone enum (e.g. EasternTimeUSCanada); default the ad account’s own. Microsoft marks a campaign time zone deprecated and may keep its own — the reply states the zone it stored |
locations | string[] | 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 |
excludeLocations | string[] | 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 |
worldwide | boolean | set 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 |
languages | string[] | campaign languages, e.g. ["English"] |
adGroup | object | build 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 destructive
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.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
campaignIdrequired | string | the campaign this ad group belongs to |
namerequired | string | ad group name, ≤256 characters |
status | "Active" | "Paused" | default Paused |
cpcBid | number | default max CPC in the account currency |
language | string | required if the campaign has no language set |
ad | object | create the responsive search ad in the same call |
keywords | object[] | keywords to add: [{text, matchType, bid, status}]; status defaults to Paused |
confirm | boolean | REQUIRED true to create this Active under a LIVE campaign |
create_microsoft_ads_ad#
Create a Microsoft responsive search ad destructive
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.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
adGroupIdrequired | string | the ad group this ad lives in |
headlines | string[] | 3–15 headlines, each ≤30 characters |
descriptions | string[] | 2–4 descriptions, each ≤90 characters |
finalUrls | string[] | the landing page(s) — at least one is required |
path1 | string | display-URL path segment, ≤15 chars, no "/" |
path2 | string | second display-URL path segment (only with path1) |
status | "Active" | "Paused" | default Paused |
confirm | boolean | REQUIRED 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
adGroupIdrequired | string | the ad group to add them to |
keywordsrequired | object[] | the keywords |
confirm | boolean | REQUIRED true to add an Active keyword to a LIVE ad group |
search_microsoft_ads_profiles#
Find Microsoft Advertising LinkedIn profile ids read-only
Find the LinkedIn profile ids Microsoft Advertising can target: a company (CompanyName), an Industry or a JobFunction. A company is found with Microsoft’s own company search (3+ characters); industries and job functions are matched against the profile list Microsoft publishes for them. Microsoft publishes no lookup for JobTitle or JobSeniority ids, so those cannot be searched here; an id you already have can still be targeted. Read-only, free. Pass the ids to set_microsoft_ads_profile_targeting.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
profileTyperequired | "CompanyName" | "Industry" | "JobFunction" | what to search: CompanyName, Industry or JobFunction |
queryrequired | string | part of the name to find (3+ characters for a company) |
languageLocale | string | en (default), fr, de, it, pt-BR, es or zh-Hant |
limit | number | 1-100, default 25 |
list_microsoft_ads_profile_targeting#
Read a Microsoft Advertising campaign’s LinkedIn profile targeting read-only
Read the LinkedIn profile targeting on a Microsoft Advertising campaign: every company, industry, job function, seniority and job title criterion with its bid adjustment and criterion id, read from Microsoft. A profile type Microsoft would not read is named as unread, never reported as none. Read-only, free.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
campaignIdrequired | string | Microsoft campaign id to read LinkedIn profile targeting from |
set_microsoft_ads_profile_targeting#
Set LinkedIn profile targeting on a Microsoft Advertising campaign destructive
Target LinkedIn profiles on a Microsoft Advertising Search, Shopping or Dynamic Search Ads campaign: add companies, industries, job functions, seniorities or job titles, each with an optional bid adjustment (whole percent, -90 to 900), or change an existing adjustment by criterion id (list_microsoft_ads_profile_targeting shows it). Find profile ids with search_microsoft_ads_profiles. On an ACTIVE campaign this changes bids on the very next auction, so show the user the changes, get an explicit yes, then call with confirm:true. The reply is what Microsoft reads back, and a change that does not read back is named. Removing a profile criterion is not available here; an adjustment of 0 has no effect on bids.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
campaignIdrequired | string | Microsoft campaign id to change LinkedIn profile targeting on |
add | object[] | profile criteria to add: [{profileType, profileId, bidAdjustment}] |
update | object[] | bid adjustments to change on existing criteria: [{criterionId, bidAdjustment}] |
confirm | boolean | REQUIRED true when the campaign is ACTIVE |
set_microsoft_ads_budget#
Set a Microsoft Advertising daily budget destructive
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.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
campaignIdrequired | string | the campaign whose budget changes |
dailyBudgetrequired | number | new daily budget in the account’s currency |
confirm | boolean | REQUIRED true to change the budget of a LIVE campaign |
set_microsoft_ads_status#
Activate or pause a Microsoft Advertising campaign / ad group / ad destructive
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.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
level | "campaign" | "adGroup" | "ad" | what to change — default campaign |
campaignId | string | campaign id (level:"campaign") |
adGroupId | string | ad group id (level:"adGroup", or with adId for level:"ad") |
adId | string | ad id (level:"ad" — pass adGroupId too) |
statusrequired | "Active" | "Paused" | Active = start spending; Paused = stop |
confirm | boolean | REQUIRED 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
level | "campaign" | "adGroup" | "ad" | "keyword" | what to delete — default campaign |
campaignId | string | campaign id (level:"campaign"; also the parent for level:"adGroup" if you know it) |
adGroupId | string | ad group id (level:"adGroup"; REQUIRED as the parent for level:"ad" and level:"keyword") |
adId | string | ad id (level:"ad" — pass adGroupId too) |
keywordId | string | keyword id (level:"keyword" — pass adGroupId too) |
confirm | boolean | REQUIRED true — the delete is permanent |
confirmName | string | the object’s EXACT name, required when it has children / is Active / has spent |
confirmChildren | number | the exact number of children reported by the unconfirmed call, required when it has any |
Microsoft Merchant Center
list_microsoft_merchant_stores#
List Microsoft Merchant Center stores read-only
List the brand's MICROSOFT MERCHANT CENTER stores — the product-feed side of Microsoft Advertising, the exact twin of Google Merchant Center. THIS IS WHERE THE merchantId COMES FROM: every other Microsoft Merchant tool needs it and there is no other way to discover it (Content API's own Store resource is closed-beta only, so Hermoso reads the list from Campaign Management instead, which is open to everyone). Each row carries the store name, its URL, and whether it is active, has a catalog and has product ads enabled. Microsoft's own caveat, which the note repeats: those flags are set inside Merchant Center and are read-only here — an inactive store can still be referenced by a Shopping campaign. An account with NO store has no product feed at all, and a store cannot be created through the API. Read-only, free, 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
list_microsoft_merchant_products#
List Microsoft Merchant Center product offers read-only
Read the product offers in a Microsoft Merchant Center store — the feed a Microsoft Shopping campaign serves from. Pass productId to fetch ONE offer, or omit it to page the store (limit up to 250, then pass the returned pageToken). MICROSOFT'S PRODUCT IDS ARE COMPOSED AND CASE SENSITIVE: the id is channel:contentLanguage:targetCountry:offerId (e.g. Online:en:US:Sku123), not the bare offerId, and a re-cased id addresses nothing — always use the id this tool returned. Read-only, free, 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
merchantId | string | Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store |
productId | string | fully qualified id (channel:contentLanguage:targetCountry:offerId, e.g. Online:en:US:Sku123) to fetch ONE offer |
limit | number | up to 250, default 25 |
pageToken | string | the nextPageToken from a previous call |
upsert_microsoft_merchant_product#
Add or update Microsoft Merchant Center product offers destructive
Add or update product offers in a Microsoft Merchant Center store. THE ONE THING TO KNOW BEFORE CALLING IT: Microsoft has NO partial update — 'because an update is an insert operation, you must include all fields of the offer in the request' — so a title-only 'update' CLEARS every other field. Always send the whole offer. Ten fields are required: availability, channel, condition, contentLanguage, imageLink, link, offerId, price, targetCountry, title; brand/gtin/mpn are strongly recommended and their absence is set as identifierExists:false for you. Pass product for one offer or products[] for a batch (Hermoso caps a batch at 300 — Microsoft publishes two contradictory ceilings for this and we take the lower). Pass dryRun:true to validate against Microsoft without writing anything; Microsoft returns no ids on a dry run, so none is claimed. Feed edits cannot spend and cannot start an auction, but an offer goes through editorial review — being STORED is not the same as being served. Everything is read back from Microsoft. 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
merchantId | string | Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store |
product | object | ONE whole offer — availability, channel, condition, contentLanguage, imageLink, link, offerId, price{value,currency}, targetCountry, title are required; brand/gtin/mpn strongly recommended |
products | object[] | several whole offers in one batch, maximum 300 |
catalogId | string | write into a specific catalog instead of the store default |
dryRun | boolean | validate against Microsoft and write nothing — Microsoft returns no ids for a dry run, so none is claimed |
delete_microsoft_merchant_product#
Delete a Microsoft Merchant Center product offer destructive
Delete one product offer from a Microsoft Merchant Center store. Pass productId as the FULLY QUALIFIED id (channel:contentLanguage:targetCountry:offerId), not the offerId. Call without confirm first — nothing is deleted and you get Microsoft's own better advice back: deleted products can take UP TO 12 HOURS to stop delivering, so if the goal is to stop showing it today, upsert it with availability 'out of stock' instead. The outcome is READ BACK by re-fetching the offer, and says 'deleted', 'not confirmed' or 'could not tell' — repeat that verbatim rather than claiming success from the delete's own empty response. 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
merchantId | string | Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store |
productIdrequired | string | the FULLY QUALIFIED id (channel:contentLanguage:targetCountry:offerId), not the offerId |
confirm | boolean | REQUIRED true — call without it first to see Microsoft’s own advice about the 12-hour delivery tail |
list_microsoft_merchant_issues#
Why Microsoft Merchant Center products are not serving read-only
Why a Microsoft Merchant Center store's products are NOT serving — the approved / disapproved / expiring counts plus the per-offer disapprovals and warnings with Microsoft's own issue codes. This is the tool to reach for when a Microsoft Shopping campaign is live and showing nothing. TWO CAVEATS THAT LOOK LIKE FAILURES AND ARE NOT, both repeated in the note: Microsoft returns detail rows ONLY for offers that are Disapproved or in Warning, so an EMPTY detail list is the healthy state; and a status change takes up to two hours to reach the summary, so a fresh upload legitimately shows as nothing yet. Read-only, free, 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
merchantId | string | Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store |
limit | number | up to 250, default 25 |
pageToken | string | nextPageToken from a previous result, to get the next page |
list_microsoft_merchant_catalogs#
List Microsoft Merchant Center catalogs read-only
List the catalogs inside a Microsoft Merchant Center store. Catalogs logically group products, and a Shopping campaign's product scope points at them. Each row says whether publishing is ENABLED — products in a catalog with publishing off do not serve at all, which is a common and invisible reason a feed appears healthy and delivers nothing. Products go into the store's default catalog unless a call names catalogId. Read-only, free, 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
merchantId | string | Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store |
manage_microsoft_merchant_catalog#
Create, update or delete a Microsoft Merchant Center catalog destructive
Create, update or delete a catalog in a Microsoft Merchant Center store. Creating needs name + market + isPublishingEnabled; updating needs catalogId + name + isPublishingEnabled (market is not settable after creation). isPublishingEnabled is NEVER defaulted on — products serve only when it is true, so publishing a catalogue is always an explicit choice. Names must be unique within the store and are capped at 70 characters. Pass deleteIt:true with catalogId and confirm:true to remove one. Every outcome is READ BACK from Microsoft. 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
merchantId | string | Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store |
catalogId | string | set to UPDATE or DELETE an existing catalog; omit to create a new one |
name | string | unique within the store, maximum 70 characters |
market | string | REQUIRED when creating — where the products are served, e.g. en-US. Not settable afterwards |
isPublishingEnabled | boolean | products serve ONLY when this is true — never defaulted on |
deleteIt | boolean | true to delete the catalog named by catalogId |
confirm | boolean | REQUIRED true to delete |
microsoft_ads_editorial_reasons#
Why a Microsoft ad or keyword was disapproved read-only
WHY a Microsoft Advertising ad or keyword was DISAPPROVED: Microsoft's editorial reason codes, the word or phrase it objected to (term), where it appeared (location), the countries it applies to, and any appeal status, read live from Microsoft. Use it whenever a read-back shows editorialStatus Disapproved, or the user asks why a Bing ad is not running. Pass adGroupId plus adIds OR keywordIds (Microsoft will not mix ads and keywords in one request). The reply links Microsoft's table of what each code means. Free.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
adGroupIdrequired | string | the ad group the ads or keywords sit in |
adIds | string[] | the ads to explain |
keywordIds | string[] | or the keywords to explain (not both) |
Microsoft Advertising: negatives, bidding, conversion tracki…
list_microsoft_ads_negative_keywords#
List Microsoft Advertising negative keywords read-only
The NEGATIVE keywords (excluded searches) on a Microsoft Advertising campaign (campaignId) or ad group (adGroupId) with their ids and match types; with no id, the account’s SHARED negative keyword lists; with listId, the keywords in one list. Read-only, free.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
campaignId | string | campaign whose negative keywords to list |
adGroupId | string | ad group whose negative keywords to list |
listId | string | a shared negative keyword list |
add_microsoft_ads_negative_keywords#
Exclude searches on Microsoft Advertising (negative keywords)
EXCLUDE searches from a Microsoft Advertising campaign or ad group with negative keywords — Exact or Phrase only (Microsoft has no broad negative; default Phrase, which blocks every search containing the phrase), up to 20,000 per campaign or ad group. Broad-match keywords without negatives are the classic way to waste money: “stop paying for free” is this tool. It can only narrow spend, so it is never confirm-gated. Read back. Free.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
campaignId | string | campaign to add negatives to; pass this or adGroupId |
adGroupId | string | pass this OR campaignId |
keywordsrequired | object[] | negative keywords, each with text and matchType (Exact or Phrase) |
remove_microsoft_ads_negative_keywords#
Remove Microsoft Advertising negative keywords destructive
Remove negative keywords (by id, from list_microsoft_ads_negative_keywords) from a Microsoft Advertising campaign or ad group. On a LIVE campaign that lets those searches trigger paid ads again, so it needs confirm:true after the user agrees. Read back. Free.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
campaignId | string | campaign to remove negatives from; pass this or adGroupId |
adGroupId | string | ad group to remove negatives from; pass this or campaignId |
negativeKeywordIdsrequired | string[] | ids of the negative keywords to remove, from list_microsoft_ads_negative_keywords |
confirm | boolean | REQUIRED true on a LIVE campaign |
manage_microsoft_ads_negative_keyword_list#
Manage a shared Microsoft negative keyword list destructive
A SHARED negative keyword list on Microsoft Advertising — one set of exclusions reused across up to 20 campaigns. action: create (name + keywords; campaignIds attaches it straight away), add (keywords), remove (negativeKeywordIds), attach / detach (campaignIds), delete (permanent, confirm:true). Removing, detaching or deleting on a LIVE campaign lets those searches trigger paid ads again, so it needs confirm:true. Read back. Free.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
actionrequired | "create" | "add" | "remove" | "attach" | "detach" | "delete" | create, add, remove, attach, detach or delete a shared negative keyword list |
listId | string | every action but create |
name | string | create: ≤255 characters |
keywords | object[] | keywords for create or add: [{text, matchType}] |
negativeKeywordIds | string[] | keyword ids to remove from the list (action remove) |
campaignIds | string[] | campaigns to attach to or detach from the list (action attach or detach) |
confirm | boolean | true to delete the list, or to remove, detach or delete on a LIVE campaign |
set_microsoft_ads_bidding#
Change a Microsoft Advertising bid strategy destructive
Change a Microsoft Advertising campaign’s BID STRATEGY, read back: EnhancedCpc, MaxClicks (maxCpc cap), MaxConversions (optional targetCpa), MaxConversionValue (optional targetRoas), TargetImpressionShare (targetImpressionShare + targetAdPosition). Search takes any; Audience EnhancedCpc / MaxConversions; Shopping EnhancedCpc; Performance Max MaxConversions / MaxConversionValue. TargetCpa and TargetRoas are sent as MaxConversions / MaxConversionValue with the target, which is what Microsoft converts them to since 2025-08-04. Conversion strategies need a UET tag and a conversion goal recording conversions. On a LIVE campaign it needs confirm:true. Free.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
campaignIdrequired | string | Microsoft campaign id whose bid strategy changes |
biddingSchemerequired | "EnhancedCpc" | "MaxClicks" | "MaxConversions" | "MaxConversionValue" | "TargetImpressionShare" | "TargetCpa" | "TargetRoas" | new bid strategy; TargetCpa and TargetRoas are sent as MaxConversions and MaxConversionValue |
maxCpc | number | bid cap in account currency, for MaxClicks and MaxConversions |
targetCpa | number | target cost per conversion in account currency, for MaxConversions |
targetRoas | number | ratio, e.g. 4 = 400% |
targetImpressionShare | number | 1–100 |
targetAdPosition | "FirstPage" | "MainLine" | "MainLine1" | FirstPage, MainLine or MainLine1 (MainLine1 = very top), with TargetImpressionShare |
confirm | boolean | REQUIRED true on a LIVE campaign |
list_microsoft_ads_uet_tags#
List Microsoft Advertising UET tags read-only
The UET tags (Microsoft’s site tracking pixel) on this Microsoft Advertising customer: id, name, status (Active / Inactive / Unverified) and each tag’s install snippet. Read-only, free.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
tagIds | string[] | only these UET tag ids; omit for all tags |
create_microsoft_ads_uet_tag#
Create a Microsoft Advertising UET tag
Create a UET tag (Microsoft’s site tracking pixel) and get its install snippet, read back. Url / Event / Duration / PagesViewedPerVisit conversion goals, remarketing lists and conversion bidding all need one. Name ≤100 characters, unique per customer. Free.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
namerequired | string | tag name, up to 100 characters, unique per customer |
description | string | optional note on what the tag tracks |
create_microsoft_ads_conversion_goal#
Create a Microsoft Advertising conversion goal
Create a Microsoft Advertising CONVERSION GOAL, read back. type: Url (a thank-you page: urlExpression + urlOperator), Event (a UET custom event: eventAction / eventCategory / eventLabel / eventValue), OfflineConversion (conversions uploaded from a CRM with send_microsoft_ads_offline_conversions — no tag; Microsoft asks for 2 hours before the first upload), Duration (minimumDurationSeconds) or PagesViewedPerVisit (minimumPagesViewed). Every type but OfflineConversion needs tagId (create_microsoft_ads_uet_tag). category is required for Url / Event / Offline. Free.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
namerequired | string | ≤100 characters, unique per customer |
typerequired | "Url" | "Event" | "OfflineConversion" | "Duration" | "PagesViewedPerVisit" | Url (thank-you page), Event (UET custom event), OfflineConversion, Duration or PagesViewedPerVisit |
tagId | string | UET tag id from create_microsoft_ads_uet_tag; not needed for OfflineConversion |
category | "AddToCart" | "BeginCheckout" | "BookAppointment" | "Contact" | "Download" | "GetDirections" | "OutboundClick" | "PageView" | "Purchase" | "RequestQuote" | "Signup" | "SubmitLeadForm" | "Subscribe" | "Other" | conversion category, e.g. Purchase or Signup; required for Url, Event and OfflineConversion |
urlExpression | string | Url goals: the thank-you page URL or path to match |
urlOperator | "Equals" | "BeginsWith" | "Contains" | "RegularExpression" | Url goals: how urlExpression matches the page: Equals, BeginsWith, Contains or RegularExpression |
eventAction | string | Event goals: UET custom event action |
eventCategory | string | Event goals: UET custom event category |
eventLabel | string | Event goals: UET custom event label |
eventValue | number | Event goals: numeric value attached to each event |
minimumDurationSeconds | number | Duration goals: visit length in seconds that counts as a conversion |
minimumPagesViewed | number | PagesViewedPerVisit goals: page count that counts as a conversion |
revenueType | "FixedValue" | "VariableValue" | "NoValue" | FixedValue (one value per conversion), VariableValue or NoValue |
revenue | number | FixedValue: the value of one conversion; VariableValue: the default |
currencyCode | string | 3-letter ISO currency code for the revenue value, e.g. USD |
countType | "All" | "Unique" | All or Unique: how repeat conversions are counted |
conversionWindowDays | number | 1–90 |
scope | "Account" | "Customer" | Account = this ad account only; Customer = shared across the customer |
list_microsoft_ads_audience_targeting#
Audiences on a Microsoft campaign or ad group read-only
The audiences (customer lists, remarketing, in-market, custom, similar, combined, product) targeted on — or EXCLUDED from — a Microsoft Advertising campaign or ad group, with each one’s bid adjustment and criterion id. Read-only, free.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
campaignId | string | campaign whose audience targeting to list; pass this or adGroupId |
adGroupId | string | ad group whose audience targeting to list; pass this or campaignId |
set_microsoft_ads_audience_targeting#
Target or exclude a Microsoft audience destructive
Put an audience to work on a Microsoft Advertising campaign or ad group — e.g. the customer list from create_microsoft_ads_customer_list, to retarget it or EXCLUDE existing customers. action add (audienceId, exclude, bidAdjustment -90..900 %) or remove (criterionIds from list_microsoft_ads_audience_targeting). Microsoft allows biddable audiences at the campaign OR the ad-group level of one campaign, not both; exclusions at both; Performance Max can only exclude. Targeting or removing on a LIVE campaign needs confirm:true. Read back. Free.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
campaignId | string | campaign to target or exclude the audience on; pass this or adGroupId |
adGroupId | string | ad group to target or exclude the audience on; pass this or campaignId |
action | "add" | "remove" | default add |
audienceId | string | list_microsoft_ads_audiences |
exclude | boolean | true = exclude this audience instead of targeting it |
bidAdjustment | number | -90 to 900 (%); 0 = target without changing the bid |
criterionIds | string[] | criterion ids to remove (action remove), from list_microsoft_ads_audience_targeting |
confirm | boolean | true required on a LIVE campaign when targeting or removing |
create_microsoft_ads_image_ad#
Create a Microsoft image ad from a render (paused) destructive
Turn a Hermoso render, or the user’s own picture (any https URL, or a file via upload_file), into a Microsoft Advertising IMAGE ad: a responsive ad on the Microsoft Audience Network (an Audience campaign) or a multimedia ad on Search. One imageUrl is centre-cropped to the 1.91:1 and 1:1 pictures Microsoft requires (or pass images{landscape, square, logo}), uploaded, and the ad is built PAUSED and read back with its pictures. businessName ≤25, 1–15 headlines ≤30, 1–5 longHeadlines ≤90, 1–5 descriptions ≤90. Search multimedia ads get a callToAction button (default LearnMore); Audience ads have none. videoIds attach videos from upload_microsoft_ads_video. Creating it Active in a live ad group needs confirm:true. Free.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
adGroupIdrequired | string | ad group to place the image ad in |
businessNamerequired | string | business name shown on the ad, up to 25 characters |
headlinesrequired | string[] | 1-15 headlines, up to 30 characters each |
longHeadlinesrequired | string[] | 1-5 long headlines, up to 90 characters each |
descriptionsrequired | string[] | 1-5 descriptions, up to 90 characters each |
finalUrlsrequired | string[] | landing page URL(s) the ad sends people to |
imageUrl | string | one picture, cut to both shapes for you |
images | object | pictures by shape: {landscape, square, logo} URLs, instead of imageUrl |
logoUrl | string | logo picture URL |
callToAction | string | Search multimedia ads only, e.g. LearnMore, ShopNow, SignUp |
videoIds | string[] | video ids from upload_microsoft_ads_video to attach |
videoSubType | "VideoMedia16X9" | "VideoMedia5X4" | "VideoMedia1X1" | "VideoMedia4X5" | "VideoMedia9X16" | aspect ratio of the attached video: 16x9, 5x4, 1x1, 4x5 or 9x16 |
status | "Active" | "Paused" | default Paused |
confirm | boolean | true required to create the ad Active in a live ad group |
upload_microsoft_ads_video#
Upload a video to Microsoft Advertising
Upload videos (a Hermoso render, or the user’s own clip: any direct-download https file or an upload_file URL) to Microsoft Advertising, or check ones already uploaded with videoIds: Draft (still processing), Active, or Failed with Microsoft’s reason. Microsoft enables video per account and refuses clips under 6 seconds. Use the ids in create_microsoft_ads_image_ad(videoIds). Free.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
urls | string[] | direct-download https URLs of videos to upload; clips under 6 seconds are refused |
videoIds | string[] | existing video ids to check the status of, instead of uploading |
description | string | description stored with the uploaded video |
list_microsoft_ads_audiences#
List Microsoft Advertising audiences (Customer Match lists) read-only
List a Microsoft Advertising account's audiences — by default the CUSTOMER LISTS (Customer Match), with each row's membership duration and its current Search and Audience-network sizes. TWO MICROSOFT CAVEATS THAT LOOK LIKE FAILURES AND ARE NOT: a size is nil or empty for UP TO 48 HOURS while a list is being built, and Microsoft will not use an audience of fewer than 300 people at all — so never report a fresh list's 0 as a failed upload. Pass types[] to ask for other audience types. Read-only, free, 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
audienceIds | string[] | up to 100 specific ids — omit for all of the requested type |
types | string[] | audience types, default CustomerList |
create_microsoft_ads_customer_list#
Create an empty Microsoft Advertising Customer Match list
Create an EMPTY Customer Match list on a Microsoft Advertising account, ready for apply_microsoft_ads_customer_list to fill. membershipDuration is 1–390 days, or -1 for no expiration (Microsoft's default is 30). scope 'Account' (the default) makes it usable only by that ad account; scope 'Customer' makes it usable by every account under the manager — Hermoso derives the right parent id for whichever you choose, because getting that pair wrong mis-parents the list silently. An audience is a definition: it cannot spend and it cannot serve on its own. Read back from Microsoft. 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
namerequired | string | maximum 128 characters |
description | string | maximum 1024 characters |
membershipDuration | number | 1–390 days, or -1 for no expiration. Microsoft’s default is 30 |
scope | "Account" | "Customer" | Account (default) = this ad account only; Customer = every account under the manager |
apply_microsoft_ads_customer_list#
Upload customer emails into a Microsoft Customer Match list destructive
Upload customer emails into a Microsoft Advertising Customer Match list so campaigns can target or exclude them. YOU PASS PLAIN EMAIL ADDRESSES — Hermoso normalizes and SHA-256 hashes them locally and sends ONLY the digests, so no plaintext ever leaves the server; values that arrive already hashed (64 hex characters) are passed through untouched. Microsoft's own normalization is applied exactly as published: trim, remove all dots from the user portion, remove any +alias, lowercase, then SHA-256. YOU MUST GET THE USER'S AGREEMENT FIRST: show them https://about.ads.microsoft.com/en-us/legal/customer-match-terms and only then call with acceptTerms:true, which Microsoft says 'eliminates the need to accept terms through the Microsoft Advertising UI' — without it nothing is uploaded and nothing is hashed. action is Add (additive, the default), Remove or Replace. Maximum 1000 items per call; page larger lists. Email is the only supported identifier — Microsoft publishes Phone and CRM as 'Not currently supported' and both are refused by name. DO NOT read a size of 0 afterwards as a failure: Microsoft leaves it empty for up to 48 hours while the audience builds. 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
audienceIdrequired | string | the customer list to write into |
emails | string[] | PLAIN email addresses — normalized and SHA-256 hashed inside Hermoso, never sent as plaintext. Already-hashed 64-hex values pass through untouched. Maximum 1000 per call |
action | "Add" | "Remove" | "Replace" | Add (default) is additive across calls |
subType | "Email" | Email is the only identifier Microsoft supports here — it publishes Phone and CRM as “Not currently supported” |
acceptTerms | boolean | REQUIRED true — the user must agree to Microsoft’s Customer Match terms first. Without it nothing is uploaded and nothing is hashed |
AppLovin Ads
list_applovin_ads_campaigns#
List AppLovin Ads campaigns read-only
The brand’s AppLovin Ads campaigns: id, name, LIVE/PAUSED, daily budget, goal, countries, landing page. Rename or edit one with update_applovin_ads_campaign; pause or go live with set_applovin_ads_status. Read-only, free. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to get it.
| Parameter | Type | Description |
|---|
campaignIds | string[] | only these ids (up to 100) |
page | number | from 1 |
size | number | 1–100, default 100 |
create_applovin_ads_campaign#
Create an AppLovin Ads campaign (paused) destructive
Create an AppLovin Ads campaign, read back PAUSED: AppLovin ignores status on create, so Hermoso pauses it straight after. A WEB campaign needs name, websiteUrl, countries, dailyBudget, goalType and goalValue (CPE also eventTarget; CHK_ROAS also roasDayTarget). APP campaigns add platform, packageName, trackingMethod, clickUrl, impressionUrl (iOS: itunesId). Every field is checked before AppLovin is called. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to get it.
| Parameter | Type | Description |
|---|
namerequired | string | campaign name |
type | "WEB" | "APP" | default: the connected account’s type |
websiteUrl | string | WEB: full landing page URL with tracking parameters |
countriesrequired | string[] | ISO codes, e.g. ["US","CA"] |
usRegions | string[] | US state codes, only with US |
usMetros | string[] | US metro names such as NEW_YORK; not with usRegions |
dailyBudget | number | account currency per day, all countries |
countryBudgets | object | per country instead, e.g. {"US":50} |
goalTyperequired | "CPE" | "CPP" | "CHK_ROAS" | "CPI" | "AD_ROAS" | "BLD_ROAS" | WEB: CPE, CPP, CHK_ROAS |
goalValue | number | target cost (CPE/CPP/CPI) or ROAS ratio (0.9 = 90%) |
countryGoals | object | per-country goal values keyed by ISO code, e.g. {US: 2.5} |
roasDayTarget | "DAY0" | "DAY7" | "DAY28" | WEB DAY0/DAY7; APP DAY7/DAY28 |
eventTarget | string | CPE: the event, e.g. GENERATE_LEAD |
audienceStrategy | "UNIVERSAL" | "PROSPECTING" | "DISCOVERY" | CPP / CHK_ROAS only |
startDate | string | YYYY-MM-DD, default tomorrow (UTC) |
endDate | string | end date, YYYY-MM-DD |
catalogId | string | product catalog id for DPA or DOA campaigns |
variantSetId | string | product variant set id within the catalog |
catalogType | "DPA" | "DOA" | product catalog type: DPA or DOA |
platform | "IOS" | "ANDROID" | app platform: IOS or ANDROID |
packageName | string | app package name, for app campaigns |
itunesId | number | iOS App Store numeric id, for iOS app campaigns |
trackingMethod | "ADJUST" | "APPSFLYER" | "APSALAR" | "BRANCH" | "KOCHAVA" | "TENJIN" | mobile measurement partner that tracks installs: ADJUST, APPSFLYER, APSALAR, BRANCH, KOCHAVA or TENJIN |
clickUrl | string | click-tracking URL from the measurement partner, for app campaigns |
impressionUrl | string | impression-tracking URL from the measurement partner, for app campaigns |
biddingStrategy | "AUTO_BIDDING_WITH_CPM_BILLING" | "TARGET_GOAL_WITH_CPI_BILLING" | automatic bidding billed per thousand impressions, or target goal billed per install |
update_applovin_ads_campaign#
Update an AppLovin Ads campaign destructive
Rename an AppLovin campaign or change its budget, goal value, countries, US states or metros, end date or landing page, read back. On a LIVE campaign a budget, goal, country or end-date change needs confirm:true. Status is set_applovin_ads_status; goal type, bidding and start date cannot change. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to get it.
| Parameter | Type | Description |
|---|
campaignIdrequired | string | AppLovin campaign id |
name | string | new campaign name |
dailyBudget | number | new daily budget in account currency, all countries |
countryBudgets | object | per-country daily budgets keyed by ISO code, e.g. {US: 50} |
goalValue | number | new target cost, or ROAS ratio (0.9 = 90%) |
countryGoals | object | per-country goal values keyed by ISO code |
countries | string[] | ISO country codes the campaign runs in |
usRegions | string[] | US state codes; only when US is a target country |
usMetros | string[] | US metro names such as NEW_YORK; not together with usRegions |
endDate | string | YYYY-MM-DD |
websiteUrl | string | landing page URL for a WEB campaign |
audienceStrategy | "UNIVERSAL" | "PROSPECTING" | "DISCOVERY" | audience strategy: UNIVERSAL, PROSPECTING or DISCOVERY |
clickUrl | string | click-tracking URL for app campaigns |
impressionUrl | string | impression-tracking URL for app campaigns |
confirm | boolean | required for a spend change on a LIVE campaign |
set_applovin_ads_status#
Turn an AppLovin campaign or creative set live or paused destructive
LIVE or PAUSED for an AppLovin campaign (campaignId) or creative set (creativeSetId), read back. LIVE starts real ad spend: show the user the budget, get an explicit yes, then call with confirm:true. Pausing is always safe. AppLovin’s API has no delete; pause instead. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to get it.
| Parameter | Type | Description |
|---|
level | "campaign" | "creative_set" | inferred from the id you pass |
campaignId | string | AppLovin campaign id; pass this or creativeSetId |
creativeSetId | string | AppLovin creative set id; pass this or campaignId |
statusrequired | "LIVE" | "PAUSED" | LIVE starts real spend; PAUSED stops delivery |
confirm | boolean | required true for LIVE |
list_applovin_ads_creative_sets#
List AppLovin creative sets read-only
AppLovin creative sets with their assets and review status, for one campaign (campaignId) or the whole account. Read-only, free. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to get it.
| Parameter | Type | Description |
|---|
campaignId | string | campaign whose creative sets to list; omit for the whole account |
creativeSetIds | string[] | only these creative set ids |
page | number | page number of the listing |
size | number | rows per page |
create_applovin_ads_creative_set#
Create an AppLovin creative set (paused) destructive
Create an AppLovin creative set from uploaded asset ids, born PAUSED, optionally inside a campaign. It must hold EITHER an HTML playable OR a portrait image (IMG_INTER_P) plus a video; Hermoso checks that against AppLovin’s own asset types before creating it. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to get it.
| Parameter | Type | Description |
|---|
namerequired | string | creative set name |
assetIdsrequired | string[] | AppLovin asset ids |
campaignId | string | campaign to place the creative set in; optional |
countries | string[] | ISO country codes for the creative set |
languages | string[] | e.g. ENGLISH |
url | string | landing page for this set only (WEB) |
brandInfo | object | override the account brand on THIS creative set — how a brand A/B tests the end card |
useAccountBrandInfo | boolean | true = go back to the account-level brand info |
remove_applovin_ads_creative_set#
Remove an AppLovin creative set from its campaigns destructive
Take an AppLovin creative set out of its campaigns (all of them, or campaignIds). AppLovin’s API has no delete or archive, so this is its only removal: the set and its assets stay on the account and can be added back. Call it WITHOUT confirm FIRST: nothing changes and you get the campaigns it is in; show the user, then call with confirm:true, plus confirmName (its exact name) and confirmChildren when the reply asks for them. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to get it.
| Parameter | Type | Description |
|---|
creativeSetIdrequired | string | AppLovin creative set to take out of its campaigns |
campaignIds | string[] | only these campaigns (up to 20); omit for all |
confirm | boolean | true to remove, after a first call without it has shown the campaigns |
confirmName | string | its exact name, when asked |
confirmChildren | number | the campaign count the unconfirmed call reported |
update_applovin_ads_creative_set#
Edit an AppLovin creative set destructive
Edit an AppLovin creative set, read back: rename it, change its countries or languages (an empty list means all), its landing page (url; "" falls back to the campaign’s), replace its assets (assetIds, checked against AppLovin’s own asset types first), or add it to more campaigns (addToCampaignIds, up to 20). Adding a LIVE set to a LIVE campaign needs confirm:true. Status is set_applovin_ads_status. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to get it.
| Parameter | Type | Description |
|---|
creativeSetIdrequired | string | AppLovin creative set id to edit |
name | string | new creative set name |
countries | string[] | ISO codes; [] = all countries |
languages | string[] | e.g. ENGLISH; [] = all |
url | string | landing page for this set; "" = the campaign’s |
assetIds | string[] | REPLACES the set’s assets |
addToCampaignIds | string[] | also run it in these campaigns (up to 20) |
brandInfo | object | override the account brand on THIS creative set — how a brand A/B tests the end card |
useAccountBrandInfo | boolean | true = go back to the account-level brand info |
confirm | boolean | required to add a LIVE set to a LIVE campaign |
clone_applovin_ads_creative_set#
Copy an AppLovin creative set into another campaign (paused) destructive
Copy an AppLovin creative set into another campaign, born PAUSED and read back. AppLovin only clones a set that already belongs to a campaign. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to get it.
| Parameter | Type | Description |
|---|
creativeSetIdrequired | string | creative set to copy; it must already belong to a campaign |
campaignIdrequired | string | the campaign to copy it into |
applovin_ads_change_history#
AppLovin campaign change history read-only
Who changed what on the brand’s AppLovin campaigns, and when: creation, edits (field by field, before and after), status changes and ARCHIVING — including archived campaigns that list_applovin_ads_campaigns no longer returns. One campaign (campaignId) or the whole account; since/until must fall within the latest 30 UTC days and go together (neither = the last 7 days). Read-only, free. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to get it.
| Parameter | Type | Description |
|---|
campaignId | string | ONE campaign id; omit for every campaign |
since | string | YYYY-MM-DD UTC, with until |
until | string | YYYY-MM-DD UTC, with since |
page | number | from 1 |
size | number | 1–100, default 100 |
list_applovin_ads_catalogs#
List AppLovin catalogs read-only
The AppLovin account’s product catalogs and their variant sets: the catalogId and variantSetId a dynamic-ads campaign (create_applovin_ads_campaign) needs. Website accounts. Read-only, free. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to get it.
No parameters.
list_applovin_ads_assets#
List AppLovin assets read-only
Assets on the AppLovin account: id, type (VID_LONG_P, VID_SHORT_P, IMG_INTER_P, IMG_BANNER, HOSTED_HTML), review status and rejection reasons. Read-only, free. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to get it.
| Parameter | Type | Description |
|---|
assetIds | string[] | only these asset ids |
resourceType | "image" | "video" | "html" | asset kind to list: image, video or html |
page | number | page number of the listing |
size | number | rows per page |
upload_applovin_ads_assets#
Upload assets to AppLovin
Upload videos, images or an HTML playable to AppLovin from Hermoso render or Library urls or public files (mp4, mov, jpg, png, gif, html; up to 40). Returns AppLovin asset ids once processed; pass uploadId to check a pending upload again. New assets go through AppLovin review. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to get it.
| Parameter | Type | Description |
|---|
urls | string[] | files to upload; a local file goes through upload_file first |
uploadId | string | re-check an earlier upload instead |
applovin_ads_report#
AppLovin Ads performance report read-only
AppLovin Ads performance: impressions, clicks, CTR, spend, sales, ROAS and checkout revenue by day, campaign, creative set or country. Works with the Reporting Key alone, which every AppLovin account has. Web reports reach back 90 days, app 45; UTC. Read-only, free.
| Parameter | Type | Description |
|---|
since | string | YYYY-MM-DD, default 7 days ago |
until | string | YYYY-MM-DD, default today |
columns | string[] | e.g. day, campaign, campaign_id_external, creative_set, country, impressions, clicks, ctr, cost, sales, roas_7d, chka_usd_7d |
attribution | "click" | "click_and_view" | web only, default click |
filters | object | column -> values |
cohort | boolean | attribute to impression day |
notZero | boolean | drop rows whose metrics are all zero |
having | string | server-side numeric filter, e.g. "cost > 0 AND roas_7d < 1": report columns, numbers, > < >= <= = !=, AND, OR, parentheses (AppLovin warns it slows the report) |
limit | number | 1–10000, default 1000 |
offset | number | rows to skip, for paging past the limit |
applovin_ads_pixel#
AppLovin pixel install snippet read-only
The AppLovin pixel for the brand’s website: AppLovin’s own install snippet with the account’s Event Key filled in, where it goes (every page’s <head>, Google Tag Manager, or the AppLovin Shopify app), the events to fire for a store or a lead-gen site with copy-ready examples, dedupe ids, and how to validate it. The Event Key is public by design. Calls nothing at AppLovin. Free.
| Parameter | Type | Description |
|---|
eventKey | string | the AppLovin Event Key; omit to use the one saved on the connection |
siteType | "purchase" | "lead" | default purchase |
platform | "html" | "gtm" | "shopify" | where it is installed, default html |
send_applovin_ads_events#
Send conversion events to AppLovin destructive
Send website conversion events to AppLovin from the server (its Conversion API): purchase, generate_lead, add_to_cart and the rest, up to 100 per batch, each with the visitor’s public IP, user agent and one identifier. Email and phone are hashed with AppLovin’s own normalisation before sending; a purchase dedupes on its transaction id. Needs the Conversion API key, which AppLovin enables per account on request; without it the answer says how to get it. Free.
| Parameter | Type | Description |
|---|
eventsrequired | object[] | 1–100 events; one invalid event makes AppLovin drop the whole batch, so all are checked first |
MICROSOFT AD INSIGHT RECOMMENDATIONS + AUTO-APPLY
microsoft_ads_recommendations#
What Microsoft suggests changing on the account read-only
What Microsoft Advertising ITSELF suggests changing on the account: budget raises, new keywords, broad-match widenings, conflicting negative keywords to remove, and responsive search ads or extra headlines Microsoft has written. Each row carries the campaign and ad group it touches plus Microsoft’s OWN estimate of what applying it would add to clicks, impressions, conversions and COST; a budget recommendation states the current and the recommended daily amount, so the money is a vendor fact rather than a guess. SAY THIS WHEN REPORTING: every Microsoft recommendation INCREASES what the account buys, that is what they are for, so none of them is a free win, and an empty list means Microsoft has no ADVICE (usually too little delivery to compute any), never that the account is optimal. Dismissed recommendations are hidden unless you ask for them. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
types | string[] | narrow to some of ADD_BROAD_MATCH_KEYWORD, CAMPAIGN_BUDGET, KEYWORD, REMOVE_CONFLICTING_NEGATIVE_KEYWORD, RESPONSIVE_SEARCH_AD, RESPONSIVE_SEARCH_AD_ASSET, all six by default. An unknown name is refused BY NAME, because Microsoft answers one with a deserialization error that names no field |
limit | number | Microsoft’s MaxCount, omit for everything |
includeDismissed | boolean | also show recommendations already dismissed |
apply_microsoft_ads_recommendations#
Apply Microsoft recommendations (spend-gated) destructive
Apply Microsoft Advertising recommendations by id. THIS SPENDS REAL MONEY AND HAS NO UNDO. It raises daily budgets, adds keywords, widens keywords to broad match, DELETES negative keywords so the account buys more searches, and publishes Microsoft-written ads under the brand’s name, all live at the next auction. THE GATE IS UNLIKE THE OTHER MICROSOFT SPEND SWITCHES, AND confirm:true ALONE IS REFUSED: it proves you meant to apply something, not that you know what THESE ones do, and it reads identically for one keyword suggestion and forty budget raises. So call it ONCE with no confirmation. Nothing is applied, and you get every recommendation named with its campaign, exactly what it changes, the current -> recommended budget where there is one, and Microsoft’s own cost estimate. SHOW THAT LIST TO THE USER, get an explicit yes, then call again with confirm:true, confirmCount:<n> and confirmCostIncrease:<n>. BOTH NUMBERS ARE RECOMPUTED FROM A FRESH READ at that moment, so a recommendation Microsoft has withdrawn, already applied or re-priced since you looked fails the check instead of being applied unseen. If ANY id you name is no longer on offer, NOTHING is applied, not even the ones that still are, because applying part of a set you inspected whole is not what you asked for. Microsoft can reject individual items and apply the rest, so the result reports per item: never call a partial apply a failure, and never call it a success. 0 credits to us; the spend lands on the ad account.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
recommendationIdsrequired | string[] | ids from microsoft_ads_recommendations, max 100. There is deliberately no "apply everything" |
confirm | boolean | the user has SEEN the pre-flight list and said yes. Not sufficient on its own, both echo numbers below are also required |
confirmCount | number | echo back how many recommendations this applies: the exact count the un-confirmed call reported |
confirmCostIncrease | number | echo back Microsoft’s own summed cost estimate for them: the exact number the un-confirmed call reported |
dryRun | boolean | report what would happen and apply nothing at all |
dismiss_microsoft_ads_recommendations#
Dismiss Microsoft recommendations
Dismiss Microsoft Advertising recommendations by id so Microsoft stops offering them. IT CANNOT SPEND and it changes nothing about what the account runs (it takes advice off the list), so it takes no confirmation at all, which is the point: reach for this rather than applying something just to clear it. Nothing is destroyed either: Microsoft still returns a dismissed recommendation from a read, flagged as dismissed, and microsoft_ads_recommendations will show it again with includeDismissed. Ids Microsoft has already withdrawn are reported and are not an error, they are already off the list, which is the outcome you wanted. 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
recommendationIdsrequired | string[] | ids from microsoft_ads_recommendations, max 100 |
microsoft_ads_auto_apply#
Is Microsoft changing this ad account by itself? read-only
Whether Microsoft is allowed to change this ad account BY ITSELF, unattended: the auto-apply opt-in status for each of the five recommendation types Microsoft supports. THIS IS THE READ THAT ANSWERS "is something changing my account when nobody is looking?", and it is worth running on any Microsoft account you inherit: an account can already be opted in without anyone at the brand having done it, and there is no other way to find out from here. A type Microsoft returns no status for is reported as UNKNOWN, never as off. TWO THINGS THAT CATCH PEOPLE OUT AND ARE STATED IN THE REPLY: these five type names are a DIFFERENT, case-sensitive vocabulary from the six microsoft_ads_recommendations uses (Microsoft cannot auto-apply a budget recommendation at all, there is no such type), and three of the five have no readable recommendation, so there is no way to preview what auto-apply would do for those. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
types | string[] | narrow to some of ResponsiveSearchAdsOpportunity, MultiMediaAdsOpportunity, RemoveConflictingNegativeKeywordOpportunity, FixConversionGoalSettingsOpportunity, CreateConversionGoalOpportunity, all five by default |
set_microsoft_ads_auto_apply#
Let Microsoft change the account by itself, or stop it (per type) destructive
Switch Microsoft’s auto-apply on or off, per recommendation type. SWITCHING ONE ON IS UNLIKE EVERY OTHER WRITE IN HERMOSO: it is STANDING PERMISSION FOR MICROSOFT TO CHANGE THIS AD ACCOUNT ON ITS OWN, indefinitely, with nobody watching, publishing search and multimedia ads it wrote under the brand’s name, DELETING negative keywords so the account starts buying searches it currently blocks, or creating and changing conversion goals so every smart-bidding strategy on the account re-aims. It has no expiry and no schedule you can inspect, and nothing in Hermoso will ever turn it off for the user. AND THERE IS NOTHING TO PREVIEW: Microsoft decides later, on evidence that does not exist yet, so no count, no dry run and no list can tell you what it will do, which is exactly why a blast-radius echo would be meaningless here and each type has to be named instead. To turn any type ON: confirm:true AND confirmTypes listing every type being turned on, individually. There is no "all" and no default; naming four of five leaves the fifth refused BY NAME. TURNING IT OFF IS NEVER GATED, set those types to false and it goes straight through, because that only ever reduces what Microsoft does unattended. A type already in the state you asked for is not sent at all. THE ANSWER IS THE READ-BACK: the result re-reads Microsoft and says done / not done / could not tell PER TYPE, and you report that, never the fact that the call returned. 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
optInsrequired | object | map of Microsoft auto-apply type name to true/false, e.g. {"RemoveConflictingNegativeKeywordOpportunity": false}. Case-sensitive keys: ResponsiveSearchAdsOpportunity, MultiMediaAdsOpportunity, RemoveConflictingNegativeKeywordOpportunity, FixConversionGoalSettingsOpportunity, CreateConversionGoalOpportunity |
confirm | boolean | required to turn ANY type ON, after the user has been told what Microsoft will be allowed to do unattended. Never needed to turn one off |
confirmTypes | string[] | every type you are turning ON, named individually. That is the echo which proves you know WHICH permission you are granting, where a count could not |
get_microsoft_ads_account_properties#
Read Microsoft Advertising account settings read-only
Read a Microsoft Advertising ad account’s account-level settings (Microsoft’s GetAccountProperties): TrackingUrlTemplate, FinalUrlSuffix, MSCLKIDAutoTaggingEnabled, AdClickParallelTracking, IncludeViewThroughConversions, ProfileExpansionEnabled, AllowImageAutoRetrieve, BusinessAttributes, AssetAIEnhancementOptout, AIDetectedContentWatermarkPolicy and the rest Microsoft returns, each flagged settable or read-only here. A property Microsoft does not return is reported as UNREAD (often a feature that has not reached the account yet), never as off or empty. AIDetectedContentWatermarkPolicy is the account-level AI-detected content watermark policy; Microsoft has not documented its values, so report what the account stores verbatim and do not guess what it means. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
names | string[] | narrow to some account property names, e.g. ["AIDetectedContentWatermarkPolicy"]; all of them by default |
set_microsoft_ads_account_properties#
Change Microsoft Advertising account settings
Change a Microsoft Advertising ad account’s account-level settings (Microsoft’s SetAccountProperties). They apply to EVERY campaign in the account. Settable: TrackingUrlTemplate and FinalUrlSuffix ("" clears them), MSCLKIDAutoTaggingEnabled, AdClickParallelTracking (true only, Microsoft made it mandatory), IncludeViewThroughConversions, ProfileExpansionEnabled, AllowImageAutoRetrieve, BusinessAttributes (an array or a semicolon list of Microsoft’s attribute names, "" clears), AssetAIEnhancementOptout and AIDetectedContentWatermarkPolicy. Readable but refused here, by name: AutoApplyRecommendations (use set_microsoft_ads_auto_apply), the Nielsen and Netflix terms acceptances (a person accepts terms, not an agent), and properties whose values Microsoft does not document. confirm:true is required to turn ProfileExpansionEnabled ON (it widens reach and can raise spend), to turn AllowImageAutoRetrieve ON (it grants Microsoft a licence to your site images), and for ANY AIDetectedContentWatermarkPolicy value: Microsoft has not documented those values, so read the property first, tell the user that, and pass the value verbatim. Microsoft sets all or none. A value already in place is not sent. THE ANSWER IS THE READ-BACK: per property done / not done / could not tell, with the previous value and the call that restores it. Report that, never the fact that the call returned. 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Microsoft ad account id — omit to use the single shared account |
accountId | string | older name for adAccountId (MCP only) |
propertiesrequired | object | map of account property name to value, e.g. {"MSCLKIDAutoTaggingEnabled": true}, {"FinalUrlSuffix": "utm_source=bing"} or {"AIDetectedContentWatermarkPolicy": "<value read from get_microsoft_ads_account_properties>"} |
confirm | boolean | the user said yes. Required for ProfileExpansionEnabled=true, AllowImageAutoRetrieve=true and any AIDetectedContentWatermarkPolicy value |
APPLE ADS: BUILDING, NOT JUST READING
create_apple_ads_campaign#
Create an Apple Ads campaign destructive
Create an Apple Ads (Apple Search Ads) campaign promoting an iOS app on the App Store. CREATED PAUSED ALWAYS — there is no override, so nothing can spend until you separately call set_apple_ads_status(status:"ENABLED", confirm:true). REQUIRED: name (unique across the whole Apple Ads ORGANIZATION, not just this ad account — Apple 400s DUPLICATE_CAMPAIGN_NAME, and the org can contain campaigns you cannot see), appAdamId (the App Store id, e.g. "6752439949" — read it off any existing campaign’s promotedObjectId), dailyBudget (decimal string, e.g. "25.00"), countries (ISO 3166-1 alpha-2). placements defaults to APPSTORE_SEARCH_RESULTS, the classic Search Ads placement, which needs NO creative — Apple renders your App Store product page. bidStrategyType defaults to MANUAL_CPT (a fixed price per tap); MAX_CONVERSIONS optimizes for installs and is the only strategy that takes a campaign-level bid, where it means the target CPA. A MANUAL_CPT per-tap bid does NOT belong here: Apple refuses it at campaign level with an error naming a field you never sent — pass defaultBid to create_apple_ads_ad_group instead. Free.
| Parameter | Type | Description |
|---|
namerequired | string | REQUIRED. Unique across the entire Apple Ads organization. |
appAdamIdrequired | string | REQUIRED. App Store id of the promoted app, e.g. "6752439949". |
dailyBudgetrequired | string | REQUIRED. Decimal string, e.g. "25.00". |
countriesrequired | string[] | REQUIRED. ISO 3166-1 alpha-2 codes, e.g. ["US","CA"]. |
placements | string[] | Default ["APPSTORE_SEARCH_RESULTS"]. Also APPSTORE_SEARCH_TAB, APPSTORE_TODAY_TAB, APPSTORE_PRODUCT_PAGES, or MAPS_SEARCH_RESULTS / MAPS_SEARCH_HOME. One campaign cannot mix App Store and Maps placements. |
bidStrategyType | "MANUAL_CPT" | "MAX_CONVERSIONS" | Default MANUAL_CPT. |
bid | string | MAX_CONVERSIONS only — the target CPA. Refused on MANUAL_CPT. |
currency | string | Defaults to the connected ad account’s currency; an account cannot mix currencies. |
startTime | string | campaign start time, ISO 8601 |
endTime | string | Omit to run indefinitely. |
budgetOrderIds | string[] | budget order id(s) from list_apple_ads_budget_orders to assign this campaign to — a budget order is a spend CEILING SHARED across every campaign assigned to it, and it works ALONGSIDE dailyBudget rather than instead of it. Without this a budget order can never be drawn from. |
create_apple_ads_ad_group#
Create an Apple Ads ad group destructive
Create an ad group inside an Apple Ads campaign — this is where the bid and the keywords live. CREATED PAUSED ALWAYS. REQUIRED: campaignId, name. defaultBid is the most you will pay per tap (decimal string, e.g. "1.50") and applies to every keyword with no bid of its own. startTime is optional here and defaults to now: Apple documents it as optional and then REJECTS the create without it, so Hermoso always supplies one. searchMatch:true opts into Apple’s automated keyword matching, which targets relevant search terms beyond your keyword list. pricingModel defaults to CPT and must match the campaign’s billing event. Keywords cannot be created inline — make the ad group, then call add_apple_ads_keywords. targeting is who and where it reaches: FOR AN APPLE MAPS AD GROUP, targeting.locationGroupIds is the only field that says WHICH of the brand’s shops this ad group promotes, and an ad group with none stops serving rather than reaching everywhere — build the group first with create_apple_ads_location_group. Radius and geo targeting cannot be combined in one ad group and that is refused here rather than delivered on terms nobody chose. Like the campaign it lives in, it stays PAUSED until set_apple_ads_status(status:"ENABLED", confirm:true) arms it. Free.
| Parameter | Type | Description |
|---|
campaignIdrequired | string | REQUIRED. |
namerequired | string | REQUIRED. |
defaultBid | string | Max cost per tap, e.g. "1.50". |
currency | string | ISO 4217 currency code of the ad group bids |
startTime | string | Defaults to now. |
endTime | string | Omit to inherit the campaign end date. |
searchMatch | boolean | Opt into Apple’s automated keyword matching. |
pricingModel | "CPT" | "CPM" | "CPA" | Default CPT; must match the campaign billing event. |
targeting | object | Who and where this ad group reaches. For Apple Maps, locationGroupIds is what says which shops it promotes. |
add_apple_ads_keywords#
Add Apple Ads keywords
Add targeting keywords to an Apple Ads ad group. keywords is [{text, matchType, bid}] — matchType EXACT (default), BROAD, PHRASE or CATEGORY, and bid optionally overrides the ad group default for that one term. Up to 1000 per call and 5000 per ad group. They are sent ONE AT A TIME on purpose: a single term Apple refuses cannot take the rest of the batch down, and the reply names every refusal with Apple’s own reason, then reads the ad group’s keywords back to prove what actually landed. Keyword text and match type are IMMUTABLE — to change either, delete the keyword and add it again. A keyword is created ENABLED because it cannot serve unless its ad group and campaign are enabled too; if the ad group is ALREADY live, adding keywords serves on the next auction and therefore takes confirm:true. Free.
| Parameter | Type | Description |
|---|
adGroupIdrequired | string | REQUIRED. |
keywordsrequired | object[] | REQUIRED. Up to 1000. |
matchType | "EXACT" | "BROAD" | "PHRASE" | "CATEGORY" | Default for entries that do not set one. |
currency | string | ISO 4217 currency code for the bids |
confirm | boolean | Required ONLY when the ad group is already ENABLED. |
add_apple_ads_negative_keywords#
Add Apple Ads negative keywords
Add negative keywords to an Apple Ads campaign or ad group — search terms your ads must NOT show for. Pass exactly one of campaignId (campaign-wide) or adGroupId (that ad group only); Apple rejects a request carrying both. keywords is [{text, matchType}] with matchType BROAD (default) or EXACT. Never confirm-gated and never a spend risk, because a negative keyword only ever RESTRICTS where you show. Text and match type are immutable — delete and re-add to change either. Free.
| Parameter | Type | Description |
|---|
campaignId | string | Campaign-wide. Pass this OR adGroupId, never both. |
adGroupId | string | One ad group. Pass this OR campaignId, never both. |
keywordsrequired | object[] | REQUIRED. |
matchType | "EXACT" | "BROAD" | "PHRASE" | "CATEGORY" | Default BROAD. |
update_apple_ads_object#
Edit an Apple Ads campaign, ad group, keyword or location group destructive
Edit an existing Apple Ads campaign, ad group, keyword, creative or ad. Pass level and id plus ONLY the fields you want to change — Apple leaves every field you omit exactly as it is. Campaign: name, dailyBudget, startTime, endTime, countries, bidStrategyType. Ad group: name, defaultBid, startTime, endTime, searchMatch. Keyword: bid only — its text and match type are immutable, so delete and re-add to change either. Creative: name, plus (Apple Maps creatives only) a new creativeSpec — a creative’s type and destination are locked at creation, so pointing at a different app or product page means creating a NEW creative. Ad: name only — its creative and ad group are locked too, and Apple’s own instruction for serving a different creative is to create a new ad and delete the old one. Location group: name, description, and locationIds or rules. WARNING, AND IT APPLIES TO EVERY ARRAY HERE: AN ARRAY REPLACES RATHER THAN MERGES. The countries you send become the campaign’s entire geographic targeting, the targeting dimensions you send become the ad group’s entire setting for those dimensions, and Apple states it for location groups in terms — to add ONE shop to a group, read its current locationIds, append, and send the whole list back, or you have just removed every other shop. Send everything you want to keep, never just the new one. This tool deliberately CANNOT change a status: enabling arms real spend, so it lives behind set_apple_ads_status and its confirmation. Free.
| Parameter | Type | Description |
|---|
levelrequired | "campaign" | "adgroup" | "keyword" | "creative" | "ad" | "location_group" | REQUIRED. |
idrequired | string | REQUIRED. |
name | string | new name for the campaign, ad group, creative, ad or location group |
dailyBudget | string | Campaign only. |
defaultBid | string | Ad group only. |
bid | string | Keyword only. |
countries | string[] | Campaign only. REPLACES the whole list. |
bidStrategyType | "MANUAL_CPT" | "MAX_CONVERSIONS" | Campaign only. |
searchMatch | boolean | Ad group only. |
targeting | object | Ad group only. Same shape as create_apple_ads_ad_group.targeting — locationGroupIds, radius, adminAreaIds, daypart and the rest. REPLACES the dimensions you name. |
locationIds | string[] | Location group only (STATIC). REPLACES the whole membership. |
rules | object[] | Location group only (DYNAMIC). REPLACES every rule and puts the group back to PENDING. |
description | string | Location group only. |
startTime | string | start time, ISO 8601 |
endTime | string | end time, ISO 8601 |
currency | string | ISO 4217 currency code |
budgetOrderIds | string[] | budget order id(s) from list_apple_ads_budget_orders to assign this campaign to — a budget order is a spend CEILING SHARED across every campaign assigned to it, and it works ALONGSIDE dailyBudget rather than instead of it. Without this a budget order can never be drawn from. |
set_apple_ads_status#
Pause or enable an Apple Ads object destructive
Pause or enable an Apple Ads campaign, ad group, keyword or ad. PAUSING is immediate and needs no confirmation. ENABLING IS THE ONE SWITCH THAT ARMS REAL MONEY: it requires confirm:true, and without it nothing changes and the refusal names the object Apple actually holds under that id — read back from Apple, never echoed from your input, because aiming at the wrong campaign is invisible until money moves. An object serves only when it AND every parent above it are ENABLED, so enabling a keyword inside a paused campaign spends nothing. Prefer pausing to deleting: pausing is reversible and Apple’s delete is not. A CREATIVE, AN ASSET AND A LOCATION GROUP ARE DELIBERATELY ABSENT from the levels here and that is not an oversight: none of them has an advertiser status — a creative and a location group carry only a read-only systemStatus Apple computes — so to stop a creative serving, pause the ADS that reference it, and to stop promoting a set of shops, pause the AD GROUP that targets the location group. The reply reports the status Apple STORED, which is a different claim from the one it accepted. Free.
| Parameter | Type | Description |
|---|
levelrequired | "campaign" | "adgroup" | "keyword" | "negative_keyword" | "ad" | REQUIRED. |
idrequired | string | REQUIRED. |
statusrequired | "ENABLED" | "PAUSED" | REQUIRED. |
confirm | boolean | REQUIRED to ENABLE — it arms real spend. Pausing needs none. |
delete_apple_ads_object#
Delete an Apple Ads object destructive
Delete an Apple Ads campaign, ad group, keyword, negative keyword, ad, creative, asset, location group or budget order. Requires confirm:true, and optionally confirmName echoed back to prove you aimed at the right object. THIS CASCADES AND CANNOT BE UNDONE: Apple soft-deletes with no undelete, and deleting a campaign takes every ad group, keyword and ad underneath it. THE BLAST RADIUS DIFFERS BY LEVEL and the refusal states the right one for the object you named: deleting a CREATIVE does not delete the ads that use it — it makes every one of them stop serving, permanently, with no way back but a new creative; deleting an ASSET makes any Apple Maps creative using it INVALID; deleting an AD leaves its creative untouched and available; deleting a LOCATION GROUP is permanent with no restore, and every ad group targeting it loses that constraint immediately — an ad group left targeting no location group STOPS serving rather than reaching everywhere. In almost every case set_apple_ads_status(status:"PAUSED") is what you actually want — it stops all spend and is reversible — but a creative, an asset, a location group and a budget order have NO status, so for those the reversible move is pausing whatever references them. DELETING A BUDGET ORDER CASCADES TO NOTHING — it is a spend ceiling, not a parent — but every campaign assigned to it loses that ceiling, and Apple REFUSES the delete outright while any campaign is still assigned, or once the order has started, or if it is cancelled, completed or exhausted; the refusal names whichever applies before anything is sent. The reply is confirmed by RE-READING the object: for a delete, an absent or deleted-flagged row is the proof, never the HTTP 200. Free.
| Parameter | Type | Description |
|---|
levelrequired | "campaign" | "adgroup" | "keyword" | "negative_keyword" | "ad" | "creative" | "asset" | "location_group" | "budget_order" | REQUIRED. |
idrequired | string | REQUIRED. |
confirm | boolean | REQUIRED. Nothing is deleted without it. |
confirmName | string | Optional — echo the object’s exact name to prove you aimed at the right one. |
SUGGESTIONS · RECOMMENDATIONS · INSIGHTS · CHANGE HISTORY ·…
apple_ads_keyword_suggestions#
Apple Ads keyword, phrase and category suggestions read-only
Apple’s OWN search data for an app you advertise — the fastest way to build a real Apple Search Ads keyword list instead of guessing. THE THREE KINDS DO NOT SHARE A FILTER VOCABULARY and Apple 400s any field that is not an attribute of the one you query, so each takes different parameters. kind:"keyword" (the default) returns suggested keyword strings and accepts `terms` + `countriesOrRegions` — it has NO queryType, and passing one is refused. kind:"phrase" returns longer search phrases and REQUIRES `phrases` (the phrases to look up); kind:"category" returns App Store categories and REQUIRES `categories`. Neither accepts `terms`. POPULARITY IS RELATIVE, NOT A VOLUME: it ranks terms against each other and cannot be read as searches per month. queryType defaults to "SEARCH", which looks up phrases/categories you name; "SUGGESTION" asks Apple to discover new ones and IS CURRENTLY BROKEN AT APPLE — measured 2026-08-17, it hangs ~34s and returns a server error for a request identical to Apple’s own published example, so retrying will not clear it. REQUIRED: promotedObjectId — the app’s adamId (search_apple_ads_apps with no arguments lists the apps this organization owns) or an Apple Maps brandId. An empty result is about the APP, not the account. Feed the winners straight into add_apple_ads_keywords or bulk_apple_ads_keywords. Read-only, free, spends nothing.
| Parameter | Type | Description |
|---|
kind | "keyword" | "phrase" | "category" | Default keyword. Each kind takes DIFFERENT filters — see the description. |
promotedObjectIdrequired | string | REQUIRED — the app’s adamId, or an Apple Maps brandId. |
promotedObjectType | "APPSTORE_APP" | "BUSINESS_BRAND" | Default APPSTORE_APP. |
terms | string[] | kind:"keyword" ONLY. Seed search terms to expand from, e.g. ["productivity","task management"]. Refused on phrase/category. |
phrases | string[] | kind:"phrase" ONLY, and REQUIRED there on the SEARCH route — the phrases to look up, e.g. ["best productivity app"]. |
categories | string[] | kind:"category" ONLY, and REQUIRED there on the SEARCH route — the App Store categories to look up, e.g. ["Utilities"]. |
countriesOrRegions | string[] | ISO 3166-1 alpha-2, e.g. ["US","GB"]. |
queryType | "SUGGESTION" | "SEARCH" | phrase/category ONLY (refused on keyword). Default SEARCH. SUGGESTION is measured BROKEN at Apple — ~34s then a server error. |
limit | number | maximum number of suggestions to return |
offset | number | suggestions to skip, for paging |
apple_ads_target_cpa_suggestion#
Apple Ads suggested target CPA read-only
What Apple thinks an install should cost you. Returns the suggested target CPA for an app — the MAXIMUM tap-install CPI Apple observed across that app’s eligible markets over the last 28 days, together with the market that produced it. Only markets with at least 10 installs in that window qualify, so a brand-new or low-volume app legitimately has no suggestion yet and that is an answer, not a failure. This is the number to put on a MAX_CONVERSIONS campaign, where the campaign-level bid IS the target CPA — it is NOT a per-tap bid and does not belong on a MANUAL_CPT campaign, which refuses a campaign-level bid outright. Scope it with countriesOrRegions to ask about specific markets. Read-only, free, spends nothing.
| Parameter | Type | Description |
|---|
promotedObjectIdrequired | string | REQUIRED — the app’s adamId. |
promotedObjectType | "APPSTORE_APP" | "BUSINESS_BRAND" | Default APPSTORE_APP. |
countriesOrRegions | string[] | ISO 3166-1 alpha-2 markets to consider, e.g. ["US","GB","CA"]. |
list_apple_ads_recommendations#
List Apple Ads recommendations read-only
Apple’s own optimisation engine for this account: campaigns it thinks are budget-capped, and campaigns whose target CPA it thinks should move. type:"daily_budget" returns DAILYCAP recommendations, each carrying the current and suggested budget plus the historical installs/taps/spend behind it AND Apple’s projection if you raise it; type:"target_cpa" returns TCPA recommendations, which Apple produces ONLY for campaigns on a Maximize Conversions bid strategy — a MANUAL_CPT campaign will never have one. Defaults to state AVAILABLE because that is the only actionable state: APPLIED and DISMISSED are terminal and can never be re-actioned (pass state:"ALL" to see the history). Apple names only two recommendation categories with endpoints; its enum also mentions keyword and bid categories but this API version publishes no way to query or act on those. Read-only, free — reading changes nothing.
| Parameter | Type | Description |
|---|
type | "daily_budget" | "target_cpa" | Default daily_budget. |
promotedObjectIdrequired | string | REQUIRED — the app’s adamId, or an Apple Maps brandId. |
promotedObjectType | "APPSTORE_APP" | "BUSINESS_BRAND" | Default APPSTORE_APP. |
state | string | AVAILABLE (default), APPLIED, DISMISSED, DELETE, or "ALL" to skip the filter. |
limit | number | maximum number of recommendations to return |
offset | number | recommendations to skip, for paging |
apply_apple_ads_recommendation#
Apply an Apple Ads recommendation destructive
THIS CHANGES WHAT A LIVE CAMPAIGN MAY SPEND, and it is the only tool in the Apple Ads suggestion/recommendation family that touches money. Applying a daily-budget recommendation raises that campaign’s daily budget to Apple’s suggested amount; applying a target-CPA recommendation moves the CPA its auto-bidder chases. REQUIRES confirm:true, and the refusal you get without it is built from the recommendation READ BACK FROM APPLE — it names the campaign, states the daily budget current -> recommended (Apple keeps no current target CPA anywhere readable, so a target-CPA refusal states the recommended target against the campaign’s average CPA so far), and quotes Apple’s own projected spend over the next seven days — so you are confirming a specific change to a specific campaign rather than confirming that you pressed a button. Apple publishes no dry-run for this call; that refusal IS the preview, and it dispatches nothing. An id Hermoso cannot read back from Apple right now (unknown, expired, another app, or Apple not answering) is refused even WITH confirm:true. Pass amount to apply a DIFFERENT number than Apple suggested; omit it to take Apple’s. The campaign’s STATUS IS NOT TOUCHED: this moves a ceiling, it never enables or pauses anything, and it does not make a paused campaign start spending. Only an AVAILABLE recommendation can be applied and applying is TERMINAL. APPLIED is reported only from READ-BACKS, never from Apple’s acknowledgement: a daily budget only when the re-read CAMPAIGN carries the new budget; a target CPA (which Apple stores on no campaign) only when the re-read campaign exists and the recommendation re-queries as APPLIED. Anything less comes back NOT CONFIRMED and says which read failed. Free to call; the spending it enables is real.
| Parameter | Type | Description |
|---|
type | "daily_budget" | "target_cpa" | Default daily_budget. |
idrequired | string | REQUIRED — the recommendation id from list_apple_ads_recommendations. |
promotedObjectIdrequired | string | REQUIRED — the same app or brand id the recommendation was queried for. |
promotedObjectType | "APPSTORE_APP" | "BUSINESS_BRAND" | Default APPSTORE_APP. |
amount | string | Override Apple’s suggestion with your own decimal amount, e.g. "500.00". Omit to apply what Apple suggested. |
currency | string | ISO 4217; defaults to the currency of the recommendation Apple quoted (then the ad account’s). A different currency from the recommendation’s is refused — an Apple Ads account cannot mix currencies. |
confirm | boolean | REQUIRED true. Without it nothing changes and the refusal states the campaign and the exact budget or CPA change. |
dismiss_apple_ads_recommendation#
Dismiss an Apple Ads recommendation
Decline an Apple Ads recommendation. NOTHING ABOUT THE CAMPAIGN CHANGES — no budget moves, no CPA moves, no status changes — it records that you reviewed the recommendation and said no, and Apple stops offering that particular one (it may generate a new one later). No confirmation is needed and that is deliberate rather than an oversight: dismissing cannot spend money, and gating the safe direction is what teaches people to pass confirm reflexively on the direction that matters. Only an AVAILABLE recommendation can be dismissed; APPLIED and DISMISSED are terminal, and trying to re-action one is refused for free. Free.
| Parameter | Type | Description |
|---|
type | "daily_budget" | "target_cpa" | Default daily_budget. |
idrequired | string | REQUIRED — the recommendation id. |
promotedObjectIdrequired | string | REQUIRED — the same app or brand id the recommendation was queried for. |
promotedObjectType | "APPSTORE_APP" | "BUSINESS_BRAND" | Default APPSTORE_APP. |
apple_ads_impression_share#
Apple Ads impression share read-only
COMPETITIVE INTELLIGENCE: what fraction of the available impressions your app actually captures for each search term, by market and day or week. This is the report that finds where you are being out-bid or out-budgeted — a LOW share on a term with high search popularity means budget, bid or eligibility is capping your reach on a term people are really searching, which is the most actionable row Apple publishes. READ THE ENCODING BEFORE QUOTING A NUMBER: lowImpressionShare and highImpressionShare are TIERED, not continuous. Below 91% the two are equal and read as a single percentage; a row where they diverge to 0.91 and 1.0 means ">90%", not "91 to 100". granularity DAILY covers at most 30 days; WEEKLY_SUN_SAT covers at most 4 weeks and its start date MUST be a Sunday (both are refused for free before any request). reportType FIRST_SLOT (default) is the top ad position only; ALL_SLOTS aggregates every position. Timezone is fixed to UTC by Apple. Read-only, free.
| Parameter | Type | Description |
|---|
promotedObjectIdrequired | string | REQUIRED — the app’s adamId. Apple answers 400 without it. |
countryOrRegion | string[] | ISO 3166-1 alpha-2, e.g. ["US"]. |
startrequired | string | REQUIRED YYYY-MM-DD. Must be a SUNDAY when granularity is WEEKLY_SUN_SAT. |
endrequired | string | REQUIRED YYYY-MM-DD. |
granularity | "DAILY" | "WEEKLY_SUN_SAT" | Default DAILY (max 30-day window). WEEKLY_SUN_SAT allows at most 4 weeks. |
reportType | "FIRST_SLOT" | "ALL_SLOTS" | Default FIRST_SLOT. |
limit | number | maximum number of rows to return |
offset | number | rows to skip, for paging |
apple_ads_search_term_popularity#
Apple Ads search term popularity read-only
The most-searched App Store terms in a genre and market — MARKET-WIDE DEMAND, not your own performance, so it works for finding terms to target before you have spent anything. Cross-read it against apple_ads_impression_share: a term ranking high here where your impression share is low is exactly where budget or bid is leaving installs on the table. genre is a FREE-TEXT App Store genre name (e.g. "PRODUCTIVITY_UTILITIES", "TRAVEL") and Apple publishes no fixed enum, so it is forwarded as given rather than validated against a list that would go stale. granularity WEEKLY_SUN_SAT uses fixed Sunday–Saturday weeks generated Mondays at 07:00 UTC with 65 weeks of retention; MONTHLY uses calendar months refreshed on the 5th UTC with 15 months of retention — an empty result for a very recent window usually means Apple has not generated that period yet rather than that there is no demand. NOTE THE GRANULARITY SETS DIFFER between the two insights reports: DAILY exists on impression share and NOT here. Read-only, free.
| Parameter | Type | Description |
|---|
countryOrRegion | string[] | ISO 3166-1 alpha-2, e.g. ["US","GB","DE"]. |
genre | string[] | App Store genre names, free text, e.g. ["PRODUCTIVITY_UTILITIES"]. |
startrequired | string | REQUIRED YYYY-MM-DD. Must be a SUNDAY when granularity is WEEKLY_SUN_SAT. |
endrequired | string | REQUIRED YYYY-MM-DD. |
granularity | "WEEKLY_SUN_SAT" | "MONTHLY" | Default WEEKLY_SUN_SAT. DAILY does NOT exist on this report. |
limit | number | maximum number of rows to return |
offset | number | rows to skip, for paging |
apple_ads_rejection_reasons#
Why an Apple Ads creative or brand was rejected read-only
WHY an Apple Ads creative, custom product page or Maps brand was REJECTED: Apple’s reason code, type and level (default or custom product page, or a locale of one), the asset or product page it applies to, the country and language, and Apple’s comment — read live. Use it whenever list_apple_ads_creatives shows systemStatus INVALID, or the user asks why an Apple ad is not running. Pass adamId for an app’s reasons, mapsBrandId for a Maps brand’s policy reasons, or rejectionReasonId for one reason. Read-only, free.
| Parameter | Type | Description |
|---|
adamId | string | the app’s App Store id — every reason Apple holds for it |
mapsBrandId | string | a Maps brand id (list_apple_ads_brands) — its policy rejection reasons. Not `brandId`: over the REST API that name means a Hermoso brand |
rejectionReasonId | string | one reason by id |
limit | number | maximum number of reasons to return |
offset | number | reasons to skip, for paging |
apple_ads_change_history#
Apple Ads change history read-only
Who changed what on this Apple Ads account, and when. Call it with start and/or end to get one row per TRANSACTION GROUP — a single user action that may have touched several fields, which is what changeCount counts — with the user, the entity and the event type (CREATE / UPDATE / DELETE). Then pass a row’s detailId back as detailId to get the field-by-field before and after. THE TIME BOUND IS MANDATORY: Apple answers 400 without one, so pass start (meaning "since then"), end ("up to then"), or both. Narrow with campaignId, adGroupId, entityType, eventType or userId. Useful for "why did this campaign’s budget change overnight" and for reconstructing what an agent did on the account. On a CREATE event the old values are empty by design; a DELETE is implemented as a record update so its old values ARE populated. Read-only, free.
| Parameter | Type | Description |
|---|
detailId | string | Fetch field-level detail for ONE change. This is a COMPOSITE id ({transactionId}-{entityType}-{entityId}) that appears on each summary row — not a campaign or ad group id. |
start | string | YYYY-MM-DD. Required unless end or an explicit eventTime filter is given. |
end | string | YYYY-MM-DD. |
filters | object[] | Extra conditions, e.g. [{"field":"campaignId","operator":"EQUALS","value":"123"}]. Non-time fields accept only EQUALS and IN. |
limit | number | maximum number of changes to return |
offset | number | changes to skip, for paging |
list_apple_ads_budget_orders#
List Apple Ads budget orders read-only
Budget orders on this Apple Ads account — a spend CEILING SHARED ACROSS CAMPAIGNS, which is how an agency caps a client for a quarter: total spend across every campaign assigned to one cannot exceed its value, however generous the individual daily budgets are. Each row carries the value, the active window, the systemStatus and, when it is blocking, the systemStatusReasons saying why (EXHAUSTED, EXPIRED, CANCELLED). Pass id for one. AN EMPTY LIST IS NORMAL: a budget order is optional, and without one each campaign is capped only by its own dailyBudget. Read-only, free.
| Parameter | Type | Description |
|---|
id | string | Fetch one budget order, including its invoice details and assigned ad accounts. |
filters | object[] | Query conditions, e.g. [{"field":"name","operator":"CONTAINS","value":"Q3"}]. |
limit | number | maximum number of budget orders to return |
offset | number | budget orders to skip, for paging |
create_apple_ads_budget_order#
Create an Apple Ads budget order destructive
A BUDGET ORDER NEEDS A LINE OF CREDIT ORGANIZATION. Measured on prod 2026-08-17 against org 19534840 (PAYG): Apple answers 403 “The organization does not have a Line of Credit(LOC). Creating Budget Order is permissible only for LOC Organizations. [FORBIDDEN]” — so on a pay-as-you-go account this whole family is unusable and no request shape fixes it. That is Apple’s account tier, not a Hermoso limit and not something to reconnect over. Create a shared spending ceiling that campaigns can draw from. A BUDGET ORDER SPENDS NOTHING AND CANNOT MAKE ANYTHING SERVE — it is a cap, so creating one can only ever reduce what campaigns may spend, never raise it, which is why it takes no confirmation. Nothing draws from it until you set a campaign’s budgetOrderId. TWO APPLE RULES ARE ENFORCED FOR FREE BEFORE ANYTHING IS SENT, because Apple’s own 400s name the field rather than the rule: startTime must be TOMORROW OR LATER at midnight UTC (today is rejected outright), and exactly ONE ad account may be attached — which is why adAccountId is a single value and defaults to the account this connection is pinned to. Omit endTime for an open-ended budget. Apple requires invoice contact details, so primaryBuyerEmail and billingEmail are required and are never invented. Free.
| Parameter | Type | Description |
|---|
namerequired | string | REQUIRED. |
startTimerequired | string | REQUIRED. Must be TOMORROW or later (midnight UTC) — Apple rejects today. |
endTime | string | Omit for an open-ended budget. |
amountrequired | string | REQUIRED total budget as a decimal string, e.g. "50000.00". |
currency | string | ISO 4217; defaults to the ad account’s currency, which it must match. |
primaryBuyerEmailrequired | string | REQUIRED — Apple bills and notifies this address. |
billingEmailrequired | string | REQUIRED. |
invoiceName | string | name shown on the invoice |
orderNumber | string | purchase order or internal order number |
clientName | string | client name shown on the invoice |
primaryBuyerName | string | name of the primary buyer on the invoice |
update_apple_ads_budget_order#
Update an Apple Ads budget order destructive
Change a budget order: its name, window, total value, attached ad account or invoice details. Only the fields you send change; everything omitted keeps its value. RAISING `amount` IS HOW YOU EXTEND A BUDGET APPROACHING EXHAUSTION — an exhausted budget order stops every campaign drawing from it, so this is the tool to reach for when campaigns stop serving with budget left on their own daily caps. AN END DATE ON AN ACTIVE BUDGET ORDER CAN ONLY BE SHORTENED, NEVER EXTENDED; the one exception is openEnded:true, which clears the end date entirely and makes the budget run indefinitely. startTime, if changed, must still be tomorrow or later. Free.
| Parameter | Type | Description |
|---|
idrequired | string | REQUIRED. |
name | string | new budget order name |
startTime | string | Must still be TOMORROW or later. |
endTime | string | Can only SHORTEN an active budget order’s window. |
openEnded | boolean | true clears the end date entirely, making the budget open-ended. This is the only way to extend rather than shorten. |
amount | string | New total, e.g. "75000.00". Raise it to extend a budget nearing exhaustion. |
currency | string | ISO 4217 currency code |
adAccountId | string | Exactly one — Apple rejects more. |
orderNumber | string | purchase order or internal order number |
invoiceName | string | name shown on the invoice |
clientName | string | client name shown on the invoice |
primaryBuyerName | string | name of the primary buyer on the invoice |
primaryBuyerEmail | string | email address Apple bills and notifies for this budget order |
billingEmail | string | billing contact email for invoices |
bulk_apple_ads_keywords#
Bulk create or update Apple Ads keywords destructive
Create or update up to 1,000 Apple Ads keywords or negative keywords in ONE call — how you seed a whole campaign structure from apple_ads_keyword_suggestions, or re-bid a list after a performance review, instead of one call per keyword. A single request can span DIFFERENT AD GROUPS. op:"create" takes full keyword rows; op:"update" takes ids and can change ONLY bid and status, because a keyword’s text and matchType are immutable after creation — changing either means deleting it and creating a new one. ALLOW-PARTIAL-SUCCESS MATTERS AND APPLE’S DEFAULT IS ALL-OR-NOTHING: with it unset, one bad item (a duplicate text/matchType, say) rejects the ENTIRE batch and nothing is written; pass allowPartialSuccess:true to let the good ones land and read the per-item errors. The reply is the per-item read-back Apple returns — each row carries the stored id, text, matchType and status, so you can see exactly which items landed rather than trusting a 200. Adding keywords into an ENABLED ad group can serve on the next auction and therefore takes confirm:true, the same guard add_apple_ads_keywords uses. Apple caps 1,000 per call and 5,000 per ad group in total. Free.
| Parameter | Type | Description |
|---|
kind | "keyword" | "negative_keyword" | Default keyword. |
op | "create" | "update" | Default create. |
itemsrequired | object[] | REQUIRED. create: [{adGroupId,text,matchType,bid?}] for keywords, or [{campaignId|adGroupId,text,matchType}] for negatives. update: [{id,bid?,status?}]. |
allowPartialSuccess | boolean | true keeps the items that succeeded when others fail. Apple’s default is all-or-nothing. |
currency | string | ISO 4217 for bids; defaults to the ad account’s currency. |
confirm | boolean | Required only when the target ad group or campaign is already ENABLED, where the new keywords could serve immediately. |
search_apple_ads_geo#
Search Apple Ads geo locations read-only
Resolve a place NAME into the geo id an Apple Ads ad group actually targets. THIS IS THE ONLY WAY TO GET THOSE IDS: an ad group’s targeting takes adminAreaIds / localityIds / postalCodeIds / countryIds, it does NOT accept place names, and nothing else in Hermoso produces one — so without this tool those targeting fields cannot be used at all. (A campaign’s own `countries` field is a different thing and takes plain ISO country codes.) supplySource is REQUIRED and is not a formality: APPSTORE has NO PostalCode entity (postal codes are a Maps-level geo), and MAPS has NO Country entity and covers only the US and Canada — both are refused for free with the reason. Pass query with at least 2 characters, or "*" to list everything at an entity level. Rows flagged soft-blocked (low search volume, sparse coverage) are still targetable — Apple returns them by default with their eligibility attached. To go the other way and look UP ids you already have, pass geos:[{id|legacyId, entity}] instead of query. Read-only, free.
| Parameter | Type | Description |
|---|
supplySourcerequired | "APPSTORE" | "MAPS" | REQUIRED. APPSTORE for App Store campaigns, MAPS for Apple Maps. Decides which entity types exist. |
query | string | Name to search, minimum 2 characters. "*" returns everything at the entity level. |
entity | "Country" | "AdminArea" | "Locality" | "PostalCode" | Narrow to one granularity. |
countryCode | string | ISO 3166-1 alpha-2 to scope the search. |
eligible | boolean | false excludes soft-blocked geos entirely. Default includes them with eligibility data. |
geos | object[] | Look UP known geos instead of searching by name: [{"id":"12345","entity":"Locality"}] or [{"legacyId":"US|CA|SanFrancisco","entity":"Locality"}]. Exactly one of id or legacyId per entry. |
limit | number | maximum number of geo locations to return |
offset | number | geo locations to skip, for paging |
list_apple_ads_supported_languages#
List Apple Ads supported languages read-only
Every country and region Apple Ads serves, with the languages supported in each market. Use it to VALIDATE A LOCALE BEFORE setting it on an Apple Maps creative’s localizedText or defaultLocale, or on an ad group — a locale a market does not support makes the creative INVALID after it is built, which surfaces as a serving failure long after the mistake was made. Also the definitive answer to "can we advertise in this market and in what language" when expanding a campaign. Read-only, free.
| Parameter | Type | Description |
|---|
filters | object[] | Scope to specific markets, e.g. [{"field":"countryCode","operator":"IN","value":["US","DE"]}]. |
limit | number | maximum number of markets to return |
offset | number | markets to skip, for paging |
get_apple_ads_ad_account#
Get an Apple Ads ad account read-only
The full record for an Apple Ads ad account — its name, currency, timezone, payment model, productFeatures and the advertiser resources delegated to it. Called with no id it returns the account this connection is pinned to. THE FIELD THAT DECIDES WHAT THIS ACCOUNT CAN DO IS productFeatures, and it is IMMUTABLE: APPSTORE_APP_MANUAL can never run Apple Maps ads and BUSINESS_BRAND_MANUAL can never run App Store ads, so an organization that needs both keeps a separate ad account for each. currency, timeZone and paymentModel are inherited from the organization and are immutable too — which is why a campaign’s amounts must be in this currency and cannot be mixed. Read-only, free.
| Parameter | Type | Description |
|---|
id | string | Defaults to the ad account this Apple Ads connection resolved. |
list_apple_ads_advertiser_resources#
List Apple Ads advertiser resources read-only
The brands and content providers this Apple Ads ORGANIZATION can delegate to an ad account — the ids create_apple_ads_ad_account and update_apple_ads_ad_account need. resourceType CONTENT_PROVIDER returns App Store Connect content providers, each identified by its CPID, which is what links an ad account to App Store advertising; BUSINESS_BRAND returns Apple Maps brands by Brand ID. resourceType is REQUIRED — Apple errors without it. This list is ORGANIZATION-WIDE and is deliberately not scoped to any one ad account, which is why a resource can appear here that no account has claimed yet. Read-only, free.
| Parameter | Type | Description |
|---|
resourceTyperequired | "CONTENT_PROVIDER" | "BUSINESS_BRAND" | REQUIRED. CONTENT_PROVIDER = App Store (CPID); BUSINESS_BRAND = Apple Maps (Brand ID). |
create_apple_ads_ad_account#
Create an Apple Ads ad account
Create a new ad account under this Apple Ads organization — the container an agency opens per client, or a business opens to run Apple Maps ads alongside App Store ones. AN AD ACCOUNT SPENDS NOTHING: it holds campaigns, and every campaign inside it is still born PAUSED. productFeatures is REQUIRED, is a CHOICE OF ONE, and is IMMUTABLE FOREVER — APPSTORE_APP_MANUAL for App Store advertising or BUSINESS_BRAND_MANUAL for Apple Maps, never both, and an organization needing both keeps two accounts. Delegations must MATCH that choice: an App Store account delegates a CONTENT_PROVIDER (a CPID from list_apple_ads_advertiser_resources), a Maps account delegates a BUSINESS_BRAND — a mismatch is refused for free here, because Apple accepts it and you get an account that cannot advertise anything. currency, timeZone and paymentModel are inherited from the organization and cannot be set. NOTE: Hermoso keeps writing to the ONE ad account this connection is pinned to — reconnect Apple Ads and choose the new account to build inside it. Free.
| Parameter | Type | Description |
|---|
namerequired | string | REQUIRED. |
productFeaturesrequired | "APPSTORE_APP_MANUAL" | "BUSINESS_BRAND_MANUAL" | REQUIRED and IMMUTABLE. App Store or Apple Maps, never both. |
delegations | object[] | [{"resourceType":"CONTENT_PROVIDER","resourceId":"<CPID>"}] for App Store, or BUSINESS_BRAND + Brand ID for Maps. Must match productFeatures. |
update_apple_ads_ad_account#
Update an Apple Ads ad account destructive
Rename an Apple Ads ad account or change which advertiser resources it delegates — how you link App Store Connect (a CONTENT_PROVIDER / CPID) to an account that cannot yet advertise an app, or attach a brand for Apple Maps. WARNING: DELEGATIONS REPLACE RATHER THAN MERGE. The list you send becomes the account’s ENTIRE delegation set and anything omitted is removed, so to ADD one, read the current set with get_apple_ads_ad_account, append to it, and send the whole list back. productFeatures cannot be changed and Apple ignores it on update — an App Store account can never be converted into an Apple Maps one; create a second ad account instead. currency, timeZone and paymentModel are immutable, inherited from the organization. Free.
| Parameter | Type | Description |
|---|
id | string | Defaults to the ad account this connection resolved. |
name | string | new ad account name |
delegations | object[] | REPLACES the entire set. [{"resourceType":"CONTENT_PROVIDER","resourceId":"<CPID>"}]. |
X ADS CUSTOM AUDIENCES + CONVERSION API
list_x_ads_audiences#
List X custom audiences read-only
List the custom audiences (customer lists) on an X ad account — size, whether each can be targeted yet and why not. Read-only, free.
| Parameter | Type | Description |
|---|
adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
accountId | string | older name for adAccountId (MCP only) |
audienceIds | string[] | return only these audience ids |
query | string | name prefix |
shared | boolean | lists shared with this account instead of owned |
limit | number | max audiences to return |
create_x_ads_audience#
Create an X custom audience
Create an X custom audience (a customer list for retargeting or exclusion), optionally filled with people in the same call. Pass ordinary emails, phones (E.164), X handles, X user ids or device ids — they are normalised and SHA-256 hashed on Hermoso's server before X sees them; 64-hex hashes pass as is; anything unusable is named, never sent. X needs up to 48h and about 100 matched people before it can be targeted (add_x_ads_targeting with targetingType CUSTOM_AUDIENCE). Read back. Free.
| Parameter | Type | Description |
|---|
adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
accountId | string | older name for adAccountId (MCP only) |
namerequired | string | unique per account |
description | string | optional description of the audience |
emails | string[] | plain addresses or 64-hex SHA-256 hashes |
phones | string[] | E.164, e.g. +14155550100, or hashes |
handles | string[] | X handles, with or without @ |
twitterIds | string[] | X user ids to add; hashed on Hermoso's server before X receives them |
deviceIds | string[] | IDFA / AdID / Android id |
users | object[] | one person per entry with several identifiers: {email, phone, handle, twitterId, deviceId} |
expiresAt | string | ISO 8601; X defaults to 13 months |
update_x_ads_audience#
Change an X custom audience destructive
Change an X custom audience: rename / redescribe it, and/or ADD or REMOVE people (operation add|remove) — same identifiers as create_x_ads_audience, hashed on our server. X reports how many it processed and Hermoso relays it; a mismatch is flagged as not fully applied. Read back. Free.
| Parameter | Type | Description |
|---|
adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
accountId | string | older name for adAccountId (MCP only) |
audienceIdrequired | string | custom audience id to change, from list_x_ads_audiences |
name | string | new audience name |
description | string | new audience description |
operation | "add" | "remove" | add or remove: whether the people in this call are added to or removed from the audience |
emails | string[] | plain addresses or 64-hex SHA-256 hashes |
phones | string[] | E.164, e.g. +14155550100, or hashes |
handles | string[] | X handles, with or without @ |
twitterIds | string[] | X user ids to add; hashed on Hermoso's server before X receives them |
deviceIds | string[] | IDFA / AdID / Android id |
users | object[] | one person per entry with several identifiers: {email, phone, handle, twitterId, deviceId} |
effectiveAt | string | ISO 8601 time the membership change takes effect (sent to X as effective_at) |
expiresAt | string | ISO 8601 time the members expire; must be later than effectiveAt |
send_x_ads_conversions#
Send conversions to X (Conversion API) destructive
Report conversions to X server-side (the X Conversion API) — purchases, sign-ups, leads — so X attributes them to the ads that caused them and conversion bidding (goal WEBSITE_CONVERSIONS_V2) has something to optimise toward. pixelId and each eventId come from X Events Manager. Each event needs a click id (twclid from the landing URL), an email or an E.164 phone — emails and phones are SHA-256 hashed on Hermoso's server; IP + user agent only count together or beside one of those. Send conversionId to deduplicate against the X Pixel. Free.
| Parameter | Type | Description |
|---|
adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
accountId | string | older name for adAccountId (MCP only) |
pixelIdrequired | string | the X Pixel (event source) id from Events Manager |
eventId | string | default Event ID for every event |
eventsrequired | object[] | conversion events to report, one entry per purchase, sign-up or lead |
add_x_ads_targeting#
Target an X ads line item
Add targeting criteria to an X LINE ITEM. Targeting does NOT attach to a campaign on X — a campaign carries only budget and funding. Pass locationIds resolved with x_ads_geo_search, and/or criteria[] for any of X's other 37 targeting vocabularies when you already hold the ids. X takes ONE criterion per API call, so a set is several calls: each is reported individually, a partial failure NAMES what did not apply, and the line item's FULL targeting is read back afterwards so the answer is what the line item carries rather than what was sent. Adds no spend — the line item stays PAUSED.
| Parameter | Type | Description |
|---|
adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
accountId | string | older name for adAccountId (MCP only) |
lineItemIdrequired | string | line item id to target, from list_x_ads_line_items |
locationIds | string[] | opaque ids from x_ads_geo_search |
criteria | object[] | [{targetingType, targetingValue, operatorType?}] — operatorType defaults to EQ |
x_ads_geo_search#
Find X ads location ids read-only
Look up X targeting location ids by name — countries, regions, metros, cities and postal codes. X location ids are opaque hashes (Canada is 3376992a082d67c7), so this is the ONLY way to obtain one and there is no name-based targeting parameter to fall back on. Pass the ids to add_x_ads_targeting as locationIds. Read-only, free.
| Parameter | Type | Description |
|---|
queryrequired | string | a place name, e.g. "Canada" or "Austin" |
locationType | "COUNTRIES" | "REGIONS" | "METROS" | "CITIES" | "POSTAL_CODES" | COUNTRIES, REGIONS, METROS, CITIES or POSTAL_CODES; omit to search all types |
limit | number | max results to return |
x_ads_report#
X ads performance report read-only
Performance stats for X campaigns, line items or promoted posts. `placement` here is SINGULAR and comes from a FOUR-value set (ALL_ON_TWITTER / PUBLISHER_NETWORK / SPOTLIGHT / TREND) — deliberately not the twelve placements a line item accepts; do not carry one across. THE TRAP THIS REPORTS: an entity id that does not exist on the account answers 200 with every metric null, which is indistinguishable from a real zero, so an all-null response is FLAGGED rather than narrated as zero performance. Max 20 ids per call. Read-only, free.
| Parameter | Type | Description |
|---|
adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
accountId | string | older name for adAccountId (MCP only) |
entity | "ACCOUNT" | "CAMPAIGN" | "FUNDING_INSTRUMENT" | "LINE_ITEM" | "MEDIA_CREATIVE" | "ORGANIC_TWEET" | "PROMOTED_ACCOUNT" | "PROMOTED_TWEET" | default CAMPAIGN |
entityIdsrequired | string[] | max 20 |
startTime | string | ISO 8601; defaults to 7 days before endTime |
endTime | string | ISO 8601; defaults to now |
granularity | "HOUR" | "DAY" | "TOTAL" | default TOTAL |
placement | "ALL_ON_TWITTER" | "PUBLISHER_NETWORK" | "SPOTLIGHT" | "TREND" | default ALL_ON_TWITTER |
metricGroups | "BILLING" | "ENGAGEMENT" | "LIFE_TIME_VALUE_MOBILE_CONVERSION" | "MEDIA" | "MOBILE_CONVERSION" | "VIDEO" | "WEB_CONVERSION"[] | default ENGAGEMENT |
list_openai_ads_conversion_events#
List ChatGPT Ads conversion events read-only
List the conversion event settings on the connected ChatGPT Ads account. Their ids are what a campaign points at (conversionEventSettingIds) so it optimises for CONVERSIONS rather than raw clicks — without one, conversion-optimised bidding has nothing to optimise toward. Read-only, free.
| Parameter | Type | Description |
|---|
limit | number | max conversion events to return |
create_openai_ads_pixel#
Create a ChatGPT Ads pixel
Create a ChatGPT Ads web pixel — the thing that observes actions on the site. IT RECORDS NOTHING until its snippet is installed on the site, so say that rather than implying tracking is live. MATCH RATE: the snippet’s oaiq("init", { user: {…} }) call accepts email_sha256, phone_number_sha256, external_id_sha256, first_name_sha256, last_name_sha256 (all SHA-256, lowercase hex) plus raw country, city, region and postal_code — tell the site owner to pass whatever they know about a logged-in visitor, because every field improves conversion matching. A pixel is a measurement definition and cannot spend. The next step is create_openai_ads_conversion_event, which says WHICH observed action counts as a conversion. AUTOMATIC ADVANCED MATCHING IS ON BY DEFAULT — that is OpenAI’s own default for pixels created through the Ads API since 2026-08-17, and it means the Pixel COLLECTS AND HASHES CUSTOMER INFORMATION (email, phone) from the page. Tell the user that in plain words, and pass automaticAdvancedMatching:false if they want it off. The reply reports what OpenAI actually stored: there is no endpoint to read a pixel back afterwards, so that one reply is the only record you will ever get.
| Parameter | Type | Description |
|---|
namerequired | string | a name for the pixel |
automaticAdvancedMatching | boolean | default true (OpenAI’s own default). false stops the Pixel collecting and hashing customer information from the page. |
create_openai_ads_conversion_event#
Define a ChatGPT Ads conversion
Define what counts as a conversion on ChatGPT Ads, measured from one or more pixels. THIS IS THE PREREQUISITE for a conversion-optimised campaign: pass the returned id as conversionEventSettingIds to create_openai_ads_campaign. Creating one cannot spend and cannot serve — it is a definition, so it is not confirm-gated.
| Parameter | Type | Description |
|---|
namerequired | string | display name of the conversion event |
eventTyperequired | string | e.g. order_created, lead_created, registration_completed — the plausible words "purchase", "lead" and "signup" are all REFUSED by ChatGPT Ads |
sourceIdsrequired | string[] | exactly ONE source id from create_openai_ads_pixel (ChatGPT Ads takes one source per event; create one event per pixel) |
customEventName | string | for a non-standard event |
attributionWindowDays | number | default 30, which is what OpenAI recommends; 1-90 |
list_openai_ads_audiences#
List ChatGPT Ads custom audiences read-only
List the custom audiences on the connected ChatGPT Ads account. Pass intendedUse to see only the ones eligible for exclusion, inclusion or a bid multiplier (inclusion and bid multipliers need roughly 25,000 matched users). Read-only, free.
| Parameter | Type | Description |
|---|
limit | number | max audiences to return |
intendedUse | "exclusion" | "inclusion" | "bid_multiplier" | only audiences eligible for this use |
create_openai_ads_audience#
Create a ChatGPT Ads custom audience
Create a ChatGPT Ads custom audience from a customer list. Pass plain emails and/or phone numbers: Hermoso NORMALISES AND SHA-256 HASHES THEM LOCALLY and uploads only the digests, so no plaintext personal data leaves Hermoso. OR pass fileUrl: a public link to a UTF-8 .csv or .txt customer list (a CSV needs a header naming its column: email, phone_number, email_sha256, phone_number_sha256 or gaid; identifierResolution "auto" reads several columns at once). A file is uploaded to ChatGPT Ads as it is and OpenAI hashes raw emails and phones itself; the members list is hashed here first. Matching is asynchronous, so read the audience back for its matched count. Values that are neither an email nor a phone number are skipped and counted, never silently dropped. An audience is a definition and cannot spend.
| Parameter | Type | Description |
|---|
namerequired | string | audience name |
members | string[] | emails and/or phone numbers (already-SHA256-hashed emails are passed through as-is); or use fileUrl |
fileUrl | string | public link to a UTF-8 .csv or .txt customer list; upload_file turns a local file into one |
fileName | string | the file name with .csv or .txt, when the link hides it |
identifierType | "email" | "phone" | "email_sha256" | "phone_number_sha256" | "gaid" | the one identifier the file holds (inferred from a CSV header when omitted) |
identifierResolution | "auto" | auto = read every identifier column of a mixed CSV |
description | string | optional audience description |
get_openai_ads_audience#
Read one ChatGPT Ads custom audience read-only
Read one ChatGPT Ads custom audience: its processing status, how many users matched, what it can actually be USED for, and its membership revision. Read-only, free. STATUS is not the same question as eligibility — "ready" only means processing succeeded. EXCLUSION (suppression) has NO minimum size, but INCLUSION and bid multipliers need roughly 25,000 matched users, so a small list used for inclusion simply never serves. The membershipRevision this returns is what update_openai_ads_audience_members REQUIRES for a replace.
| Parameter | Type | Description |
|---|
audienceIdrequired | string | ChatGPT Ads audience id, from list_openai_ads_audiences |
update_openai_ads_audience_members#
Add, remove or replace ChatGPT Ads audience members destructive
Change who is IN a ChatGPT Ads custom audience. Pass plain emails, phone numbers and/or Google Advertising IDs (GAID, UUID-shaped) in ONE list — identifier types may be mixed in a single request: Hermoso normalises and SHA-256 hashes emails and phones locally and only the digests are sent; a GAID rides raw, as the API defines it. SIZE RULES (OpenAI, read 2026-09-03): an audience of ANY size, even empty, can be used for EXCLUSION once ready; for inclusion or bid adjustments plan on 25,000 matched users; there is no 5M ceiling any more, and the reported size is a privacy band (under_25k, 25k_100k, 100k_500k, 500k_1m, 1m_5m, 5m_plus). THREE OPERATIONS: "add" puts people in, "remove" takes the named people OUT (the only way to stop advertising to a segment already in a list), "replace" swaps the WHOLE membership. THIS IS ASYNCHRONOUS — ChatGPT Ads returns an operation id and the change is NOT applied when this returns; poll it with get_openai_ads_audience_operation until it reports succeeded or failed. A replace REQUIRES expectedRevision (read membershipRevision from get_openai_ads_audience) so a wholesale swap cannot land on top of someone else’s change; a mismatch is refused by ChatGPT Ads and applies nothing.
| Parameter | Type | Description |
|---|
audienceIdrequired | string | ChatGPT Ads audience id, from list_openai_ads_audiences |
operationrequired | "add" | "remove" | "replace" | add | remove | replace — replace swaps the entire membership |
membersrequired | string[] | emails and/or phone numbers (already-SHA256-hashed emails pass through as-is) |
expectedRevision | number | string | REQUIRED for replace, optional for add/remove — membershipRevision from get_openai_ads_audience |
list_openai_ads_audience_operations#
List ChatGPT Ads audience membership operations read-only
List the membership operations (add, remove, replace) ChatGPT Ads has accepted on one custom audience, each with its status. Use it to find an operation whose id was lost, or to see what is still pending, before get_openai_ads_audience_operation, cancel_openai_ads_audience_operation or resume_openai_ads_audience_operation. Read-only, free. Pages with limit (1-100, default 25) and cursor.
| Parameter | Type | Description |
|---|
audienceIdrequired | string | ChatGPT Ads audience id whose operations to list |
limit | number | 1-100, default 25 |
cursor | string | nextCursor from the previous page |
cancel_openai_ads_audience_operation#
Cancel a ChatGPT Ads audience membership operation destructive
Cancel an accepted ADD or REMOVE on a ChatGPT Ads custom audience BEFORE its membership changes begin. ChatGPT Ads refuses once the operation is already applying changes, and that refusal is reported as it came. Free. The reply is the status read back after the cancel, never the request alone.
| Parameter | Type | Description |
|---|
audienceIdrequired | string | audience the operation belongs to |
operationIdrequired | string | operation id returned by update_openai_ads_audience_members |
resume_openai_ads_audience_operation#
Resume a ChatGPT Ads audience membership operation destructive
Resume an existing ADD or REMOVE on a ChatGPT Ads custom audience from its own checkpoints with its original inputs, so nothing is uploaded again. Free. The reply is the status read back after the resume, never the request alone.
| Parameter | Type | Description |
|---|
audienceIdrequired | string | audience the operation belongs to |
operationIdrequired | string | operation id of the stopped add or remove |
get_openai_ads_audience_operation#
Check a ChatGPT Ads audience membership operation read-only
Poll an add/remove/replace submitted by update_openai_ads_audience_members until it reports succeeded or failed. Read-only, free. Until it succeeds the membership has NOT changed, so never report an audience update as done on the strength of the submission alone.
| Parameter | Type | Description |
|---|
audienceIdrequired | string | audience the operation belongs to |
operationIdrequired | string | operation id returned by update_openai_ads_audience_members |
merge_openai_ads_audiences#
Merge ChatGPT Ads custom audiences
Combine 2 to 64 existing ChatGPT Ads custom audiences into ONE new audience. The SOURCE audiences are left unchanged, later updates to them do NOT propagate into the merged one, and no existing campaign switches to the new id by itself — re-point targeting deliberately if that is the intent. Creates a new audience; changes nothing that is already serving.
| Parameter | Type | Description |
|---|
namerequired | string | name for the new merged audience, at least 3 characters |
audienceIdsrequired | string[] | 2 to 64 DISTINCT audience ids from list_openai_ads_audiences |
archive_openai_ads_audience#
Archive a ChatGPT Ads custom audience (permanent) destructive
Retire a ChatGPT Ads custom audience. ARCHIVING IS PERMANENT AND THERE IS NO DELETE TO UNDO IT: an archived audience can never be restored, targeted or bid on again, and any campaign that includes or excludes it loses that audience. Without confirm:true this archives NOTHING and instead reports the audience’s real name, status and matched size read back from ChatGPT Ads, so the cost is visible before it is paid. Pass confirm:true only once that is what the user wants.
| Parameter | Type | Description |
|---|
audienceIdrequired | string | ChatGPT Ads audience id to archive; archiving is permanent |
confirm | boolean | must be true to actually archive — this cannot be undone |
create_openai_ads_campaign#
Build a ChatGPT Ads campaign (paused) destructive
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). Geo (countries / locationIds) and PLATFORMS (which of the iOS app, Android app and web the ad runs on) are the only other dimensions — leave platforms out to run on all three. 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.
| Parameter | Type | Description |
|---|
namerequired | string | campaign name, at least 3 characters |
description | string | optional campaign description |
dailyBudget | number | daily cap in the AD ACCOUNT’S currency — ChatGPT Ads’ own minimum for a DAILY budget is 25.00 |
lifetimeBudget | number | lifetime cap in the account currency — no 25.00 floor applies here, so use this to spend less than that in total. Pass this and/or dailyBudget; a budget is required. |
businessAgentId | string | run a BUSINESS AGENT campaign (mode business_agent): the id from create_openai_ads_business_agent, which must be PUBLISHED |
objective | "reach" | "clicks" | "conversions" | the campaign objective. objective "conversions" with billingEventType "impression" is the impression-billed, conversion-optimised shape |
billingEventType | "impression" | "click" | what the campaign is billed on |
landingPageQueryTemplate | string | query-string template appended to landing URLs (e.g. utm_source=chatgpt&utm_campaign={campaign_id}) — ChatGPT Ads’ landing_page_configuration.query_string_template |
biddingType | "impressions" | "clicks" | "conversions" | default clicks (CPC). OpenAI suggests starting at a 3–5 max bid per click. "conversions" is oCPC — you still pay per click, but ChatGPT Ads optimises toward a conversion event, and it REQUIRES conversionEventSettingIds naming exactly one active event setting. |
countries | string[] | 2-letter country codes |
locationIds | string[] | ids from openai_ads_geo_search — up to 2,500 |
platforms | "ios_app" | "android_app" | "web" | "desktop_web" | "ios_web" | "android_web"[] | WHICH CHATGPT SURFACES THIS CAMPAIGN RUNS ON — OpenAI’s “Eligible platforms”: ios_app (the ChatGPT iOS app), android_app (the Android app), web (ChatGPT in ANY browser), or narrower browser targets desktop_web, ios_web and android_web. OMIT IT to run everywhere, which is the default and almost always right; naming a subset STOPS the ad serving everywhere else. Never combine web with desktop_web / ios_web / android_web: web already includes them, and OpenAI says to name the individual browser values without web when narrowing (that combination is refused before anything is sent). There is no empty state — ChatGPT Ads refuses an empty list — so widening back means naming ios_app, android_app and web. |
customAudienceIds | string[] | TARGET a CUSTOM AUDIENCE — ids from list_openai_ads_audiences (created with create_openai_ads_audience, then filled with members). Until 2026-08-12 an audience could be created AND uploaded and then pointed at nothing: this is the field that consumes them. Combines with geo — the ad reaches people in the named locations who are ALSO in these audiences. |
excludedCustomAudienceIds | string[] | EXCLUDE custom audiences — same ids, opposite effect (suppressing existing customers, say). An id in BOTH lists is refused rather than resolved by a guess, because OpenAI does not document which side wins. |
conversionEventSettingIds | string[] | REQUIRED when biddingType is "conversions" (oCPC): exactly one active conversion-event-setting id from list_openai_ads_conversion_events, the event ChatGPT Ads optimises toward. Ignored for clicks/impressions bidding. |
startTime | number | unix seconds |
endTime | number | unix seconds |
adGroup | object | build 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 destructive
Add an ad group to an existing ChatGPT Ads campaign. Created PAUSED by default. Bidding: pass maxBid for a fixed bid, or bidStrategy "maximize_clicks" / "maximize_conversions" to let ChatGPT Ads set the bid for the budget (OpenAI’s "Maximize results"), in which case maxBid is optional. 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.
| Parameter | Type | Description |
|---|
campaignIdrequired | string | campaign id to add the ad group to |
namerequired | string | ad group name |
description | string | optional ad group description |
maxBid | number | max bid in the account currency — REQUIRED unless bidStrategy is a maximize_* one, which sets the bid itself |
bidStrategy | "fixed_bid" | "maximize_clicks" | "maximize_conversions" | HOW THIS AD GROUP BIDS — OpenAI’s “Maximize results”. fixed_bid (default) uses your maxBid as a hard cap; maximize_clicks and maximize_conversions let ChatGPT Ads set the bid to get the most of that outcome for the budget, and with either of those maxBid is OPTIONAL. maximize_conversions additionally needs the CAMPAIGN on biddingType "conversions" with a conversion event setting attached. |
billingEvent | "click" | "impression" | default click |
contextHints | string[] | up to 2,000, deduplicated server-side |
landingPageQueryTemplate | string | query string added to every landing page click, e.g. utm_source=chatgpt&utm_content={ad_id}. Placeholders: {campaign_id} {ad_group_id} {ad_id} {ad_account_id} {oppref}; oppref and olref cannot be parameter names. On an update, an empty string clears it. |
audienceBidMultipliers | object[] | bid more or less for people in a custom audience (eligible ids: list_openai_ads_audiences with intendedUse bid_multiplier, about 25,000 matched users). On an update this REPLACES the list; [] removes them all, and a bid change keeps the existing multipliers. |
status | "active" | "paused" | default paused |
confirm | boolean | REQUIRED true to create this ACTIVE under a live campaign (real spend) |
create_openai_ads_ad#
Create a ChatGPT Ads ad destructive
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. OpenAI creative enhancements (on by default for self-serve campaigns since 2026-10-30) may serve this copy rewritten, translated or on another page of the same site; the switch is in ChatGPT Ads Manager (account and campaign) and the API does not expose it, so never promise the exact words will run.
| Parameter | Type | Description |
|---|
adGroupIdrequired | string | ad group id to create the ad in |
name | string | internal name — defaults to the title |
creativerequired | object | title 3-50 characters, body up to 100, landing page URL and one still image; no video |
landingPageQueryTemplate | string | query string added to every landing page click, e.g. utm_source=chatgpt&utm_content={ad_id}. Placeholders: {campaign_id} {ad_group_id} {ad_id} {ad_account_id} {oppref}; oppref and olref cannot be parameter names. On an update, an empty string clears it. |
status | "active" | "paused" | default paused |
confirm | boolean | REQUIRED true to create this ACTIVE in a live ad group (real spend) |
openai_ads_bulk#
ChatGPT Ads bulk create/update job destructive
Create or update up to 1,000 ChatGPT Ads campaigns, ad groups and ads in ONE asynchronous job — the direct analogue of a Google Ads atomic mutate, and the only way to build a whole tree in a single call. It builds CHAT-CARD campaigns only: its campaign.create has no product-feed mode, and OpenAI’s spreadsheet bulk creation of product-feed campaigns and ad groups with auto-generated ad templates (announced 2026-09-30) is an Ads Manager feature with no API. Build a product-feed campaign with create_openai_ads_campaign (mode product_feed), create_openai_ads_ad_group and an ad whose creative type is the product-ad template instead. Each operation is {operation_id, type, idempotency_key|target_resource_id, input}. Types: campaign.create, campaign.update, ad_group.create, ad_group.update, ad.create, ad.update. FORWARD REFERENCES are the point: set input.campaign_idempotency_key / input.ad_group_idempotency_key to another CREATE operation’s idempotency_key and the child attaches to the parent made in the same job (a key no operation mints is refused here, for free, rather than failing the whole job at OpenAI). EVERYTHING CREATED IS PAUSED and spends nothing — status:"active" on a create is overridden, and an UPDATE that would set "active" is REFUSED BY NAME, because one job could otherwise arm a thousand objects in a single call that no confirmation ever saw; turn things on one at a time with set_openai_ads_status. Use validateOnly:true for a FREE dry run (it checks fields and dependencies but NOT update-target existence, image fetching or entity limits, so a validated job can still fail for real). Returns a jobId — poll it with openai_ads_bulk_job; an operation’s result is only final once the job is completed, partially_failed or failed. LIMITS (OpenAI’s own): 1–1000 operations, 16 MiB body, 512 KiB per operation, 10 job creates per 10 seconds per ad account, campaign budget ≥ 1000000 micros, names 3–1000 chars, ad titles 3–50, bodies ≤100, URLs ≤2048, ≤2500 location ids, ≤2000 context hints. THE BULK API IS IN LIMITED PREVIEW AND ENABLED PER AD ACCOUNT: a 404 means this account has not been granted it (not a wrong path), and the refusal says so and names the per-object tools that do the same work.
| Parameter | Type | Description |
|---|
operationsrequired | object[] | 1–1000 operations: {operation_id, type, idempotency_key (creates) or target_resource_id (updates), input:{…}} |
validateOnly | boolean | true = FREE dry run; nothing is created or changed |
partialFailure | boolean | default true (independent operations continue after an error). false skips later operations after a failure and does NOT roll back what already completed. |
idempotencyKey | string | request-level key that makes an uncertain retry safe. Reusing it with a DIFFERENT body is an error at OpenAI. To rerun a failed job, submit the same body with a NEW request-level key. |
openai_ads_bulk_job#
Read a ChatGPT Ads bulk job read-only
Poll a ChatGPT Ads bulk job and read the result of EVERY operation in it. Reports the job status (pending, in_progress, completed, partially_failed, failed — the last three are terminal) plus a per-operation verdict: created, updated, validated, failed or skipped, each with the new resource id or the error. THE VERDICT COMES FROM THE OPERATIONS, NEVER FROM THE JOB STATUS — "the job finished" and "your ad was created" are different questions, and a partially_failed job answers yes to one and no to the other. While a job is still running the results are an INCOMPLETE SNAPSHOT and there is no cursor to page with; once complete is true, page with `after` set to the last operation_id. A failed operation that reports retryable names retry_after_seconds — reuse the ORIGINAL create idempotency_key when resubmitting or the retry creates a duplicate. Free, read-only.
| Parameter | Type | Description |
|---|
jobIdrequired | string | the id openai_ads_bulk returned |
limit | number | 1–100 results per page, default 100 |
after | string | the last operation_id from the previous page — only available once complete is true |
update_openai_ads_feed_products#
Update ChatGPT Ads product-feed variants destructive
Update the PRICE, TITLE or AVAILABILITY of variants already in a ChatGPT Ads product feed — send only what changed instead of re-uploading the catalog. This is what stops a stale feed serving ads for out-of-stock items. Pass feedId and products:[{id, variants:[{id, title?, price?:{amount,currency}, availability?:{available|status}}]}], where products[].id is the PARENT product id and variants[].id the existing variant id, both from the catalog already in the feed. price.amount is an INTEGER IN MINOR UNITS — 8999 means $89.99 — so a decimal is a hundredfold error in the price you advertise, and is refused. IT UPDATES EXISTING VARIANTS ONLY: it never creates a feed, uploads a catalog, or adds a product that is not already there. ACCEPTED IS NOT LIVE — OpenAI applies the change asynchronously and returns no completion timestamp and no downstream result, so an out-of-stock product keeps serving until it propagates; say that rather than reporting it as done. A 403 naming product_feed_api_disabled / product_feed_delta_api_disabled means the ACCOUNT lacks Feeds access — nothing about the request was wrong and retrying it unchanged will not help. A 404 means the feed id is wrong, the feed is not linked to this ad account, or Feeds is not enabled.
| Parameter | Type | Description |
|---|
feedIdrequired | string | the product feed already linked to this ad account |
productsrequired | object[] | [{id, variants:[{id, title?, price?:{amount:int minor units, currency}, availability?:{available:bool}|{status:"in_stock"|"out_of_stock"}}]}] |
update_openai_ads_object#
Edit a ChatGPT Ads campaign / ad group / ad destructive
EDIT an existing ChatGPT Ads object in place — rename it, change a campaign’s budget or geo targeting, rewrite an ad group’s context hints or bid, or replace an ad’s title, body, landing page or image. Pass level:"campaign" + campaignId, level:"adGroup" + adGroupId, or level:"ad" + adId. Only the fields you pass are changed, but note that context hints, bidding, TARGETING and the creative are REPLACED WHOLESALE rather than merged, so send the complete list. A patch that would silently DESTROY something is refused by name with what would have been lost, rather than going through: dropping a campaign’s geo, custom audiences or PLATFORM targeting, deleting an ad group’s max bid, or demoting a maximize_* bid strategy to a fixed bid. 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.
| Parameter | Type | Description |
|---|
level | "campaign" | "adGroup" | "ad" | inferred from which id you pass |
campaignId | string | campaign id to edit; use with level campaign |
adGroupId | string | ad group id to edit; use with level adGroup |
adId | string | ad id to edit; use with level ad |
name | string | new name |
description | string | new description |
dailyBudget | number | new daily budget in the account currency; campaign level |
lifetimeBudget | number | new lifetime budget in the account currency; campaign level |
countries | string[] | 2-letter country codes; replaces the whole geo list, so send the full list |
locationIds | string[] | location ids from openai_ads_geo_search; replaces the whole list |
endTime | number | unix seconds when the campaign stops; campaign level |
platforms | "ios_app" | "android_app" | "web" | "desktop_web" | "ios_web" | "android_web"[] | REPLACES which ChatGPT surfaces the campaign runs on (ios_app / android_app / web, or the narrower browser targets desktop_web / ios_web / android_web; never web together with those, since web already includes them). Wholesale like the rest of targeting: a patch that changes geo or audiences on a campaign that already restricts platforms is REFUSED by name rather than silently widening it back to every surface. There is no [] — name all three to go back to everywhere. |
customAudienceIds | string[] | REPLACES the campaign’s targeted custom audiences. TARGETING IS REPLACED WHOLESALE, not merged — a patch that omits something the campaign already targets is REFUSED by name rather than silently dropping it, so restate it here or pass [] to clear it deliberately. |
excludedCustomAudienceIds | string[] | REPLACES the campaign’s excluded custom audiences — same wholesale rule as customAudienceIds. |
conversionEventSettingIds | string[] | NOT CHANGEABLE: ChatGPT Ads refuses a new conversion event on an existing campaign ("cannot be modified after creation"). Passing it is refused with the way forward: build a new campaign with create_openai_ads_campaign, move the ad group and ads, pause this one. |
contextHints | string[] | REPLACES the existing list |
maxBid | number | max bid per billing event in the account currency; ad group level |
billingEvent | "click" | "impression" | required alongside maxBid — bidding is replaced wholesale |
bidStrategy | "fixed_bid" | "maximize_clicks" | "maximize_conversions" | CHANGE HOW THIS AD GROUP BIDS (OpenAI’s “Maximize results”). BIDDING IS REPLACED WHOLESALE, so a patch that moves the bid without restating the strategy would DEMOTE a maximize_* ad group to a fixed bid, and one that sets a strategy without restating maxBid DELETES the cap — both are refused by name with what would have been lost. |
creative | object | REPLACES the ad’s creative (text + image card only) |
landingPageQueryTemplate | string | query string added to every landing page click, e.g. utm_source=chatgpt&utm_content={ad_id}. Placeholders: {campaign_id} {ad_group_id} {ad_id} {ad_account_id} {oppref}; oppref and olref cannot be parameter names. On an update, an empty string clears it. |
audienceBidMultipliers | object[] | bid more or less for people in a custom audience (eligible ids: list_openai_ads_audiences with intendedUse bid_multiplier, about 25,000 matched users). On an update this REPLACES the list; [] removes them all, and a bid change keeps the existing multipliers. |
confirm | boolean | REQUIRED true to change budget / bid / creative on a LIVE object |
set_openai_ads_budget#
Set a ChatGPT Ads campaign budget destructive
Change a ChatGPT Ads campaign’s budget — a daily cap, a lifetime cap, or both, in the ad account’s currency. A LIFETIME (total) budget is PACED by ChatGPT Ads to spread spend evenly across the campaign’s dates (actual daily spend still varies with available delivery), so a total budget is not a first-come-first-served pool. ChatGPT Ads’ own minimum for a DAILY budget is 25.00 (measured live 2026-08-05; a LIFETIME budget has no such floor, so use one to spend less than that in total). 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.
| Parameter | Type | Description |
|---|
campaignIdrequired | string | campaign id whose budget changes |
dailyBudget | number | daily cap in the account currency; ChatGPT Ads minimum is 25.00 |
lifetimeBudget | number | lifetime cap in the account currency; no 25.00 minimum applies |
confirm | boolean | REQUIRED true when the campaign is live |
openai_ads_conversions#
ChatGPT Ads attributed conversions read-only
ATTRIBUTED CONVERSIONS for ChatGPT Ads — the number the whole pixel + conversion-event setup exists to produce, beyond what openai_ads_report shows: that report carries conversions and CPA for the account, a campaign, ad group or ad, while this tool adds the click-through / view-through split, totals per id across many ids, and the received-event sample. Pass entityIds — the campaign / ad group / ad ids to report on — with a matching level; the default window is the last 30 days. NEVER ADD viewThroughConversions TO conversions: since OpenAI’s 2026-09-30 change conversions already counts click-through PLUS view-through goal conversions within the chosen windows (their example: 3 click-through and 2 view-through give conversions 5), and the rows and totals carry clickThroughConversions and viewThroughConversions as its two parts. Post-click conversion rate stays click-through only. NO ROWS means no attributed conversion was recorded, not that data is missing — say exactly that, and check that an event setting exists (list_openai_ads_conversion_events) and that its pixel snippet is actually live on the site. RECEIVED EVENTS: pass recentEvents:true (no ids needed) to read a recent SAMPLE of the events OpenAI actually received on the pixel — type, event time, receive time, API channel and the event id — which answers "did OpenAI get the signup at all?" when a conversion is missing. It is a sample, not a complete log, and receiving an event is not the same as attributing it. BEYOND THE GOAL (on by default): each row and the reply carry attributedEvents, every event OpenAI attributed whether or not it is the campaign’s goal (e.g. purchases on a sign-up campaign), with count and value; totals.orderSales is attributed purchase value. An event count already includes goal activity: NEVER add it to conversions; a null value is unknown, not zero. A current day is not final until OpenAI’s daily processing runs. Read-only, free.
| Parameter | Type | Description |
|---|
level | "ad_account" | "campaign" | "ad_group" | "ad" | inferred from which id you pass — default ad_account |
entityIds | string[] | the campaign, ad group or ad ids to report on, required below the account level; omit for level ad_account, which sums every campaign |
campaignId | string | campaign id to report on; sets the level to campaign |
adGroupId | string | ad group id to report on; sets the level to ad_group |
adId | string | ad id to report on; sets the level to ad |
since | string | YYYY-MM-DD |
until | string | YYYY-MM-DD |
granularity | "none" | "daily" | daily = one row per day per entity, plus a byDate total; default none = one total per entity |
breakdown | "device" | "country" | split each row by device or by country |
recentEvents | boolean | true = list a recent sample of the events OpenAI RECEIVED on the pixel instead of attributed totals |
pixelId | string | with recentEvents: the pixel_id to read; omit to use the account's first pixel |
limit | number | with recentEvents: how many events, 1-50 (default 50) |
attributedEvents | boolean | default true; false = goal counts only |
eventNames | string[] | only these events, e.g. ["order_created"] |
attributionTimeBasis | "ad_event_time" | "conversion_time" | default ad_event_time |
clickWindowDays | number | number | number | default 30 |
viewWindowDays | number | number | default 1; 0 = no views |
includeZeroRows | boolean | default true |
preview_openai_ads_ad#
Preview a ChatGPT Ads ad read-only
Render a real preview of an existing ChatGPT Ads ad, so a human can LOOK at what will run instead of reading a list of ids back. Returns a hosted preview URL. THE LINK EXPIRES 24 HOURS AFTER IT IS CREATED — state that whenever you hand it to anyone, and generate a fresh one rather than re-sending an old one, because a dead link given to a client is worse than no link. Changes nothing about the ad; free.
| Parameter | Type | Description |
|---|
adIdrequired | string | the ad to preview — list_openai_ads_campaigns with an adGroupId lists them |
send_openai_ads_conversions#
Send server-side conversion events to ChatGPT Ads destructive
Send conversion events to ChatGPT Ads from a SERVER (the Conversions API). USER MATCHING (schema of 2026-09-03): pass plaintext email / phone (with country code) / externalId / firstName / lastName and Hermoso hashes them locally, plus raw country / city / region / zip_code and gaid (Android advertising id); each becomes the API’s plural list (emails_sha256, phone_numbers_sha256, …), the first three unique values per list count, and more fields = better matching. This is the only way a conversion that did not happen in the browser — an offline sale, a webhook, a mobile backend, a CRM — is ever counted, and it is the half of the measurement loop that create_openai_ads_pixel and create_openai_ads_conversion_event exist to set up. TWO IDS LOOK ALIKE AND ONLY ONE WORKS: pass `pixelSnippetId`, which is OpenAI’s `pixel_id` and is what create_openai_ads_pixel returns under that name — NOT the `pixelId` (their `clidsrc_…` value), which is the conversion SOURCE id that an event setting takes as sourceIds. OpenAI’s own words: "Use `id` as a `source_ids` value when you create an event setting. Use `pixel_id` … when you send Conversions API events." THE KEY IS ALSO NOT THE ONE YOU THINK: `apiKey` is the CONVERSIONS API key from create_openai_ads_conversion_api_key, not the Advertiser API key this workspace is connected with — OpenAI return it exactly once so Hermoso holds no copy and it must be passed in. Each event needs an `id` and a `type`, plus `source_url` for a web event; the data SHAPE is fixed by the event type and Hermoso fills it in, and MONEY IS AN INTEGER IN THE CURRENCY’S MINOR UNIT (4250 means $42.50 — sending 42.50 is refused, not rounded). Timestamps must be inside the last 7 days and no more than 10 minutes ahead. ONE BAD EVENT FAILS THE WHOLE BATCH of up to 1,000, so Hermoso validates locally first and names the offending event and field instead of letting OpenAI discard all of them. Use validateOnly:true for a free dry run that VALIDATES AND SAVES NOTHING — never tell a user a validate-only run was measured. If the browser pixel and the server both send the same conversion, give them the SAME id so OpenAI deduplicates it. Attribution is not instant: read openai_ads_conversions later rather than promising a number now. Free — costs no credits.
| Parameter | Type | Description |
|---|
pixelSnippetIdrequired | string | OpenAI’s pixel_id — create_openai_ads_pixel returns it as pixelSnippetId. NOT the clidsrc_… pixelId, which is the conversion source id. |
apiKeyrequired | string | the Conversions API key (create_openai_ads_conversion_api_key) — NOT the Advertiser API key the connector stores |
eventsrequired | object[] | up to 1000 events. Each: {id, type (order_created / lead_created / page_viewed / custom / …), timestamp_ms?, source_url? (required for web), action_source?, user? {email or externalId — Hermoso hashes them locally, plus country/city/zip_code/ip_address/user_agent/obref}, data? {amount as an INTEGER in minor units, currency, contents[]}} |
validateOnly | boolean | true validates the batch against ChatGPT Ads and SAVES NOTHING — no conversion is recorded |
integrationSource | string | stable identifier for the integration sending the batch; defaults to "hermoso" |
create_openai_ads_conversion_api_key#
Create a ChatGPT Ads Conversions API key
Create a ChatGPT Ads Conversions API key — the credential a SERVER uses to send conversion events, which is the only way a conversion that happens off the page (an offline sale, a webhook, a mobile backend) can be counted at all. PERMANENT AND UNREPEATABLE, and both halves of that are why it is confirm-gated even though a key cannot spend: ChatGPT Ads publishes NO way to list, rotate or delete a key, so it exists on the ad account forever and Hermoso cannot clean it up; and OpenAI returns the secret EXACTLY ONCE. Tell the user both facts, get an explicit yes, then call with confirm:true — and tell them to store it in a server-side secret manager and never place it in browser code, client-visible environment variables, logs or source control. If OpenAI answers that key creation is not enabled for this ad account, that is an account-enablement answer from OpenAI (they say to contact your partner representative) — the connection is fine and nothing is broken on Hermoso’s side.
| Parameter | Type | Description |
|---|
namerequired | string | a descriptive name, 3–1000 characters — it can never be listed or renamed afterwards |
confirm | boolean | REQUIRED true — the key is permanent and its secret is shown once |
update_openai_ads_account#
Update the ChatGPT Ads account (brand / account / legal name, URL, favicon) destructive
Update the ChatGPT Ads account’s names, primary URL and/or favicon. brandName is shown IN ADS, accountName is internal to Ads Manager, legalName is the business. Per OpenAI, a brand-name change PAUSES AD DELIVERY until brand review passes and a legal-name change restarts business review and may pause serving; an account name triggers no review. So a brand or legal change needs confirm:true: without it nothing changes and the reply names the old and new values and the pause, which the user must agree to first. Legacy name sets all three (cannot be combined with them). An unchanged name is not sent. Also url, faviconUrl (public image or home page, ≥256×256) or faviconFileId. An account whose brand review is not approved cannot serve: re-read it before promising anything runs. A 403 “not enabled” is OpenAI’s account setting, not a Hermoso failure.
| Parameter | Type | Description |
|---|
brandName | string | shown in ads; pauses delivery until review (confirm) |
accountName | string | internal; no review |
legalName | string | restarts business review (confirm) |
confirm | boolean | required for a brand or legal change |
name | string | legacy: sets all three names |
url | string | the account’s primary destination — a public http(s) URL. OpenAI accepts this field even though their published parameter table omits it. |
faviconUrl | string | public image URL (or the brand home page) to upload and assign as the account favicon — minimum 128×128 |
faviconFileId | string | an already-uploaded favicon file id, if you have one |
set_openai_ads_status#
Activate, pause or archive a ChatGPT Ads account / campaign / ad group / ad destructive
Turn a ChatGPT Ads AD ACCOUNT, campaign, AD GROUP or AD on (active), off (paused), or ARCHIVE it. Pass level:"account" (NO id — the connection IS the account), level:"campaign" + campaignId, level:"adGroup" + adGroupId, or level:"ad" + adId. LEVEL "account" IS THE EMERGENCY STOP AND BEHAVES DIFFERENTLY FROM EVERY OTHER LEVEL: BOTH directions take confirm:true, because pausing it stops ALL ChatGPT Ads delivery for the brand at once and activating it un-suppresses EVERY active campaign underneath — Hermoso cannot tell you in advance which of them will start spending. An account cannot be archived. 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.
| Parameter | Type | Description |
|---|
level | "account" | "campaign" | "adGroup" | "ad" | inferred from which id you pass — default campaign. "account" is the whole ad account and takes no id. |
campaignId | string | campaign id to change; use with level campaign |
adGroupId | string | ad group id to change; use with level adGroup |
adId | string | ad id to change; use with level ad |
statusrequired | "active" | "paused" | "archived" | active = start spending; paused = stop; archived = permanent (not available at level "account") |
confirm | boolean | REQUIRED true to activate (real spend), to archive (irreversible), or for EITHER direction at level "account" |
confirmName | string | when archiving or removing: its EXACT name, as the unconfirmed call names it. Required when it has children or is live |
confirmChildren | number | when archiving or removing: the exact number of children the unconfirmed call reported. Required when it has any |
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.
| Parameter | Type | Description |
|---|
level | "campaign" | "adGroup" | "ad" | what to archive — default campaign |
campaignId | string | campaign id to archive; use with level campaign |
adGroupId | string | ad group id to archive; use with level adGroup |
adId | string | ad id to archive; use with level ad |
confirm | boolean | REQUIRED true — archiving cannot be undone |
confirmName | string | the object’s EXACT name, required when it has children or is live |
confirmChildren | number | the exact number of children reported by the unconfirmed call, required when it has any |
ChatGPT ADS BREADTH
list_openai_ads_lead_forms#
List ChatGPT Ads lead forms read-only
The ChatGPT Ads LEAD FORMS on this ad account — id, status (draft/published/archived), draft and published revision ids, fields. A lead form is what a Business Agent uses to collect a name/email/choice inside the chat; it only collects once PUBLISHED, and leads only arrive somewhere once a lead-sync subscription exists (list_openai_ads_lead_sync). Read-only, 0 credits. Needs ChatGPT Ads connected.
No parameters.
create_openai_ads_lead_form#
Create a ChatGPT Ads lead form (draft)
Draft a ChatGPT Ads LEAD FORM: a name, an optional privacy policy URL, and fields — each {fieldId, fieldType "text" | "choice", label, required, options (choice only)}. Created as a DRAFT with a draftRevisionId; publish_openai_ads_lead_form makes it live, then reference it from a Business Agent (leadFormId). Needs ChatGPT Ads connected.
| Parameter | Type | Description |
|---|
namerequired | string | lead form name |
privacyPolicyUrl | string | privacy policy URL shown with the form |
fieldsrequired | object[] | form questions, in the order shown |
get_openai_ads_lead_form#
Read a ChatGPT Ads lead form read-only
One ChatGPT Ads lead form with its fields and revision ids (pass revisionId to read a specific revision). Read-only, 0 credits.
| Parameter | Type | Description |
|---|
leadFormIdrequired | string | lead form id, from create_openai_ads_lead_form or the account |
revisionId | string | read this revision instead of the latest one |
update_openai_ads_lead_form#
Save a new draft of a ChatGPT Ads lead form destructive
Save a NEW DRAFT REVISION of a ChatGPT Ads lead form (the whole definition: name + fields are replaced). expectedDraftRevisionId must be the draftRevisionId you last read — ChatGPT Ads refuses a stale one so two editors cannot clobber each other. The published version is untouched until publish_openai_ads_lead_form.
| Parameter | Type | Description |
|---|
leadFormIdrequired | string | lead form id to edit |
expectedDraftRevisionIdrequired | string | draftRevisionId from the last read; refused if stale |
namerequired | string | lead form name; the whole definition is replaced |
privacyPolicyUrl | string | privacy policy URL shown with the form |
fieldsrequired | object[] | all form questions in order; replaces the whole field list |
publish_openai_ads_lead_form#
Publish a ChatGPT Ads lead form destructive
Publish the current draft of a ChatGPT Ads lead form so Business Agents can use it. expectedDraftRevisionId is the draftRevisionId you last read. Show the user the fields before publishing: the form is what real people will see.
| Parameter | Type | Description |
|---|
leadFormIdrequired | string | lead form id to publish |
expectedDraftRevisionIdrequired | string | draftRevisionId from the last read; refused if stale |
archive_openai_ads_lead_form#
Archive a ChatGPT Ads lead form destructive
Archive a ChatGPT Ads lead form. Not reversible through the API, and refused by ChatGPT Ads while a PUBLISHED Business Agent references it. confirm:true is required; without it nothing changes and the reply says what would.
| Parameter | Type | Description |
|---|
leadFormIdrequired | string | lead form id to archive |
confirm | boolean | true to archive; archiving cannot be reversed through the API |
test_openai_ads_lead_form#
Send a synthetic test lead
Queue an explicitly SYNTHETIC, signed test lead for a PUBLISHED lead form to the account’s lead-sync webhook, so the receiving system can be checked end to end. expectedPublishedRevisionId is the form’s publishedRevisionId. The payload is marked synthetic; never count it as a lead.
| Parameter | Type | Description |
|---|
leadFormIdrequired | string | published lead form id |
expectedPublishedRevisionIdrequired | string | publishedRevisionId of the form, from get_openai_ads_lead_form |
list_openai_ads_lead_sync#
List ChatGPT Ads lead-sync subscriptions read-only
The lead-sync subscription(s) on this ChatGPT Ads account — the webhook that receives every lead a lead form collects. With none, leads have nowhere to go. Read-only, 0 credits.
No parameters.
set_openai_ads_lead_sync#
Provision ChatGPT Ads lead delivery (webhook)
Provision lead delivery for this ChatGPT Ads account. OMIT destinationUrl to deliver to HERMOSO: leads land in Hermoso’s own signed receiver, list_openai_ads_leads reads them and the HubSpot lead sync can push them into HubSpot (no secret to store). Or pass your own https destinationUrl (a managed webhook), optionally with your own signingSecret; then THE SIGNING SECRET IS RETURNED ONCE — every lead webhook is signed with it. Needs ChatGPT Ads connected.
| Parameter | Type | Description |
|---|
destinationUrl | string | omit to deliver the leads to Hermoso |
signingSecret | string | your own webhook signing secret; only used with destinationUrl, returned once |
list_openai_ads_leads#
ChatGPT Ads leads received read-only
The ChatGPT Ads leads delivered to Hermoso for this brand, newest first: each lead's answers keyed by the form's question labels (and by email / phone / full_name), the form id, the campaign / ad group / ad ids, submittedAt. ChatGPT Ads has no endpoint that LISTS leads — they exist here only after set_openai_ads_lead_sync was called WITHOUT a destinationUrl, and only from then on. Synthetic test submissions are hidden unless includeSynthetic. These are real people's contact details. Read-only, free.
| Parameter | Type | Description |
|---|
formId | string | return only leads from this lead form id |
since | string | ISO date |
limit | number | default 100, max 1000 |
includeSynthetic | boolean | true = include synthetic test leads, which are hidden by default |
delete_openai_ads_lead_sync#
Delete a ChatGPT Ads lead-sync subscription destructive
Delete the lead-sync subscription: every lead form on the account stops delivering leads. confirm:true required.
| Parameter | Type | Description |
|---|
subscriptionIdrequired | string | lead-sync subscription id, from the subscriptions list of list_openai_ads_leads |
confirm | boolean | true to delete; every lead form on the account stops delivering leads |
list_openai_ads_business_agents#
List ChatGPT Ads Business Agents read-only
The BUSINESS AGENTS on this ChatGPT Ads account — a Business Agent is a branded assistant ChatGPT serves inside an ad (mode business_agent on the campaign): instructions, conversation starters, optional product feeds, tools and a lead form. Shows draft/published status and pending changes. Read-only, 0 credits.
No parameters.
list_openai_ads_business_agent_tools#
List tools a Business Agent may use read-only
The eligible TOOLS installed for this ChatGPT Ads account that a Business Agent can be given (pass their ids as toolIds to create_openai_ads_business_agent). Read-only, 0 credits.
No parameters.
create_openai_ads_business_agent#
Create or replace a ChatGPT Ads Business Agent destructive
Draft a ChatGPT Ads BUSINESS AGENT: name, instructions (the system prompt real users will experience — write it as the brand), optional description, privacy policy URL, conversation starters, product feed ids, connector ids, tool ids (list_openai_ads_business_agent_tools) and a published lead form id. Pass businessAgentId to REPLACE an existing agent’s configuration (it becomes a pending change). Nothing is served until publish_openai_ads_business_agent. Needs ChatGPT Ads connected.
| Parameter | Type | Description |
|---|
businessAgentId | string | replace THIS agent’s configuration instead of creating a new one |
namerequired | string | business agent name |
instructionsrequired | string | system prompt that users experience, written in the brand's voice |
description | string | optional description of the agent |
privacyPolicyUrl | string | privacy policy URL shown with the agent |
conversationStarters | string[] | suggested opening questions shown to users |
productFeedIds | string[] | product feed ids the agent may draw on, from list_openai_ads_feeds |
connectorIds | string[] | connector ids the agent may use |
toolIds | string[] | tool ids the agent may use, from list_openai_ads_business_agent_tools |
leadFormId | string | published lead form id the agent collects leads with |
get_openai_ads_business_agent#
Read a ChatGPT Ads Business Agent read-only
One Business Agent with its full configuration and publish state. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
businessAgentIdrequired | string | business agent id |
preview_openai_ads_business_agent#
Chat with a Business Agent (preview) read-only
Send a message (or a whole messages[] transcript) to a ChatGPT Ads Business Agent and get its reply — a PREVIEW that publishes and serves nothing. Use it to test instructions before publish_openai_ads_business_agent.
| Parameter | Type | Description |
|---|
businessAgentIdrequired | string | business agent id to chat with |
message | string | one user message to send; use messages for a full transcript |
messages | object[] | full transcript as [{role, content}], in place of message |
publish_openai_ads_business_agent#
Publish a ChatGPT Ads Business Agent destructive
Publish a Business Agent’s draft so every campaign referencing it serves this version to real users. confirm:true required; show the user the instructions first.
| Parameter | Type | Description |
|---|
businessAgentIdrequired | string | business agent id to publish |
confirm | boolean | true to publish; campaigns serve this version to real users |
list_openai_ads_spend_windows#
List ChatGPT Ads account spending limits read-only
Account-level SPENDING LIMITS on ChatGPT Ads, both kinds: the date-range SPEND-LIMIT WINDOWS (a ceiling on what the whole account may spend between an inclusive start date and an exclusive end date, with amount and spent so far) and the DAILY LIMIT (a per-day ceiling that renews at midnight in the account timezone, with what is spent and left today). Also returns the configuration `revision` that set_openai_ads_daily_spend_limit and remove_openai_ads_daily_spend_limit need as expectedRevision, and the earliest date a new daily limit may start. A limit caps spend; it never makes anything spend. OpenAI serves account spending limits only to ad accounts on postpaid INVOICE billing, and every spending-limit request, reads included, needs billing-management permission: a card-billed account is refused (a 403 that says only admins can do it) from any key, including one its own admin created. Read-only, 0 credits.
No parameters.
set_openai_ads_spend_window#
Create or edit a ChatGPT Ads spend-limit window destructive
Create (no windowId) or edit (windowId) an account-level spend-limit window on ChatGPT Ads: startDate YYYY-MM-DD inclusive, endDate YYYY-MM-DD EXCLUSIVE (the window ends the day before), amount in the account currency (sent as micros), optional name and insertion-order id (ioId). A window is a CEILING on what the whole account may spend in that range; it never makes anything spend, and campaign budgets still apply underneath it. On edit, pass only the fields that change; a window whose canEdit is false (already started, per OpenAI) cannot be edited. Raising the amount lets campaigns spend up to their budgets, so show the user the old and new amounts. Reply carries the stored window (amount, spent so far, status). The live API serves this path since at least 2026-09-12 (it answered "Invalid URL" on 2026-09-03). It needs postpaid INVOICE billing and billing-management permission; a card-billed account is refused whatever key is used.
| Parameter | Type | Description |
|---|
windowId | string | spend-limit window id to edit; omit to create a new window |
startDate | string | YYYY-MM-DD, first day of the window (inclusive) |
endDate | string | YYYY-MM-DD, end of the window (exclusive; the window ends the day before) |
amount | number | spend ceiling for the whole account over the window, in the account currency |
name | string | window name |
ioId | string | insertion-order id, when billing uses one |
delete_openai_ads_spend_window#
Delete a ChatGPT Ads spend-limit window destructive
Delete an active or scheduled spend-limit window before it ends — this REMOVES a spending ceiling. confirm:true required.
| Parameter | Type | Description |
|---|
windowIdrequired | string | spend-limit window id to delete; removes that spending ceiling |
confirm | boolean | true to delete the window |
set_openai_ads_daily_spend_limit#
Set the ChatGPT Ads account daily spending limit destructive
Create or change the ACCOUNT-WIDE DAILY SPENDING LIMIT on ChatGPT Ads: the most all campaigns together may spend each day, renewing at midnight in the account timezone, until it is removed or reaches its optional end date. OpenAI offers it only on ad accounts with postpaid INVOICE billing and it needs billing-management permission; a card-billed account is refused whatever key is used. amount is per day in the account currency and is required even when only the end date changes (0 stops account spend). A NEW limit starts tomorrow or later: list_openai_ads_spend_windows names the earliest start date. On an existing limit leave startDate out, because its start cannot change. An account can hold SEVERAL non-overlapping daily rules (list_openai_ads_spend_windows returns each with a windowId): pass windowId to change one, or newRule:true (with startDate) to add another; with more than one rule OpenAI requires the windowId. endDate is exclusive; noEndDate:true clears it. A daily limit and a date-range window cannot overlap. Lowering it can stop delivery and raising it lets campaigns spend up to their own budgets, so show the user the current and new values first: without confirm:true AND expectedRevision (the revision list_openai_ads_spend_windows returns) nothing changes and the reply states what would. The result is read back from ChatGPT Ads.
| Parameter | Type | Description |
|---|
amountrequired | number | daily limit in the account currency; 0 stops all account spend |
windowId | string | the daily rule to change, from list_openai_ads_spend_windows; required when the account has more than one |
newRule | boolean | true adds another non-overlapping daily rule instead of editing one |
expectedRevision | number | the revision from list_openai_ads_spend_windows |
startDate | string | YYYY-MM-DD, first day of a NEW daily limit (tomorrow or later) |
endDate | string | YYYY-MM-DD, exclusive |
noEndDate | boolean | true removes the end date |
confirm | boolean | true applies the change; needs expectedRevision as well |
remove_openai_ads_daily_spend_limit#
Remove the ChatGPT Ads account daily spending limit destructive
Remove the account-wide DAILY spending limit on ChatGPT Ads. This takes a spending CEILING off the whole account, so campaigns may then spend up to their own budgets every day; date-range spend-limit windows are not touched. Like every account spending-limit request it needs postpaid INVOICE billing and billing-management permission; a card-billed account is refused whatever key is used. Without confirm:true AND expectedRevision (the revision list_openai_ads_spend_windows returns) nothing changes and the reply names the limit that would be removed. With several daily rules, pass windowId to name the one to remove (OpenAI requires it then). The result is read back from ChatGPT Ads.
| Parameter | Type | Description |
|---|
windowId | string | the daily rule to remove; required when the account has more than one |
expectedRevision | number | the revision from list_openai_ads_spend_windows |
confirm | boolean | true removes the limit; needs expectedRevision as well |
set_openai_ads_negative_keywords#
Set ChatGPT Ads account negative keywords
CURRENTLY UNAVAILABLE: OpenAI no longer serves account-level negative keywords. The live API answers "Invalid URL" (measured 2026-09-13) and the endpoint left its published ChatGPT Ads spec the same week, so nothing can be set through Hermoso or any other API client. Tell the user that plainly instead of retrying. When it did work it REPLACED the whole account list, and the API never offered a way to read the current one.
| Parameter | Type | Description |
|---|
keywordsrequired | string[] | negative keywords; currently unavailable because OpenAI no longer serves this endpoint |
list_openai_ads_feeds#
List ChatGPT Ads product feeds read-only
The PRODUCT FEEDS on this ChatGPT Ads account (id, name, countries, currencies, the count of ingested products eligible for ads, and campaign count). A product-feed campaign (mode product_feed) advertises products from one of these; update_openai_ads_feed_products fills one. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
limit | number | max feeds to return |
after | string | pagination cursor from the previous page |
create_openai_ads_feed#
Create a ChatGPT Ads product feed
Create an EMPTY product feed on ChatGPT Ads with a name and the countries it serves (ISO codes). Fill it with update_openai_ads_feed_products, or set up SFTP bulk delivery with set_openai_ads_feed_sftp. inventoryType:"hotel" creates a HOTEL property feed instead (schema hotel_property_v1, filled with a full property snapshot over SFTP; an OpenAI limited beta for approved pilot advertisers, so an account outside the pilot is refused by OpenAI).
| Parameter | Type | Description |
|---|
namerequired | string | product feed name |
countries | string[] | ISO country codes the feed serves, e.g. US |
inventoryType | "product" | "hotel" | default product; hotel = a Hotel property feed (limited beta) |
list_openai_ads_feed_uploads#
List ChatGPT Ads feed uploads read-only
Recent product-feed UPLOADS across the account with per-upload status, rows accepted / rejected / ads-eligible and OpenAI’s DIAGNOSTICS (code, severity, field and rows affected, e.g. invalid_value on price) — the way to learn why products are not serving. Per-product review status (why ONE product was rejected) is shown only in ChatGPT Ads Manager > Products: OpenAI does not expose it through the API, so say that rather than guessing. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
limit | number | max uploads to return |
archive_openai_ads_feed#
Archive a ChatGPT Ads product feed destructive
Archive a product feed — its products leave every campaign using it. confirm:true required.
| Parameter | Type | Description |
|---|
feedIdrequired | string | product feed id to archive |
confirm | boolean | true to archive; its products leave every campaign that uses it |
query_openai_ads_feed_products#
Query products in a ChatGPT Ads feed read-only
List the products in a feed that match filters ({field, operator in|not_in|gt|gte|lt|lte|contains|not_contains|starts_with, values[]}) — the SAME filter shape an ad group’s productSet takes, so this previews exactly which products that ad group would advertise. Each product carries its price and filter values. It does not carry a per-product review status: that is only in ChatGPT Ads Manager > Products. Paginate with after. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
feedIdrequired | string | product feed id to query |
filters | object[] | product filters; same shape as an ad group's productSet |
limit | number | max products per page |
after | string | pagination cursor from the previous page |
get_openai_ads_feed_sftp#
Read a feed’s SFTP delivery settings read-only
Whether SFTP bulk delivery is enabled for a ChatGPT Ads product feed, its connection URI and authentication method. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
feedIdrequired | string | product feed id |
set_openai_ads_feed_sftp#
Set up, activate or pause feed SFTP delivery destructive
Manage SFTP bulk delivery for a ChatGPT Ads product feed (the alternative to pushing products through update_openai_ads_feed_products). action "create" (default): create or REPLACE the feed's SFTP credentials — authenticationMethod "password" (OpenAI returns the password ONCE in this reply; store it) or "ssh_key" (pass the public key as sshPublicKey). Replacing credentials invalidates the previous ones immediately, so re-run only when the user means to rotate. action "activate" / "pause": switch delivery on or off without touching credentials. The reply carries the connection URI and whether delivery is enabled; get_openai_ads_feed_sftp reads the same state without changing it. Files delivered over SFTP show up in list_openai_ads_feed_uploads with accepted / rejected / ads-eligible row counts.
| Parameter | Type | Description |
|---|
feedIdrequired | string | product feed id |
action | "create" | "activate" | "pause" | create (default) sets up or rotates credentials; activate or pause switches delivery only |
authenticationMethod | "password" | "ssh_key" | password (returned once in the reply) or ssh_key |
sshPublicKey | string | SSH public key, used with authenticationMethod ssh_key |
get_openai_ads_feed_settings#
Read a ChatGPT Ads feed’s settings read-only
A product feed’s name, countries and whether its organic ChatGPT search listing respects each product’s is_eligible_search flag. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
feedIdrequired | string | product feed id |
update_openai_ads_feed_settings#
Change a ChatGPT Ads feed’s name or organic-listing setting destructive
Rename a product feed and/or set searchEligibleRespected (its organic ChatGPT search listing follows each product’s is_eligible_search flag). Touches no ads or spend; the reply is what OpenAI stored.
| Parameter | Type | Description |
|---|
feedIdrequired | string | product feed id to edit |
name | string | new feed name |
searchEligibleRespected | boolean | true = the organic ChatGPT listing follows each product's is_eligible_search flag |
list_openai_ads_audit_logs#
ChatGPT Ads change history (audit log) read-only
Who changed what on the ChatGPT Ads account and when: each create/update with its actor and every changed field’s before and after value. Read it first when a campaign starts or stops spending unexpectedly. Filter by campaignId (includes its ad groups and ads), adGroupId, adId, actorId, since/until (date, ISO time or unix seconds). Read-only, 0 credits.
| Parameter | Type | Description |
|---|
campaignId | string | include changes to this campaign and its ad groups and ads |
adGroupId | string | only changes to this ad group |
adId | string | only changes to this ad |
actorId | string | only changes made by this actor id |
since | string | start of the window: date, ISO time or unix seconds |
until | string | end of the window: date, ISO time or unix seconds |
order | "asc" | "desc" | asc or desc, by time |
limit | number | 1-100, default 25 |
after | string | pagination cursor for the next page |
before | string | pagination cursor for the previous page |
get_openai_ads_landing_page_crawl#
What ChatGPT Ads’ crawler saw on an ad’s landing page read-only
The saved evidence from OpenAI’s landing-page crawler for an ad’s submitted version: crawl status and failure code, the requested and final URL (a redirect shows here), capture time, whether robots.txt allowed the crawler and its user agent, and a screenshot of what it saw. Read it when an ad is rejected or reports a landing-page crawl serving issue. The screenshot link expires within 24 hours. available:false with pending:true means the capture is still running; available:false otherwise means OpenAI holds no capture (or the ad id is not on this account). The status is the crawl job’s, not the page’s HTTP status. Reading never starts a crawl. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
adIdrequired | string | the ad, e.g. ad_… from list_openai_ads_campaigns with an adGroupId |
openai_ads_hotel_report#
ChatGPT Ads Hotel Insights (per property) read-only
Delivery per HOTEL PROPERTY for a ChatGPT Ads Hotel feed: impressions, clicks, spend, CTR, CPC and CPM per property (hotel id, name, brand, city, star rating), optionally per day or month and broken down by campaign, ad group or ad; timeseries:true returns daily totals across the selected properties instead. Filter by campaignIds / adGroupIds / adIds, exact properties [{feedId, hotelId}], or filters on hotel_name / hotel_brand / hotel_category / hotel_city / hotel_region / hotel_country_code (IN) and impressions / clicks / spend / ctr / cpc / cpm (GREATER_THAN / LESS_THAN). OpenAI’s rules, said in every reply: impressions and clicks mix settled and estimated data, spend and cost rates are settled only, and the coverage block decides whether an empty or short answer is real. No conversions and no attribution windows exist here. Hotel Insights is an OpenAI limited beta for accounts enrolled in Hotel reporting; others are refused by OpenAI. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
sincerequired | string | YYYY-MM-DD, account timezone |
untilrequired | string | YYYY-MM-DD, account timezone, no later than today |
timeseries | boolean | true = daily totals across the selected properties |
granularity | "none" | "daily" | "monthly" | none (one total per property), daily or monthly rows |
breakdown | "campaign" | "ad_group" | "ad" | expand each property into hierarchy rows; each row repeats its property total under `property` |
campaignIds | string[] | return only these campaign ids |
adGroupIds | string[] | return only these ad group ids |
adIds | string[] | return only these ad ids |
properties | object[] | exact feed/property pairs |
filters | object[] | filters on hotel attributes or metrics |
includeZeroActivity | boolean | also list active properties with no delivery |
sortField | "impressions" | "clicks" | "spend" | "ctr" | "cpc" | "cpm" | metric to sort rows by |
sortDirection | "asc" | "desc" | asc or desc |
limit | number | property/time-bucket groups per page, up to 2000 |
after | string | pagination cursor for the next page |
includeTotalCount | boolean | true to return the total row count |
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 profile — do this first to pick a target. Call WITH adAccountId to list that account’s campaigns (id, name, status, objective, budget, and Pinterest’s own summary status). Add campaignId to get that ONE campaign’s whole tree — its AD GROUPS and ADS with their ids, statuses and review status. That is the only way to enumerate them, and it matters: Pinterest has no delete, so an ad group you cannot see is one you cannot even archive. 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 (or call list_connector_accounts with provider pinterest_ads, then set_connector_accounts).
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to list the ad accounts shared with this profile |
campaignId | string | one campaign -> its ad groups and ads too (the only way to enumerate them) |
statuses | "ACTIVE" | "PAUSED" | "ARCHIVED" | "DRAFT"[] | filter by ACTIVE, PAUSED, ARCHIVED or DRAFT; omit to include all four |
pinterest_ads_report#
Pinterest ads performance report read-only
Performance for a Pinterest ad account — spend, impressions, clicks, CTR, effective CPC and conversions — at ANY of Pinterest’s four levels: the whole ACCOUNT, by CAMPAIGN, by AD GROUP, or by individual AD. Ad level is how you answer "WHICH AD IS WINNING". EVERY SUB-ACCOUNT LEVEL NEEDS ITS IDS — this was measured, not read: Pinterest refuses campaign level without campaignIds, ad-group level without adGroupIds, AND ad level without adIds ("Either ads id filter or both pin id and campaign id filters must be specified"), so there is NO account-wide per-ad call. Get the ids from list_pinterest_ads_campaigns (name a campaignId and it returns that campaign’s ad groups and ads), or use level:"account" for a whole-account total with no ids at all. At AD level Pinterest publishes one alternative its own refusal names: pinIds AND campaignIds TOGETHER, which reports every ad promoting those Pins — half of that pair is refused here naming the missing half. The level is inferred from whichever ids you pass, so naming campaignIds still reports by campaign. Default columns lead with that level’s OWN id and name, because a report whose rows cannot be told apart answers nothing. 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id; omit a list call to see the shared accounts |
level | "account" | "campaign" | "adGroup" | "ad" | which level to report at — omit and it is inferred from the ids you pass (none -> the whole account) |
campaignIds | string[] | REQUIRED for campaign level; also the second half of the ad-level pinIds pair |
adGroupIds | string[] | REQUIRED for ad-group level — Pinterest has no all-of-them form there |
adIds | string[] | REQUIRED for ad level — Pinterest refuses /ads/analytics without it, unless you pass pinIds AND campaignIds instead |
pinIds | string[] | ad level only, and only TOGETHER with campaignIds — every ad promoting these Pins |
since | string | YYYY-MM-DD, default 30 days ago |
until | string | YYYY-MM-DD, default today |
granularity | "TOTAL" | "DAY" | "HOUR" | "WEEK" | "MONTH" | default TOTAL |
columns | string[] | Pinterest metric column names — omit for the standard set for that level |
create_pinterest_ads_campaign#
Build a Pinterest ad campaign (paused) destructive
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.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id, from list_pinterest_ads_campaigns |
namerequired | string | campaign 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 |
dailyBudget | number | daily cap in the ad account’s currency |
lifetimeBudget | number | lifetime cap instead of a daily one — Pinterest then requires endTime |
isPerformancePlus | boolean | Pinterest Performance+ on this campaign; with no lifetime budget Pinterest also requires isFlexibleDailyBudgets:true |
isAutomatedCampaign | boolean | Pinterest automated campaign |
isTopOfSearch | boolean | top-of-search campaign |
isFlexibleDailyBudgets | boolean | flexible daily budgets |
isLtvOptimized | boolean | optimize for long-term value |
isCampaignBudgetOptimization | boolean | default true at Pinterest: the budget sits on the campaign |
intendedPromotionType | "LEGACY" | "STANDARD_AD" | "CATALOG" | the kind of ads it will carry |
bidOptions | object | Pinterest bid_options object: multipliers by age, gender, placement, audience or frequency |
startTime | number | Unix timestamp in SECONDS |
endTime | number | Unix timestamp in SECONDS |
adGroup | object | build 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 destructive
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.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id, from list_pinterest_ads_campaigns |
campaignIdrequired | string | the campaign this ad group belongs to |
namerequired | string | ad group name |
billableEvent | "CLICKTHROUGH" | "IMPRESSION" | "VIDEO_V_50_MRC" | LEAVE THIS OUT unless you know better — Pinterest ties it to the campaign objective and refuses a mismatch: CONSIDERATION takes CLICKTHROUGH; AWARENESS, SALES, LEADS, WEB_CONVERSION, VIDEO_COMPLETION and APP_INSTALL take IMPRESSION; CATALOG_SALES takes either. Omitted -> the right one for the objective is used. |
bid | number | REQUIRED — what you pay per billable event, in the ad account’s currency. Pinterest rejects an ad group without one and Hermoso will not invent a bid. It must also be BELOW the campaign budget and above Pinterest’s own bid floor for the placement, both of which Pinterest states in its refusal. |
budget | number | ad-group budget — only valid when the campaign is NOT budget-optimized (Pinterest optimizes at campaign level by default) |
placementGroup | "ALL" | "SEARCH" | "BROWSE" | "OTHER" | where the ads run: ALL, SEARCH, BROWSE or OTHER |
pacing | "STANDARD" | "ACCELERATED" | delivery speed: STANDARD or ACCELERATED |
targetingSpec | object | Pinterest targeting object, e.g. {"GEO":["US"],"AGE_BUCKET":["25-34","35-44"]} — at least one GEO or LOCATION is REQUIRED by Pinterest. Age: use AGE_BUCKET, or MINIMUM_AGE and MAXIMUM_AGE TOGETHER (18–65, with "65+" allowed as the maximum) — a minimum on its own is refused. |
status | "ACTIVE" | "PAUSED" | "DRAFT" | default PAUSED |
ad | object | create the ad in the same call |
confirm | boolean | REQUIRED true to create this ACTIVE under a LIVE campaign |
create_pinterest_ads_ad#
Create a Pinterest ad destructive
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.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id, from list_pinterest_ads_campaigns |
adGroupIdrequired | string | the ad group this ad lives in |
pinIdrequired | string | the 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 |
name | string | internal ad name |
destinationUrl | string | where the click goes |
utm | object | UTM tags merged into destinationUrl as utm_source / utm_medium / utm_campaign / utm_term / utm_content — Pinterest has no UTM field of its own. Other query parameters and Pinterest macros like {campaignid} are kept |
status | "ACTIVE" | "PAUSED" | "DRAFT" | default PAUSED |
confirm | boolean | REQUIRED true to create this ACTIVE in a live ad group |
update_pinterest_ads_ad_group#
Edit a Pinterest ad group destructive
EDIT an existing Pinterest ad group in place — its name, bid, budget, pacing, placement and, above all, its TARGETING. Before this, targeting was frozen the moment an ad group was created: a mistyped bid or a missing country meant building the whole tree again, and because PINTEREST HAS NO DELETE every correction left a permanent archived shell behind. This is a PARTIAL edit — a field you do not name is left exactly as Pinterest has it, so send only what changes, and never re-send everything you just read (that would overwrite a concurrent edit). targetingSpec is the exception: it REPLACES the whole targeting object, so include every criterion you still want. IT DOES NOT CHANGE STATUS — set_pinterest_ads_status owns ACTIVE / PAUSED / ARCHIVED, and passing status here is refused by name. Changing the bid, the budget, or which campaign it spends from WHILE IT IS LIVE moves real money on the next auction: show the user the exact new value, get an explicit yes, then pass confirm:true. Pinterest refuses targetingTemplateIds alongside targetingSpec / trackingUrls / autoTargeting / placementGroup, which is refused here naming the conflicting pair. The ad group is READ BACK from Pinterest afterwards and the note describes what Pinterest actually stored — print it verbatim. Free.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id, from list_pinterest_ads_campaigns |
adGroupIdrequired | string | the ad group to edit |
status | "ACTIVE" | "PAUSED" | "ARCHIVED" | "DRAFT" | REFUSED HERE ON PURPOSE — use set_pinterest_ads_status, which confirm-gates real spend and Pinterest’s archive-as-delete |
name | string | new ad group name |
bid | number | what you pay per billable event, in the ad account’s currency |
budget | number | only on a campaign that is NOT budget-optimized |
budgetType | "DAILY" | "LIFETIME" | "CBO_ADGROUP" | budget mode: DAILY, LIFETIME or CBO_ADGROUP |
billableEvent | "CLICKTHROUGH" | "IMPRESSION" | "VIDEO_V_50_MRC" | Pinterest ties this to the campaign objective and refuses a mismatch |
placementGroup | "ALL" | "SEARCH" | "BROWSE" | "OTHER" | where the ads run: ALL, SEARCH, BROWSE or OTHER |
pacing | "STANDARD" | "ACCELERATED" | delivery speed: STANDARD or ACCELERATED |
autoTargeting | boolean | true or false: Pinterest automatic targeting |
targetingSpec | object | REPLACES the whole targeting spec — get ids from search_pinterest_ads_targeting, never guess one |
trackingUrls | object | Pinterest tracking URL object for the ad group |
targetingTemplateIds | string[] | Pinterest refuses these alongside targetingSpec / trackingUrls / autoTargeting / placementGroup |
optimizationGoalMetadata | object | Pinterest optimization goal metadata object |
startTime | number | Unix timestamp in SECONDS |
endTime | number | Unix timestamp in SECONDS |
lifetimeFrequencyCap | number | CPM (IMPRESSION-billed) ad groups only, and Pinterest requires endTime with it |
campaignId | string | MOVE the ad group into a different campaign — it then spends from that budget |
confirm | boolean | REQUIRED true to change bid / budget / campaign on a LIVE ad group |
update_pinterest_ads_ad#
Edit a Pinterest ad destructive
EDIT an existing Pinterest ad in place — its name, DESTINATION URL, creative type, tracking URLs, deep links, lead form, or which ad group it sits in. A PARTIAL edit: a field you do not name is left exactly as Pinterest has it. THE PIN CANNOT BE SWAPPED ON A REAL AD — Pinterest documents pinId as updatable "only for draft ads", so to promote a different Pin create a new ad in the same ad group with create_pinterest_ads_ad and archive this one; that is refused up front with the way through rather than after Pinterest rejects it. IT DOES NOT CHANGE STATUS — set_pinterest_ads_status owns ACTIVE / PAUSED / ARCHIVED, and passing status here is refused by name. EDITING AN AD SENDS IT BACK THROUGH PINTEREST’S REVIEW: the read-back reports the review status and says so when it moved, and an ad in review is NOT serving — never report an edited ad as live on the strength of the edit succeeding. Free.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id, from list_pinterest_ads_campaigns |
adIdrequired | string | the ad to edit |
status | "ACTIVE" | "PAUSED" | "ARCHIVED" | "DRAFT" | REFUSED HERE ON PURPOSE — use set_pinterest_ads_status, which confirm-gates real spend and Pinterest’s archive-as-delete |
name | string | new ad name |
destinationUrl | string | where the click goes |
utm | object | UTM tags merged into destinationUrl (or, if you omit destinationUrl, into the ad’s CURRENT destination) as utm_source / utm_medium / utm_campaign / utm_term / utm_content — Pinterest has no UTM field of its own. Other query parameters and Pinterest macros like {campaignid} are kept |
creativeType | "REGULAR" | "VIDEO" | "SHOPPING" | "CAROUSEL" | "MAX_VIDEO" | "COLLECTION" | "IDEA" | "SHOWCASE" | "QUIZ" | "COLLAGE" | "APP" | creative format, e.g. REGULAR, VIDEO, CAROUSEL or APP |
adGroupId | string | move the ad into a different ad group |
trackingUrls | object | Pinterest tracking URL object for the ad |
clickTrackingUrl | string | click-through tracking URL |
viewTrackingUrl | string | view-through tracking URL |
leadFormId | string | Pinterest lead form id for a lead ad |
iosDeepLink | string | iOS deep link for an app ad |
androidDeepLink | string | Android deep link for an app ad |
carouselDestinationUrls | string[] | one destination URL per carousel card, in card order |
pinId | string | DRAFT ads only — Pinterest refuses it on any other status |
set_pinterest_ads_budget#
Set a Pinterest campaign budget destructive
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.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id, from list_pinterest_ads_campaigns |
campaignIdrequired | string | the campaign whose budget changes |
dailyBudget | number | daily cap in the ad account currency; use one budget type per campaign |
lifetimeBudget | number | lifetime cap in the ad account currency; use instead of dailyBudget |
confirm | boolean | REQUIRED 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id, from list_pinterest_ads_campaigns |
level | "campaign" | "adGroup" | "ad" | what to change — default campaign |
campaignId | string | campaign id to change; use with level campaign |
adGroupId | string | ad group id to change; use with level adGroup |
adId | string | ad id to change; use with level ad |
statusrequired | "ACTIVE" | "PAUSED" | "ARCHIVED" | "DRAFT" | ACTIVE = start spending; PAUSED = stop; ARCHIVED = retire (Pinterest’s delete) |
confirm | boolean | REQUIRED true for ACTIVE (real spend) or ARCHIVED (irreversible retirement) |
confirmName | string | when archiving or removing: its EXACT name, as the unconfirmed call names it. Required when it has children or is live |
confirmChildren | number | when archiving or removing: the exact number of children the unconfirmed call reported. Required when it has any |
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.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
level | "campaign" | "adGroup" | "ad" | what to archive — default campaign |
campaignId | string | campaign id to archive; used with level:"campaign" |
adGroupId | string | ad group id to archive; used with level:"adGroup" |
adId | string | ad id to archive; used with level:"ad" |
confirm | boolean | REQUIRED true — archiving is not self-service reversible |
confirmName | string | the object’s EXACT name, required when it has children or is live |
confirmChildren | number | the exact number of children reported by the unconfirmed call, required when it has any |
list_pinterest_ads_conversion_tags#
List Pinterest conversion tags read-only
Read the brand’s Pinterest CONVERSION TAGS — the Pinterest tag a WEB_CONVERSION or SALES campaign optimises on. view:"all" (default) lists every tag with status, when it last fired (a tag that has NEVER fired is not installed) and its code snippet; conversionTagId reads one tag in full; view:"ocpm_eligible" lists the (tag, event) pairs a conversion ad group can OPTIMISE for — put one in create_pinterest_ads_ad_group’s optimizationGoalMetadata as {"conversion_tag_v3_goal_metadata":{"conversion_event":…,"conversion_tag_id":…}}; view:"page_visit" lists page-visit events. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
conversionTagId | string | read one tag in full — code snippet and enhanced-match config |
view | "all" | "ocpm_eligible" | "page_visit" | all (default) lists tags; ocpm_eligible lists (tag, event) goals; page_visit lists page-visit events |
includeDeleted | boolean | true also includes deleted tags in the list |
create_pinterest_ads_conversion_tag#
Create a Pinterest conversion tag
Create a Pinterest conversion TAG on the ad account — the base code a website installs so Pinterest sees page visits, sign-ups and checkouts, and a WEB_CONVERSION / SALES campaign has something to optimise for. Returns the tag id and the code snippet to paste into every page’s <head>, READ BACK from Pinterest. enhancedMatch:true (or a per-field object) turns on Pinterest’s Automatic Enhanced Match. Spends nothing, 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
namerequired | string | tag name, as shown in Pinterest Ads Manager |
enhancedMatch | boolean | object | true enables all Automatic Enhanced Match fields; or an object to enable individual fields |
send_pinterest_ads_conversions#
Send conversions to Pinterest (Conversions API) destructive
Send conversions to Pinterest server-side — purchases, sign-ups, leads, add-to-carts or any of Pinterest’s 22 events — so Pinterest attributes them to the ads that caused them and conversion campaigns can optimise. Pass ordinary emails, phones and names: they are normalised Pinterest’s way and SHA-256 hashed on our server before they reach Pinterest. Every event needs something to match on: an email, a mobile ad id (maid), or BOTH ipAddress and userAgent — refused here otherwise. Pass eventId (your order or event id) if the site also runs the Pinterest tag, or the same conversion is counted twice. test:true makes Pinterest validate the events and record NOTHING — use it first. The reply counts what Pinterest PROCESSED, not what was sent, and names every failed event. Spends nothing, 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
test | boolean | validate only — Pinterest records nothing |
eventsrequired | object[] | up to 1,000 events |
list_pinterest_ads_audiences#
List Pinterest audiences and customer lists read-only
List the Pinterest AUDIENCES on the ad account (what:"audiences", default) or its CUSTOMER LISTS (what:"customer_lists"), or read one by audienceId / customerListId. An audience id is what an ad group’s targetingSpec takes as {"AUDIENCE_INCLUDE":[id]} (retarget) or AUDIENCE_EXCLUDE (suppress). READY delivers; TOO_SMALL (under 100 people) never does. ownership:"RECEIVED" lists audiences shared with this account. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
what | "audiences" | "customer_lists" | audiences (default) or customer_lists |
audienceId | string | read one audience in full by its id |
customerListId | string | read one customer list in full by its id |
ownership | "OWNED" | "RECEIVED" | OWNED or RECEIVED; RECEIVED lists audiences shared with this account |
create_pinterest_ads_audience#
Create a Pinterest audience
Create a Pinterest AUDIENCE to retarget or suppress. audienceType: VISITOR (people who fired your Pinterest tag — conversionTagIds, optional event, retentionDays 1–540), ENGAGEMENT (people who engaged with your Pins — engagementDomains REQUIRED, your claimed domain; optional engagementType click/save/closeup/comment/like), ACTALIKE (people similar to seedAudienceIds — country US, CA or GB, percentage 1–10) or CUSTOMER_LIST (customerListId, OR pass emails / maids and the list is uploaded — hashed on our server — and the audience made in one call). Every range Pinterest documents is checked before anything is sent. READ BACK from Pinterest; a new audience starts INITIALIZING. Spends nothing, 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
namerequired | string | audience name |
audienceTyperequired | "VISITOR" | "ENGAGEMENT" | "ACTALIKE" | "CUSTOMER_LIST" | "PERSONA" | audience kind; decides which other fields are required (see tool description) |
description | string | optional note describing the audience |
conversionTagIds | string[] | VISITOR: the tag(s) whose visitors to collect |
event | string | VISITOR: pagevisit, signup, checkout, viewcategory, search, addtocart, watchvideo, lead, custom (or a partner-defined tag event) |
retentionDays | number | VISITOR / ENGAGEMENT: 1–540, Pinterest default 180 |
eventSources | "web" | "mobile" | "offline"[] | web, mobile and/or offline: where the audience's events come from |
ingestionSources | "tag" | "mmp" | "file_upload" | "conversions_api"[] | tag, mmp, file_upload and/or conversions_api: how events reach Pinterest |
engagementDomains | string[] | ENGAGEMENT: REQUIRED — your claimed website domain |
engagementType | "click" | "save" | "closeup" | "comment" | "like" | click, save, closeup, comment or like; omit to include all engagements |
engagerType | number | ENGAGEMENT: 1 or 2 |
pinIds | string[] | ENGAGEMENT: only people who engaged with these Pins |
seedAudienceIds | string[] | ACTALIKE: the audience(s) to find people similar to |
country | "US" | "CA" | "GB" | ACTALIKE |
percentage | number | ACTALIKE: 1 (most similar) to 10 (widest) |
customerListId | string | CUSTOMER_LIST: id of an existing customer list to target |
emails | string[] | CUSTOMER_LIST: upload these as a new list (hashed here) and make the audience in one call |
maids | string[] | CUSTOMER_LIST: mobile ad ids to upload as a new list, hashed on our server |
rule | object | raw Pinterest rule fields, merged last — expert use |
update_pinterest_ads_audience#
Edit or remove a Pinterest audience destructive
Rename a Pinterest audience, change its description or rule, or REMOVE it (remove:true). Removing is destructive — any ad group that includes or excludes it loses that targeting and Pinterest has no undo — so it needs confirm:true; without it nothing changes and the reply names what would go. Read back from Pinterest afterwards. 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
audienceIdrequired | string | id of the audience to rename, edit or remove |
name | string | new audience name |
description | string | new audience description |
rule | object | raw Pinterest rule fields merged onto the audience's existing rule (expert use) |
remove | boolean | true removes the audience; requires confirm:true |
confirm | boolean | true required together with remove:true |
create_pinterest_ads_customer_list#
Upload a Pinterest customer list
Upload a Pinterest CUSTOMER LIST from emails (or mobile ad ids) — buyers, subscribers or leads — and, by default, make the CUSTOMER_LIST audience that targets it in the same call. Every address is SHA-256 hashed on our server; no plain address reaches Pinterest. Pinterest must find at least 100 of them on Pinterest or the list ends TOO_SMALL and cannot deliver. READ BACK from Pinterest. 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
namerequired | string | customer list name |
emails | string[] | email addresses to upload; hashed on our server before sending |
maids | string[] | mobile ad ids to upload; hashed on our server before sending |
listType | "EMAIL" | "MAID" | "IDFA" | IDFA or MAID for mobile ids; ignored when emails are sent (list becomes EMAIL) |
createAudience | boolean | default true |
audienceName | string | name of the CUSTOMER_LIST audience made from this list (when createAudience is on) |
update_pinterest_ads_customer_list#
Add or remove people on a Pinterest customer list destructive
ADD people to (or REMOVE people from) an existing Pinterest customer list by email or mobile ad id — hashed on our server before they leave it. The list’s counts are read back before and after. Pinterest matches in the background, so the audience size can take hours to move. 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
customerListIdrequired | string | id of the customer list to change |
operation | "ADD" | "REMOVE" | default ADD |
emails | string[] | email addresses to add or remove (see operation); hashed on our server |
maids | string[] | mobile ad ids to add or remove (see operation); hashed on our server |
list_pinterest_ads_keywords#
List Pinterest ad keywords read-only
Read the KEYWORDS on a Pinterest ad group or campaign: the searches a SEARCH placement serves on, each with its match type. Pass adGroupId or campaignId (neither lists the account’s own negative keywords). Read-only, 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
adGroupId | string | the ad group |
campaignId | string | the campaign |
matchTypes | "BROAD" | "PHRASE" | "EXACT" | "EXACT_NEGATIVE" | "PHRASE_NEGATIVE"[] | only these match types |
bookmark | string | the next page, from the last reply |
manage_pinterest_ads_keywords#
Add or archive Pinterest ad keywords destructive
Add keywords to a Pinterest ad group, or archive them. action:"add" needs parentId (an ad group; a campaign takes only the _NEGATIVE match types) and keywords; Pinterest’s per-keyword refusals are reported. action:"archive" needs keywordIds; archiving is how Pinterest deletes a keyword. Read back from Pinterest. pinterest_ads_keyword_ideas finds candidates. 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
action | "add" | "archive" | default add |
parentId | string | add: the ad group, or a campaign for negative keywords |
keywords | object[] | add: the keywords |
keywordIds | string[] | archive: ids from list_pinterest_ads_keywords |
pinterest_ads_audience_size#
Pinterest audience size read-only
Estimate how many people a Pinterest ad group’s targeting can reach per month, before spending. Pass targetingSpec (e.g. {"GEO":["US"],"AGE_BUCKET":["25-34"]}), keywords and/or productGroupIds. A range from Pinterest’s historical data, not a promise. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
targetingSpec | object | Pinterest targeting object, the same one an ad group takes |
keywords | object[] | keywords to size |
placementGroup | "ALL" | "SEARCH" | "BROWSE" | "OTHER" | default ALL |
autoTargetingEnabled | boolean | default true at Pinterest |
creativeTypes | string[] | e.g. REGULAR, VIDEO, SHOPPING |
productGroupIds | string[] | product groups to size |
pinterest_ads_bulk#
Pinterest bulk export and upsert destructive
Pinterest’s BULK lane: many campaigns, ad groups, ads, keywords or product groups in one request. action:"download" exports them; action:"upsert" creates or updates them from create and update, in Pinterest’s own object shapes; action:"status" reads a request by requestId and gives the result file link. Download and upsert run in the background, so read status next. A created object with no status goes in PAUSED; anything that starts delivery or changes a budget or bid is refused until the user agrees and confirm:true is passed. 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
action | "download" | "upsert" | "status" | default download |
requestId | string | status: the id download or upsert answered |
entityTypes | "CAMPAIGN" | "AD_GROUP" | "PRODUCT_GROUP" | "AD" | "KEYWORD" | "LABEL" | "SCHEDULE" | "ENTITY_HISTORY"[] | download: up to 6 types |
outputFormat | "JSON" | "CSV" | download: default JSON |
entityIds | string[] | download: only these ids |
campaignFilter | object | download: Pinterest’s campaign_filter object |
updatedSince | string | download: a Unix timestamp |
create | object | upsert: objects to create by type, e.g. {"campaigns":[...],"ad_groups":[...],"keywords":[...]} |
update | object | upsert: objects to change by type |
confirm | boolean | upsert: required true when it starts delivery or changes a budget or bid |
pinterest_ads_account_data#
Pinterest ad-account data read-only
Read Pinterest ad-account data. what: conversion_eqs (Event Quality Score of the conversion signals; lookback 1d or 14d, optional sourcePlatform, ingestionSource), metrics_ready_state (whether a date’s numbers are final; date YYYY-MM-DD), report_templates, promotions, targeting_templates. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
whatrequired | "conversion_eqs" | "metrics_ready_state" | "report_templates" | "promotions" | "targeting_templates" | which data |
lookback | "1d" | "14d" | conversion_eqs: default 14d |
sourcePlatform | string | conversion_eqs: e.g. WEB |
ingestionSource | string | conversion_eqs: e.g. TAG, CONVERSIONS_API |
date | string | metrics_ready_state: YYYY-MM-DD |
searchQuery | string | targeting_templates: by name |
includeSizing | boolean | targeting_templates: add the audience size |
bookmark | string | the next page, from the last reply |
manage_pinterest_ads_targeting_template#
Manage Pinterest targeting templates destructive
Save a Pinterest targeting set to reuse across ad groups, or remove one. action:"create" needs name and targetingSpec; "remove" needs templateId. Pinterest does not let an ordinary account edit a saved one, so a changed set is a new template. Read back from Pinterest. 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
action | "create" | "remove" | default create |
templateId | string | remove: from pinterest_ads_account_data what:targeting_templates |
name | string | create: the template name |
targetingSpec | object | Pinterest targeting object, e.g. {"GEO":["US"],"AGE_BUCKET":["25-34"]} |
keywords | object[] | keywords to hold |
placementGroup | "ALL" | "SEARCH" | "BROWSE" | "OTHER" | create: default ALL |
autoTargetingEnabled | boolean | create: default true at Pinterest |
manage_pinterest_ads_promotion#
Manage Pinterest promotions destructive
Create, change or delete a Pinterest PROMOTION, the discount an ad group can show. action:"create" needs title and type (the kind of offer, which sets the text shown) and takes code, startTime, endTime (Unix seconds), templateValues (what fills the text), discountStatus, and any other Pinterest field in fields; "update" needs promotionId; "delete" needs promotionId and confirm:true. Read back from Pinterest. 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
action | "create" | "update" | "delete" | default create |
promotionId | string | update, delete: from pinterest_ads_account_data what:promotions |
title | string | create: the internal name |
type | "VARIABLE" | "SITEWIDE" | "CHECKOUT" | "SAVE_X_ON_Y" | "BUY_X_GET_Y" | "SPEND_X_SAVE_Y" | "FREE_SHIPPING" | "FREE_SHIPPING_MINIMUM" | "FREE_SHIPPING_WITH_DISCOUNT" | "SITEWIDE_IN_STORES" | "EXTRA_PERCENT_OFF" | "GIFT_WITH_PURCHASE" | "GIFT_WITH_PURCHASE_MINIMUM" | "FIXED" | "PERCENT_OFF_CLEARANCE" | "X_OFF_Y" | "GIFT_WITH_FIRST_PURCHASE" | "BUY_X_GET_ONE_FREE" | "CASH_BACK" | "POINTS_ON_ALL_PURCHASES" | "BONUS" | "POINTS_WITH_PURCHASE" | "CUSTOM" | create: the kind of offer |
templateValues | object[] | what fills the offer text, at most 2, in Pinterest template_values shape |
code | string | the code a buyer redeems |
customId | string | your own id for it |
discountStatus | string | Pinterest’s discount_status value |
startTime | number | Unix seconds |
endTime | number | Unix seconds |
fields | object | any other promotion field, by Pinterest’s own name |
confirm | boolean | delete: required true |
run_pinterest_ads_report_template#
Run a Pinterest report template
Run a saved Pinterest report template (pinterest_ads_account_data what:report_templates lists them) for an optional date range. It runs in the background and answers a token; pinterest_ads_async_report with that token gives the rows. 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
templateIdrequired | string | the template id |
startDate | string | YYYY-MM-DD |
endDate | string | YYYY-MM-DD |
granularity | "TOTAL" | "DAY" | "HOUR" | "WEEK" | "MONTH" | default the template’s own |
pinterest_ads_keyword_ideas#
Pinterest keyword ideas read-only
Pinterest KEYWORD RESEARCH through the Pinterest Ads connection. mode:"related" (default): terms related to each of your terms ("workout" -> "yoga workout", "workout motivation"); mode:"suggested": popular searches that BEGIN with the term; mode:"metrics": Pinterest-wide search volume for up to 2,000 keywords in one country. Feeds ad-group KEYWORD targeting and Pin titles. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
mode | "related" | "suggested" | "metrics" | related (default), suggested (searches that begin with the term) or metrics (search volume) |
termsrequired | string[] | keywords or seed terms; suggested mode uses the first one only |
limit | number | suggested: 1–10 |
countryCode | string | metrics: two-letter country, default US |
list_pinterest_ads_product_group_promotions#
List Pinterest product-group promotions read-only
List the PRODUCT GROUPS being promoted in one Pinterest Shopping ad group — Pinterest lists them per ad group, so adGroupId is required — with status and bid. A CATALOG_SALES ad group with none has no products to serve. Read-only, 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
adGroupIdrequired | string | Shopping ad group whose product-group promotions to list |
create_pinterest_ads_product_group_promotion#
Promote catalog product groups in a Pinterest ad group destructive
Attach catalog PRODUCT GROUPS to a Pinterest Shopping (CATALOG_SALES) ad group — this is what makes a Shopping ad group serve products. Created PAUSED by default; creating them ACTIVE under a LIVE campaign and ad group starts real spend, so that needs an explicit yes and confirm:true. Optional bid (ad account currency), included:false to exclude a group, creativeType, cta, trackingUrl. Read back from Pinterest. 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
adGroupIdrequired | string | CATALOG_SALES ad group to attach the product groups to |
productGroupIdsrequired | string[] | catalog product group ids to promote |
status | "ACTIVE" | "PAUSED" | default PAUSED |
bid | number | bid in the ad account currency |
included | boolean | false excludes these product groups instead of including them |
creativeType | "SHOPPING" | "COLLECTION" | "CAROUSEL" | "MAX_VIDEO" | SHOPPING, COLLECTION, CAROUSEL or MAX_VIDEO |
cta | string | catalog sales CTAs: SHOP_NOW, BOOK_NOW, ON_SALE, GET_DEAL |
trackingUrl | string | tracking URL appended to clicks on these promotions |
confirm | boolean | true required to create ACTIVE promotions under a live campaign and ad group |
update_pinterest_ads_product_group_promotion#
Change a Pinterest product-group promotion destructive
Change one Pinterest product-group promotion — status (ACTIVE / PAUSED / ARCHIVED), bid, included, trackingUrl. Activating it, or moving its bid, under a LIVE campaign and ad group changes real spend and needs confirm:true. Read back from Pinterest. 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
promotionIdrequired | string | promotion id from list_pinterest_ads_product_group_promotions |
status | "ACTIVE" | "PAUSED" | "ARCHIVED" | ACTIVE, PAUSED or ARCHIVED |
bid | number | new bid in the ad account currency |
included | boolean | false excludes the product group from the ad group |
trackingUrl | string | new tracking URL for clicks |
confirm | boolean | true required when activating or changing the bid under a live campaign |
preview_pinterest_ads_ad#
Preview a Pinterest ad
See a Pinterest AD before spending — Pinterest renders a preview link (valid 7 days, iframe-able) from an existing promotable Pin (pinId), from an image not yet pinned (imageUrl + title), or from a catalog product group (productGroupId, for a shopping ad). Creates no ad, spends nothing, 0 credits.
| Parameter | Type | Description |
|---|
adAccountId | string | Pinterest ad account id — omit to use the single shared account |
pinId | string | existing promotable Pin id to preview |
imageUrl | string | public image URL to preview before it is pinned; needs title |
title | string | required with imageUrl |
productGroupId | string | catalog product group id, for a shopping ad preview |
creativeType | string | pin: SHOPPING, COLLECTION, MAX_VIDEO, MAX_WIDTH_VIDEO_COLLECTION, MAX_WIDTH_REGULAR_COLLECTION; product group: SHOPPING, COLLECTION, CAROUSEL, MAX_WIDTH_COLLECTION |
itemId | string | catalog item (product) id to preview within the product group |
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 profile — do this first to pick a target. Call WITH adAccountId to read that account’s whole tree at once: campaigns, ad groups and ads, each with its configured status and Reddit’s own effective status (the effective one is what says whether it could actually serve — PENDING_APPROVAL, 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 (or call list_connector_accounts with provider reddit_ads, then set_connector_accounts).
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_…) — omit to list the ad accounts shared with this profile |
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. ONE DATED LIMIT WORTH PLANNING AROUND: from 2026-12-08 Reddit caps the HOUR breakdown at windows of 7 DAYS OR FEWER and answers 400 beyond that. Hermoso warns before that date and refuses after it rather than relaying a bare vendor 400 — for a longer stretch, run one report per <=7-day window, or use the DATE breakdown for a daily series over the whole period. Read-only, free.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
since | string | YYYY-MM-DD or full ISO timestamp, default 30 days ago |
until | string | YYYY-MM-DD or full ISO timestamp, default today |
fields | string[] | 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. |
filter | string | Reddit filter expression to restrict rows to particular ids/values |
timeZoneId | string | IANA 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
search_reddit_ads_targeting#
Resolve Reddit targeting: communities, keywords, geos, interests, devices, carriers, languages 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:"community_suggestions" asks Reddit which communities fit some topics (query, comma-separated) and/or a websiteUrl — the best way to widen reach. kind:"keywords" turns seed terms (keywords, or query comma-separated) into Reddit’s keyword suggestions WITH MONTHLY VIEWS, sorted biggest first — also a quick read on what Reddit is talking about. kind:"keyword_check" asks Reddit whether keywords are targetable under its brand-safety rules; leave the unsafe ones out of targeting.keywords. 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"). kind:"devices" / "carriers" / "languages" list what targeting.devices / carriers (e.g. VERIZON_US) / languages (e.g. EN) take; "third_party_audiences" lists partner data segments with their size and CPM data cost. Read-only, free. Use this before create_reddit_ads_ad_group rather than inventing a value.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
kind | "communities" | "community_suggestions" | "keywords" | "keyword_check" | "geolocations" | "interests" | "devices" | "carriers" | "languages" | "third_party_audiences" | default communities |
query | string | keyword — required for communities; topics for community_suggestions; seeds for keywords; filters interests / devices / carriers / languages; searches cities for geolocations |
keywords | string[] | keywords / keyword_check: the terms (up to 1,000, each ≤100 characters) |
websiteUrl | string | community_suggestions: suggest communities related to this site |
country | string | 2-letter country code, geolocations only |
postalCode | string | postal code to narrow a geolocations search |
limit | number | max 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
budgetrequired | number | budget in the ad account’s currency (not micro-currency — the conversion is handled) |
objective | "APP_INSTALLS" | "BRAND_AWARENESS" | "CATALOG_SALES" | "CLICKS" | "CONVERSIONS" | "IMPRESSIONS" | "LEAD_GENERATION" | "SALES" | "VIDEO_VIEWABLE_IMPRESSIONS" | default CLICKS |
goalType | "DAILY_SPEND" | "LIFETIME_SPEND" | DAILY_SPEND or LIFETIME_SPEND; LIFETIME_SPEND needs endTime |
bidType | "CPC" | "CPM" | "CPV" | "CPV6" | "CPV15" | CPC, CPM, CPV, CPV6 or CPV15; must fit the objective |
bidStrategy | "BIDLESS" | "MANUAL_BIDDING" | "MAXIMIZE_VOLUME" | "TARGET_CPX" | BIDLESS, MANUAL_BIDDING, MAXIMIZE_VOLUME or TARGET_CPX |
bidAmount | number | bid in the ad account currency; needed for paid bid types |
startTime | string | ISO 8601 |
endTime | string | ISO 8601 end of the forecast window |
targeting | object | same shape as create_reddit_ads_ad_group targeting |
reddit_ads_plan_forecast#
Forecast a Reddit campaign plan read-only
Reddit Campaign Planner’s forecast for a proposed campaign: the audience, projected reach, impressions, clicks, frequency and CPM/CPC/CPV for this budget, and a curve of reach at ten budgets to compare. Creates nothing and spends nothing. Use reddit_ads_forecast for a quick audience size. Reddit allows 30 of these a minute. These are Reddit’s estimates, not a guarantee, and they reserve nothing; say so, and keep each result beside the inputs that made it.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
objectiverequired | "APP_INSTALLS" | "BRAND_AWARENESS" | "CATALOG_SALES" | "CLICKS" | "CONVERSIONS" | "IMPRESSIONS" | "LEAD_GENERATION" | "SALES" | "VIDEO_VIEWABLE_IMPRESSIONS" | campaign objective |
goalTyperequired | "DAILY_SPEND" | "LIFETIME_SPEND" | whether budget is per day or for the whole plan |
budgetrequired | number | budget in the ad account’s currency (not micro-currency) |
startTime | string | ISO 8601; default now |
endTime | string | ISO 8601; default 30 days after the start, at most 365 |
bidType | "CPC" | "CPM" | "CPV" | "CPV6" | "CPV15" | CPC, CPM, CPV, CPV6 or CPV15 |
bidStrategy | "BIDLESS" | "MANUAL_BIDDING" | "MAXIMIZE_VOLUME" | "TARGET_CPX" | BIDLESS, MANUAL_BIDDING, MAXIMIZE_VOLUME or TARGET_CPX |
bidAmount | number | bid in the ad account currency |
useCatalog | boolean | SALES objective: true forecasts catalog sales |
targeting | object | same 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
budgetrequired | number | budget in the ad account’s currency |
objective | "APP_INSTALLS" | "BRAND_AWARENESS" | "CATALOG_SALES" | "CLICKS" | "CONVERSIONS" | "IMPRESSIONS" | "LEAD_GENERATION" | "SALES" | "VIDEO_VIEWABLE_IMPRESSIONS" | default CLICKS |
bidType | "CPC" | "CPM" | "CPV" | "CPV6" | "CPV15" | default CPC — must fit the campaign objective |
bidStrategy | "BIDLESS" | "MANUAL_BIDDING" | "MAXIMIZE_VOLUME" | "TARGET_CPX" | BIDLESS, MANUAL_BIDDING, MAXIMIZE_VOLUME or TARGET_CPX |
goalType | "DAILY_SPEND" | "LIFETIME_SPEND" | DAILY_SPEND or LIFETIME_SPEND |
startTime | string | ISO 8601 start of the flight to price |
endTime | string | ISO 8601 end of the flight to price |
currency | string | currency code, e.g. USD; returned amounts use the ad account currency |
targeting | object | targeting block, same shape as create_reddit_ads_ad_group targeting |
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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
redditProfileIdrequired | string | the Reddit profile id (t2_…) from list_reddit_ads_profiles |
type | "TEXT" | "IMAGE" | "VIDEO" | "CAROUSEL" | TEXT, IMAGE, VIDEO or CAROUSEL; omit for all types |
limit | number | page size, 1-100, default 25 |
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), and an IMAGE, VIDEO or CAROUSEL post can carry body text too. 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. Reddit builds every post in the background: a TEXT post needs a body, an IMAGE post and every carousel card a destinationUrl. The answer is usually the post, and when Reddit is still building it the answer says pending with a jobId, so call this again with redditProfileId and that jobId (never re-send the post, which would make a second one).
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
redditProfileIdrequired | string | the Reddit profile id (t2_…) to publish as — from list_reddit_ads_profiles |
jobId | string | PICK UP a post Reddit was still building: the jobId from an earlier pending answer. Creates nothing new; every other field is ignored. |
type | "TEXT" | "IMAGE" | "VIDEO" | "CAROUSEL" | default TEXT |
headlinerequired | string | the post title — this is the ad’s headline |
body | string | body copy: the text of a TEXT post, or text shown with an IMAGE, VIDEO or CAROUSEL post. Up to 40,000 characters. |
textFormat | "PLAIN_TEXT" | "RICH_TEXT_JSON" | how Reddit reads body: PLAIN_TEXT (the default) or RICH_TEXT_JSON, a Reddit rich-text JSON document serialized as a string. Cannot be changed after the post is created. |
media | object[] | one entry for IMAGE/VIDEO, up to 6 for CAROUSEL |
callToAction | string | applies to every media entry that has none |
thumbnailUrl | string | required for VIDEO posts |
allowComments | boolean | Reddit ads can carry a public comment thread — decide deliberately |
update_reddit_ads_post#
Turn comments on or off on a Reddit ad post destructive
Turn comments ON or OFF on an existing Reddit ad post. THAT IS THE ONLY EDIT REDDIT ALLOWS: its post-update schema permits exactly one field, `allow_comments`, and REQUIRES it — headline and body both answer “Additional fields not permitted” once a post is published (measured live 2026-08-05). So a copy change is not an edit at all: create a new post with create_reddit_ads_post and point the ad at it with update_reddit_ads_ad, or fix the wording in Reddit’s Ads Manager. Never promise to reword a live post. 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. Turning comments off is publicly visible on a post people may already be replying to, so confirm it with the user first.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
postIdrequired | string | the post id (t3_…) |
allowCommentsrequired | boolean | REQUIRED — Reddit demands allow_comments on every post update, and it is the only field it permits |
create_reddit_ads_campaign#
Create a Reddit campaign destructive
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 optimises toward LEAD / SIGN_UP pixel events (Reddit retired onsite lead forms), IMPRESSIONS and VIDEO_VIEWABLE_IMPRESSIONS buy reach, APP_INSTALLS and CATALOG_SALES are for apps and product feeds. Reddit’s NEW names (since 2026-09-21) are accepted too: BRAND_AWARENESS (= IMPRESSIONS / VIDEO_VIEWABLE_IMPRESSIONS) and SALES (= CONVERSIONS, or CATALOG_SALES when useCatalog is true); Reddit rolls them out per account, so an account without them yet answers with Reddit’s own refusal — use the legacy name then. 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_…) — omit when only one is shared |
namerequired | string | campaign name |
objective | "APP_INSTALLS" | "BRAND_AWARENESS" | "CATALOG_SALES" | "CLICKS" | "CONVERSIONS" | "IMPRESSIONS" | "LEAD_GENERATION" | "SALES" | "VIDEO_VIEWABLE_IMPRESSIONS" | default CLICKS — which is what Reddit calls website traffic |
spendCapCents | number | lifetime spend ceiling for the whole campaign, in minor units of the ad account’s currency |
useCatalog | boolean | SALES only: true makes it a product-catalog campaign (the new spelling of CATALOG_SALES) — every ad group under it must then use a catalog (create_reddit_ads_ad_group with productSetId + shoppingType; list_reddit_ads_catalogs gives the product set ids, create_reddit_ads_catalog / create_reddit_ads_product_set make them). Frozen once created |
isCampaignBudgetOptimization | boolean | CAMPAIGN BUDGET OPTIMIZATION (Reddit’s recommended structure): the budget and bidding live on the campaign and Reddit spreads the budget across its ad groups, which then inherit goal type, bid type and bid strategy. Needs budget, startTime, bidStrategy, bidType (and optimizationGoal on a CONVERSIONS / SALES / VIDEO_VIEWABLE_IMPRESSIONS campaign); a conversion pixel is attached automatically when the account has one. Cannot be changed after the campaign is created |
budget | number | CBO only: the campaign budget in the ad account’s currency, paired with goalType |
goalType | "DAILY_SPEND" | "LIFETIME_SPEND" | CBO only, default DAILY_SPEND; LIFETIME_SPEND needs endTime |
startTime | string | CBO only: ISO 8601 |
endTime | string | CBO only: ISO 8601, required for LIFETIME_SPEND |
bidStrategy | "BIDLESS" | "MAXIMIZE_VOLUME" | "TARGET_CPX" | CBO only (MANUAL_BIDDING is ad-group only) |
bidType | "CPC" | "CPM" | "CPV6" | "CPV15" | CBO only, must fit the objective — CLICKS takes CPC |
bidAmount | number | CBO only: target cost in the ad account’s currency — required for TARGET_CPX; when set here no ad group may set its own |
optimizationGoal | string | CBO only: e.g. PURCHASE, SIGN_UP, LEAD, ADD_TO_CART, PAGE_VISIT, CLICKS. Cannot be changed later |
viewThroughConversionType | "SEVEN_DAY_CLICKS" | "SEVEN_DAY_CLICKS_ONE_DAY_VIEW" | CBO only: the conversion attribution window. Cannot be changed later |
conversionPixelId | string | CBO only: needed when the ad account has more than one pixel |
schedule | object[] | CBO only: weekly delivery windows (day 0 = Sunday, hour 0-23) — replaces every ad group’s schedule |
specialAdCategories | "HOUSING_EMPLOYMENT_CREDIT" | "NONE"[] | HOUSING_EMPLOYMENT_CREDIT for housing, employment or credit ads — Reddit then restricts their targeting. Cannot be changed after creation |
appId | string | APP_INSTALLS only: the App Store id (e.g. 1064216828) or Google Play package (e.g. com.reddit.frontpage) |
fundingInstrumentId | string | bill this campaign to a specific Reddit funding instrument |
invoiceLabel | string | agency reconciliation label on the invoice, ≤80 characters — Reddit ignores it on credit-card-funded campaigns, and the reply says so |
create_reddit_ads_ad_group#
Create a Reddit ad group (targeting, budget, bidding, schedule) destructive
Create an ad group under an existing Reddit campaign — this is the tier that holds the budget, the bid and ALL the targeting. ALWAYS created PAUSED; it spends nothing until set_reddit_ads_status(confirm:true). Reddit refuses the create unless it has: a bidType, a bidStrategy, a startTime, a budget with its goalType, a bidAmount whenever the bid type is a paid rate, and a conversion pixel (resolved 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, a SALES campaign (Reddit’s new name for conversions) asked for 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
campaignIdrequired | string | the campaign this ad group belongs to |
namerequired | string | ad group name |
budget | number | budget in the ad account’s currency, paired with goalType — required, EXCEPT under a campaign-budget-optimization campaign, where the budget lives on the campaign and must be left off |
goalType | "DAILY_SPEND" | "LIFETIME_SPEND" | default DAILY_SPEND; under a CBO campaign it is taken from the campaign |
bidType | "CPC" | "CPM" | "CPV" | "CPV6" | "CPV15" | required — must fit the campaign objective, CLICKS campaigns take CPC; under a CBO campaign it is taken from the campaign |
bidStrategy | "BIDLESS" | "MANUAL_BIDDING" | "MAXIMIZE_VOLUME" | "TARGET_CPX" | required — MANUAL_BIDDING needs bidAmount; under a CBO campaign it is taken from the campaign |
bidAmount | number | bid in the ad account’s currency — required for paid bid types; ask reddit_ads_bid_suggestion |
startTime | string | ISO 8601, e.g. 2026-08-15T00:00:00Z — required (Reddit rejects the create without one), except under a CBO campaign, where it defaults to the campaign’s start |
endTime | string | ISO 8601 end time; omit for no end |
conversionPixelId | string | only needed when the ad account has more than one pixel |
optimizationGoal | string | the event this ad group optimizes toward, e.g. PURCHASE, SIGN_UP, LEAD, ADD_TO_CART, PAGE_VISIT, CLICKS, VIDEO_VIEW_6S; leave it unset on a CONVERSIONS or SALES campaign to take the campaign’s. CUSTOM_EVENT (new) optimizes toward a custom pixel event: Reddit switches it on per ad account and answers "not available for this ad account" where it has not. Cannot be changed later |
savedAudienceId | string | reuse a saved audience instead of spelling targeting out — from list_reddit_ads_saved_audiences |
viewThroughConversionType | "SEVEN_DAY_CLICKS" | "SEVEN_DAY_CLICKS_ONE_DAY_VIEW" | the conversion window counted. SEVEN_DAY_CLICKS_ONE_DAY_VIEW is only accepted with a conversion goal (PURCHASE, SIGN_UP, LEAD, ADD_TO_CART, PAGE_VISIT), and Reddit requires it for a catalog SALES ad group optimizing for PURCHASE. Cannot be changed later |
productSetId | string | CATALOG SALES — the Reddit product set this ad group advertises (a UUID from Reddit Ads Manager > Catalogs). Required on every ad group under a SALES campaign created with useCatalog:true; Reddit silently drops it on any other campaign, and the note says so |
shoppingType | "DYNAMIC" | "STATIC" | CATALOG SALES — DYNAMIC builds each ad from the product set, STATIC links a normal ad to the catalog. Required on every ad group under a useCatalog:true SALES campaign |
targeting | object | targeting block; every key optional (communities, geolocations, interests, keywords, audiences, devices) |
schedule | object[] | weekly dayparting windows — omit to run all week |
update_reddit_ads_ad_group#
Edit a Reddit ad group destructive
Change an existing Reddit ad group’s name, budget, goal type, bid, schedule dates or targeting. Budget and bid are ordinary amounts in the ad account’s currency. Targeting MERGES KEY BY KEY — measured live 2026-08-05, and it is NOT a wholesale replace: a key you send replaces that whole list, a key you LEAVE OUT is kept exactly as it was, and an explicit empty array (geolocations: []) is the only way to clear one. So passing just {communities:[…]} does NOT drop an existing geo or interest filter on an ad group that holds the budget — name every key you want gone. This does NOT activate or pause anything; use set_reddit_ads_status for that. The result is read back from Reddit.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
adGroupIdrequired | string | id of the ad group to edit |
name | string | new ad group name |
budget | number | new budget in the ad account currency, paired with goalType |
goalType | "DAILY_SPEND" | "LIFETIME_SPEND" | DAILY_SPEND or LIFETIME_SPEND |
bidAmount | number | new bid in the ad account currency |
bidType | "CPC" | "CPM" | "CPV" | "CPV6" | "CPV15" | CPC, CPM, CPV, CPV6 or CPV15; must fit the campaign objective |
bidStrategy | "BIDLESS" | "MANUAL_BIDDING" | "MAXIMIZE_VOLUME" | "TARGET_CPX" | BIDLESS, MANUAL_BIDDING, MAXIMIZE_VOLUME or TARGET_CPX |
startTime | string | ISO 8601 new start time |
endTime | string | ISO 8601 new end time |
savedAudienceId | string | point this ad group at a saved audience instead |
productSetId | string | CATALOG SALES — rebind the ad group to another product set (a UUID from Reddit Ads Manager > Catalogs). Reddit stores an edit without checking the set exists or that the campaign uses a catalog, so pass the right id; the note says if it was not kept |
shoppingType | "DYNAMIC" | "STATIC" | CATALOG SALES — DYNAMIC or STATIC product ads. Reddit has been measured keeping the value an ad group was created with, and the note says when it did not change |
targeting | object | the targeting keys to change, in the same shape as create_reddit_ads_ad_group: each key sent replaces that whole list, keys left out are kept, and an empty list clears one |
schedule | object[] | weekly delivery windows: startDay, endDay, startHour, endHour (0 = Sunday) |
create_reddit_ads_ad#
Create a Reddit ad destructive
Create the ad itself — the object that binds a POST (the creative) to an AD GROUP (the targeting and budget). Create the post first with create_reddit_ads_post and pass its id as postId; without a post there is nothing to show. A CATALOG ad (an ad group with a productSetId under a SALES useCatalog campaign) has no post: pass shoppingCreative (headline, callToAction, optional destinationUrl, secondLineCta PRICE/DESCRIPTION, dpaCarouselMode, heroCard for a collection ad) plus redditProfileId, and optionally up to 6 productIds to feature. 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. UTMs: clickUrlQueryParameters ({"utm_source":"reddit","utm_campaign":"{{CAMPAIGN_ID}}"} — Reddit macros allowed, ≤14) are appended to the click URL by Reddit — Reddit allows that list only on some campaigns and otherwise refuses “URL Parameters cannot be added to this campaign”; then write the parameters into clickUrl itself. PREVIEW: previewDays (1-30) asks Reddit for a preview link, read back as previewUrl (the very first time, Reddit wants the test URL generated once in Ads Manager — the reply says so). 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
adGroupIdrequired | string | the ad group whose targeting and budget this ad runs under |
namerequired | string | ad name |
postId | string | the post to promote (t3_…) from create_reddit_ads_post or list_reddit_ads_posts — required except for a catalog ad built from shoppingCreative |
clickUrl | string | leave unset for TEXT-post ads — Reddit rejects "Free form ads cannot have a click url" |
clickUrlQueryParameters | object | UTM / tracking parameters Reddit appends to the click URL, e.g. {"utm_source":"reddit","utm_medium":"paid","utm_content":"{{AD_ID}}"} — up to 14 |
previewDays | number | 1-30: switch on a preview link that stays valid this many days |
redditProfileId | string | the post author profile — required for catalog sales campaigns |
eventTrackers | object[] | third-party measurement URLs; only Reddit-approved providers are accepted |
productIds | string[] | catalog ads: up to 6 product ids to feature |
shoppingCreative | object | catalog (shopping) ads only |
create_reddit_ads_max_campaign#
Build a Reddit Max campaign (paused) destructive
Build a Reddit MAX campaign in one call: an automated campaign (campaign budget optimization, automatic bidding), its one automated ad group (seed targeting) and its one template ad, from which Reddit GENERATES the ads out of your creative-library assets. ALL CREATED PAUSED: nothing spends until the user approves and set_reddit_ads_status(confirm:true) runs on the campaign, the ad group and the template ad. Before calling, upload at least 3 HEADLINE and 2 IMAGE or VIDEO assets with upload_reddit_ads_assets (5 to 45 in total, all ACTIVE) and pass their ids, and take redditProfileId from list_reddit_ads_profiles. Every asset is read and counted by type before anything is created. Objectives: CLICKS, SALES, LEAD_GENERATION, APP_INSTALLS (and the legacy CONVERSIONS, Reddit’s old name for SALES). spendCap puts a lifetime ceiling on a DAILY_SPEND campaign. Bid strategies: BIDLESS (default), MAXIMIZE_VOLUME, TARGET_CPX (needs bidAmount). MAXIMIZE_VALUE and TARGET_ROAS cannot be set: Reddit exposes them only as read-only values. If a later step fails, what this call created is ARCHIVED and the reply says so. The reply says whether Reddit finished generating the ads (materialization); poll with get_reddit_ads_max_template.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
namerequired | string | campaign name, 3 to 500 characters |
objective | "CLICKS" | "SALES" | "LEAD_GENERATION" | "APP_INSTALLS" | "CONVERSIONS" | default CLICKS; CONVERSIONS is the legacy name of SALES |
budgetrequired | number | campaign budget in the ad account’s currency (a Max budget lives on the campaign) |
goalType | "DAILY_SPEND" | "LIFETIME_SPEND" | default DAILY_SPEND; LIFETIME_SPEND needs endTime |
startTimerequired | string | ISO 8601, e.g. 2026-09-20T00:00:00Z |
endTime | string | ISO 8601 end time; required for LIFETIME_SPEND |
bidStrategy | "BIDLESS" | "MAXIMIZE_VOLUME" | "TARGET_CPX" | default BIDLESS |
bidType | "CPC" | "CPM" | "CPV6" | "CPV15" | default CPC for CLICKS; required for the other objectives |
bidAmount | number | target cost per result in the ad account’s currency; required for TARGET_CPX |
optimizationGoal | string | default CLICKS for CLICKS; required otherwise: SALES / CONVERSIONS take e.g. PURCHASE, ADD_TO_CART, SIGN_UP, LEAD or CUSTOM_EVENT (a custom pixel event Reddit switches on per ad account), LEAD_GENERATION takes LEAD or SIGN_UP, APP_INSTALLS a MOBILE_CONVERSION_ goal; cannot change later |
spendCap | number | lifetime spend ceiling in the ad account’s currency; DAILY_SPEND only, at or above the daily budget |
invoiceLabel | string | up to 80 characters on the invoice; ignored on card-funded accounts |
conversionPixelId | string | only needed when the ad account has more than one pixel |
appId | string | App Store or Google Play id; required for APP_INSTALLS |
specialAdCategories | "HOUSING_EMPLOYMENT_CREDIT" | "NONE"[] | cannot change after publishing |
viewThroughConversionType | "SEVEN_DAY_CLICKS" | "SEVEN_DAY_CLICKS_ONE_DAY_VIEW" | conversion attribution window: SEVEN_DAY_CLICKS or SEVEN_DAY_CLICKS_ONE_DAY_VIEW |
fundingInstrumentId | string | funding instrument id from list_reddit_ads_funding_instruments |
schedule | object[] | weekly windows, day 0 = Sunday |
targetingrequired | object | SEED signals: at least one of communities, interests, keywords or customAudienceIds; geolocations and the excluded lists are hard controls |
adGroupName | string | name of the automated ad group |
adName | string | name of the template ad |
redditProfileIdrequired | string | the t2_ profile the template ad runs as, from list_reddit_ads_profiles |
assetIdsrequired | string[] | 5 to 45 ACTIVE creative-library asset ids: at least 3 HEADLINE and 2 IMAGE or VIDEO |
thumbnailAssetIds | string[] | up to 10 |
destinationUrlrequired | string | the https landing page |
displayUrl | string | URL shown on the ad; must be on the same domain as destinationUrl |
supplementaryText | string | up to 100 characters |
userGeneratedContent | "OPT_IN" | "OPT_OUT" | let Reddit add user-generated content enhancements |
get_reddit_ads_max_template#
Read a Reddit Max template ad read-only
Read a Reddit Max template ad: its status, its assets, its destination and whether Reddit has finished generating ads from it (materialization SUCCESS, PARTIAL or FAILED with Reddit’s reasons, or still generating). Poll this after create_reddit_ads_max_campaign or update_reddit_ads_max_template. Read-only, free.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
adIdrequired | string | the template ad id |
update_reddit_ads_max_template#
Edit a Reddit Max template ad destructive
Change a Reddit Max template ad: its assets (5 to 45 ACTIVE, at least 3 HEADLINE and 2 IMAGE or VIDEO, read and counted before anything is sent), thumbnails, destination URL, supplementary text, user-generated content enrollment or name. The reply is the template read back, including whether Reddit regenerated the ads. Pausing, activating or archiving it is set_reddit_ads_status.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
adIdrequired | string | template ad id, from create_reddit_ads_max_campaign |
name | string | new template ad name |
assetIds | string[] | REPLACES the template’s assets |
thumbnailAssetIds | string[] | up to 10; an empty list clears them |
destinationUrl | string | new https landing page |
displayUrl | string | new display URL; same domain as destinationUrl |
supplementaryText | string | up to 100 characters |
userGeneratedContent | "OPT_IN" | "OPT_OUT" | OPT_IN or OPT_OUT of Reddit user-generated content enhancements |
update_reddit_ads_ad#
Edit a Reddit ad destructive
Edit a Reddit ad, including a LIVE one: rename it, point it at a different post, change its click url, set or clear its UTM / tracking parameters (clickUrlQueryParameters; pass null to clear), switch on or refresh its PREVIEW link (previewDays 1-30, read back as previewUrl; previewOff:true switches it off), replace its third-party event trackers, or for a catalog ad change its shoppingCreative / featured productIds. 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 — and Reddit does NOT move the click url with the post, so the reply says where clicks still go. The reply also carries Reddit’s rejection reason when the ad was rejected. The result is read back from Reddit.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
adIdrequired | string | id of the ad to edit |
name | string | new ad name |
postId | string | promote a different post |
clickUrl | string | pass an empty string to clear it |
clickUrlQueryParameters | object | null | UTM / tracking parameters, e.g. {"utm_source":"reddit","utm_content":"{{AD_ID}}"} (≤14); null clears them |
previewDays | number | 1-30: (re)enable the preview link for this many days |
previewOff | boolean | true switches the preview link off |
eventTrackers | object[] | replaces the third-party trackers (Reddit-approved providers only) |
redditProfileId | string | Reddit profile id (t2_...) the ad runs as |
productIds | string[] | catalog ads: up to 6 featured product ids |
shoppingCreative | object | catalog ads: same fields as create_reddit_ads_ad |
update_reddit_ads_campaign#
Edit a Reddit campaign destructive
Change an existing Reddit campaign’s name, spend cap, budget, goal type or flight dates. Amounts are ordinary numbers in the ad account’s currency. The OBJECTIVE and useCatalog cannot be changed: Reddit freezes both at creation and refuses a change even from an old objective name to its new one (IMPRESSIONS to BRAND_AWARENESS), so create a new campaign instead. This does NOT activate, pause or archive anything — use set_reddit_ads_status for that. The result is read back from Reddit before you are told it took.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
campaignIdrequired | string | id of the campaign to edit |
name | string | new campaign name |
spendCap | number | lifetime spend ceiling in the ad account’s currency |
budget | number | new campaign budget in the ad account currency |
goalType | "DAILY_SPEND" | "LIFETIME_SPEND" | DAILY_SPEND or LIFETIME_SPEND |
startTime | string | ISO 8601 new flight start |
endTime | string | ISO 8601 new flight end |
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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
kindrequired | "campaign" | "ad_group" | "ad" | campaign, ad_group or ad: which type the id refers to |
idrequired | string | id of the campaign, ad group or ad |
statusrequired | "ACTIVE" | "PAUSED" | "ARCHIVED" | "DELETED" | ACTIVE = start spending; PAUSED = stop; ARCHIVED = retire; DELETED = permanent, and blocked for 3h after any change |
confirm | boolean | REQUIRED true for ACTIVE (real spend), ARCHIVED and DELETED |
confirmName | string | when archiving or removing: its EXACT name, as the unconfirmed call names it. Required when it has children or is live |
confirmChildren | number | when archiving or removing: the exact number of children the unconfirmed call reported. Required when it has any |
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").
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
kindrequired | "campaign" | "ad_group" | "ad" | campaign, ad_group or ad: which type the id refers to |
idrequired | string | id of the campaign, ad group or ad to remove |
status | "DELETED" | "ARCHIVED" | DELETED = permanent (default); ARCHIVED = out of service but reversible to PAUSED |
confirm | boolean | REQUIRED true |
confirmName | string | the object’s EXACT name, required when it has children or is live |
confirmChildren | number | the 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_…) — omit when only one is shared |
list_reddit_ads_catalogs#
List Reddit product catalogs, feeds and product sets read-only
Read the Reddit PRODUCT CATALOGS behind a shared ad account — what a catalog SALES campaign (create_reddit_ads_campaign objective SALES + useCatalog:true) advertises. With no catalogId: every catalog on the ad account’s business, with its product counts (total / approved / rejected / invalid), currency, language and attached pixels. With catalogId: that catalog’s linked FEEDS (url, REPLACE/UPDATE mode, schedule, next import), its PRODUCT SETS (id, name, filter, how many products match) and its recent IMPORTS — the product set id is the productSetId create_reddit_ads_ad_group needs. Read-only, free.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_…) — omit when only one is shared |
catalogId | string | a catalog id from the list — returns its feeds, product sets and recent imports |
create_reddit_ads_catalog#
Create a Reddit product catalog
Create an empty Reddit PRODUCT CATALOG on the business behind the shared ad account — the product inventory a catalog SALES campaign advertises (dynamic product ads, collection ads). Spends nothing. Name it, give the CURRENCY your prices are in (USD, GBP, CAD, EUR, AUD, JPY, CHF, NZD, SEK, NOK or BRL — Reddit’s list) and optionally the language and the Reddit pixel(s) to match products against (list_reddit_ads_pixels; catalog ads optimize on pixel events such as PURCHASE). The currency and language cannot be changed later. Then fill it — upsert_reddit_ads_products for a handful, or create_reddit_ads_product_feed to have Reddit fetch your product file on a schedule — and define which products to advertise with create_reddit_ads_product_set. Reddit requires its Catalog manager role for catalog writes, which Business admin does not include (a login that can run campaigns can still be refused “no permissions to Create Catalog action”) — that refusal is about the role, not the connection, and the reply says how to fix it. Read back from Reddit.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
namerequired | string | catalog name |
defaultCurrencyrequired | "USD" | "GBP" | "CAD" | "EUR" | "AUD" | "JPY" | "CHF" | "NZD" | "SEK" | "NOK" | "BRL" | the currency your product prices are in — fixed once created |
defaultLanguage | "en" | "de" | "es" | "fr" | "it" | "pt" | default en — fixed once created |
pixelIds | string[] | Reddit pixel ids from list_reddit_ads_pixels to match products against; each must be on the shared ad account |
create_reddit_ads_product_feed#
Link a product feed to a Reddit catalog destructive
Have Reddit FETCH your product file (CSV/TSV/XML at a public https:// or FTP/SFTP address — a Shopify, Google Merchant or WooCommerce product feed works) into a catalog, once or on a schedule. mode REPLACE (default) treats the file as the whole catalog, so products missing from it are removed; UPDATE only adds and changes. A catalog takes at most TWO feeds and they must be of different types. Feed credentials (username/password) go to Reddit and are never echoed back. Spends nothing. Check how the import went with list_reddit_ads_catalogs(catalogId) then get_reddit_ads_catalog_import. Read back from Reddit.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
catalogIdrequired | string | catalog to import the feed into |
urlrequired | string | the product file Reddit fetches |
mode | "REPLACE" | "UPDATE" | default REPLACE — the file is the whole catalog |
name | string | feed name, for your reference |
username | string | only if the url needs a login |
password | string | password for the feed URL; sent to Reddit and never echoed back |
schedule | object | how often Reddit re-fetches the file; omit for a one-off |
create_reddit_ads_product_set#
Create a Reddit product set (which catalog products to advertise)
Define a PRODUCT SET in a Reddit catalog — the rule that picks which products a catalog ad group advertises; its id is the productSetId create_reddit_ads_ad_group takes. The filter is Reddit’s JSON rule: one rule {"attribute":{"operator":value}} or several combined with {"and":[…]} / {"or":[…]}. Attributes: id, title, description, price, currency, sale_price, availability, brand, color, gender, product type, item_group_id, condition, age_group, size, pattern, material, custom_label_0…4. Operators: contains, not_contains, i_contains, i_not_contains, starts_with, i_starts_with, is_any, is_not_any, i_is_any, i_is_not_any (the *_any ones take a list), eq, neq, lt, lte, gt, gte — case-sensitive unless prefixed i_. Examples: everything in stock {"availability":{"eq":"IN_STOCK"}}; a brand under 50 {"and":[{"brand":{"eq":"Acme"}},{"price":{"lt":"50"}}]}. Validated before Reddit is asked; spends nothing; read back with how many products match.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
catalogIdrequired | string | catalog the product set belongs to |
namerequired | string | product set name |
filterrequired | object | the rule as an object (or a JSON string), e.g. {"availability":{"eq":"IN_STOCK"}} |
update_reddit_ads_catalog_object#
Edit a Reddit catalog, feed or product set destructive
Change a Reddit catalog (kind "catalog": its name or attached pixelIds — currency and language are fixed), a product FEED (kind "feed": url, mode, name, credentials or its schedule, including pausing it with schedule.isPaused) or a PRODUCT SET (kind "product_set": name or filter, same rule format as create_reddit_ads_product_set). A changed product-set filter changes what every catalog ad group using that set advertises, immediately — say so before doing it. Read back from Reddit.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
kindrequired | "catalog" | "feed" | "product_set" | catalog, feed or product_set: which type the id refers to |
idrequired | string | the catalog, feed or product set id |
name | string | new name |
pixelIds | string[] | catalog only — replaces the attached pixels |
url | string | feed only |
mode | "REPLACE" | "UPDATE" | feed only |
username | string | feed only |
password | string | feed only |
schedule | object | feed only — same shape as create_reddit_ads_product_feed |
filter | object | product_set only — the new rule |
delete_reddit_ads_catalog_object#
Delete a Reddit catalog, feed or product set destructive
PERMANENTLY delete a Reddit catalog (with all its products, feeds and product sets), a product feed, or a product set. Unlike campaigns these really are deleted, and it cannot be undone. Call WITHOUT confirm first: nothing is deleted, and the answer lists what would go with it — including any ad group on the shared ad account still advertising an affected product set. Show that to the user, get an explicit yes, then call again with confirm:true. The result is checked by reading the object back (Reddit then answers 404).
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
kindrequired | "catalog" | "feed" | "product_set" | catalog, feed or product_set: which type the id refers to |
idrequired | string | id of the catalog, feed or product set to delete |
confirm | boolean | true only after the user approved the listed impact |
list_reddit_ads_products#
List products in a Reddit catalog or product set read-only
Read the products in a Reddit catalog (catalogId) or in one product set (productSetId), each with its title, price, availability, link, image and Reddit’s own review state and issues (the "reddit" block). Filter by approvalDecision (PENDING / APPROVED / REJECTED — REJECTED is how to find out why products are not serving), a search term, or specific ids. Paginated: pass the returned nextPageToken as pageToken. Read-only, free.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
catalogId | string | one of catalogId or productSetId |
productSetId | string | list the products in this product set instead of the whole catalog |
approvalDecision | "PENDING" | "APPROVED" | "REJECTED" | PENDING, APPROVED or REJECTED: filter by Reddit review state |
search | string | text to match against product fields |
ids | string[] | specific product ids to read |
limit | number | page size, default 50, max 1000 |
pageToken | string | nextPageToken from the previous page |
upsert_reddit_ads_products#
Add or update products in a Reddit catalog destructive
Add products to a Reddit catalog, or update them — re-sending an id you already sent UPDATES that product, it never duplicates it. Up to 1,000 per call. Fields are Reddit’s own snake_case product attributes: id (your SKU, required), title, description, price as "<amount> <ISO currency>" e.g. "19.99 USD", link, image_link, availability (IN_STOCK, OUT_OF_STOCK, PREORDER, BACKORDER), brand, sale_price, additional_image_links, product type, Google product category, gtin, mpn, condition, color, size, gender, age_group, item_group_id, custom_label_0…4 and the rest of Reddit’s list. Reddit answers per product — CREATED, UPDATED, UNCHANGED or INVALID with the exact field and issue code — and that answer is what is reported. New products go through Reddit’s review before they can serve. Spends nothing.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
catalogIdrequired | string | catalog to add or update products in |
productsrequired | object[] | each {id, title, price:"19.99 USD", link, image_link, availability, …} |
delete_reddit_ads_products#
Remove products from a Reddit catalog destructive
PERMANENTLY remove products (by id) from a Reddit catalog — Reddit: "This action can't be undone". Up to 1,000 ids per call. Call WITHOUT confirm first: nothing is deleted and the answer says how many would go; get the user's yes, then call again with confirm:true. Reddit answers DELETED or NOT_FOUND per id, and that is what is reported. A catalog fed by a REPLACE feed re-imports whatever the file still contains, so remove the product from the file too.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
catalogIdrequired | string | catalog the products belong to |
productIdsrequired | string[] | product ids (your SKUs) to remove, up to 1,000 per call |
confirm | boolean | true only after the user approved the listed impact |
get_reddit_ads_catalog_import#
Check a Reddit catalog feed import read-only
See how Reddit’s fetches of a catalog’s product feed went. Without importId: the catalog’s recent imports with their status (PENDING, PROCESSING, COMPLETED, FAILED, SKIPPED) and added / modified / deleted / invalid counts. With importId: that import’s issue rows (which products failed and why) plus a time-limited link to Reddit’s full import report. An import whose file did not change is SKIPPED and has no report. Read-only, free.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
catalogIdrequired | string | catalog whose feed imports to read |
importId | string | one import's id, for its issue rows and report link; omit for recent imports |
reddit_ads_data_deletion#
Request (or check) deletion of a person’s Reddit ads data destructive
Honour a person’s data-deletion request on Reddit Ads: asks Reddit to PERMANENTLY delete the ads data it holds for their identifiers — EMAIL, PHONE_NUMBER, IDFA, AAID or EXTERNAL_ID, 1–200 per job — across the shared ad account, or only under one pixel (pixelId; Reddit requires business-admin access for that). Emails and phone numbers are canonicalised and SHA-256 hashed here exactly as the audience upload and Conversions API hash them, so the deletion matches what was sent; a 64-hex hash you made yourself passes through. It removes ads data (custom-audience matches, conversion records) and never anyone’s Reddit account. Call WITHOUT confirm first — nothing is sent and the answer states the scope — then again with confirm:true. Reddit processes jobs DAILY and finishes within 30 days: pass jobId alone to check one (QUEUED, COMPLETED or FAILED), at most once a day.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
identifiers | object[] | who to delete; omit when checking a jobId |
pixelId | string | limit the deletion to one pixel’s data (from list_reddit_ads_pixels) |
confirm | boolean | true only after the user approved — deletion is irreversible |
jobId | string | check an existing job instead of creating one |
list_reddit_ads_funding_instruments#
List Reddit funding instruments (can this account pay?) read-only
List the funding instruments (cards, credit lines, insertion orders) on a Reddit ad account and whether each can pay for delivery — Reddit’s own is_servable flag plus its reasons when not (outside its dates, credit line used up, card not approved, not active, test). This is the answer to "why isn’t my Reddit campaign spending?" when everything is ACTIVE, and the source of the fundingInstrumentId create_reddit_ads_campaign accepts. mode: ALL (default), ACTIVE, INACTIVE, UPCOMING or SELECTABLE. Read-only, free.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
mode | "ALL" | "ACTIVE" | "INACTIVE" | "UPCOMING" | "SELECTABLE" | ALL (default), ACTIVE, INACTIVE, UPCOMING or SELECTABLE |
upload_reddit_ads_assets#
Upload creatives to the Reddit asset library
Store creatives in the Reddit Ads ASSET LIBRARY for a profile: images and videos (from a public https url — Reddit downloads it; a video may carry a posterUrl), HEADLINE text, and CTA labels from Reddit’s fixed list (Shop Now, Learn More, Sign Up, …). 1–20 per call. Processing is asynchronous: the reply is read back from the library after polling (up to ~20s) and says which assets are ACTIVE, which Reddit REJECTED and why, and which are still processing (finish those with get_reddit_ads_asset_uploads). A Reddit MAX campaign builds its ads from these assets: pass the ACTIVE ids to create_reddit_ads_max_campaign (at least 3 HEADLINE and 2 IMAGE or VIDEO). A standard post-based ad does not take an asset id. Free.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_…) — omit when only one is shared |
redditProfileIdrequired | string | the Reddit profile id (t2_…) from list_reddit_ads_profiles — assets hang off a profile, and it must be on the shared ad account |
assetsrequired | object[] | creatives to store, 1-20 per call; one item per IMAGE, VIDEO, HEADLINE or CTA |
get_reddit_ads_asset_uploads#
Poll Reddit asset uploads read-only
The processing state of one or more Reddit creative-asset uploads (ids from upload_reddit_ads_assets): PROCESSING, ACTIVE with the stored asset, or INVALID_MEDIA with Reddit’s reasons. Ids Reddit no longer knows are listed as missing, never silently dropped. Free.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_…) — omit when only one is shared |
redditProfileIdrequired | string | the Reddit profile id (t2_…) from list_reddit_ads_profiles — assets hang off a profile, and it must be on the shared ad account |
uploadIdsrequired | string[] | upload ids from upload_reddit_ads_assets, 1-50 per call |
list_reddit_ads_assets#
List the Reddit asset library read-only
The IMAGE and VIDEO assets stored on a Reddit profile, with status, dimensions and media urls; filter by type and name, paginate with pageToken. Reddit’s list is media-only: HEADLINE and CTA assets are stored but never listed here — read those by id with get_reddit_ads_asset (ids come from upload_reddit_ads_assets). OR pass postId (t3_…) to list the assets ONE POST uses, every type, each with its role (HEADLINE, IMAGE, VIDEO, CTA, THUMBNAIL, CAROUSEL_ITEM, HERO, SECONDARY_IMAGE) and image crop: those asset ids are what reddit_ads_report’s ASSET_ID breakdown reports on. Free.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_…) — omit when only one is shared |
redditProfileId | string | the Reddit profile id (t2_…) from list_reddit_ads_profiles — assets hang off a profile, and it must be on the shared ad account. Required unless postId is given |
postId | string | a post id (t3_…) from list_reddit_ads_posts: list the assets that post uses, with role and crop, instead of the profile library |
types | "IMAGE" | "VIDEO"[] | Reddit lists media only |
name | string | filter by asset name |
limit | number | 1–100, default 25 |
pageToken | string | nextPageToken from the previous page |
get_reddit_ads_asset#
Read one Reddit creative asset read-only
One stored Reddit creative asset by id — type, name, status, media dimensions and urls, or the headline/CTA text, plus any processing errors Reddit holds for it. Free.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
assetIdrequired | string | asset id from upload_reddit_ads_assets or list_reddit_ads_assets |
rename_reddit_ads_asset#
Rename a Reddit creative asset
Change a stored Reddit creative asset’s name — the ONLY field Reddit lets you edit on an asset (its PATCH takes {name} and nothing else). The reply is read back from the library. Free.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
assetIdrequired | string | id of the asset to rename |
namerequired | string | new asset name |
delete_reddit_ads_asset#
Delete a Reddit creative asset destructive
Permanently delete a creative asset from the Reddit asset library. Without confirm:true it deletes nothing and reports what the asset is; with it, the reply is the read-back (the asset reads as DELETED or gone), never Reddit’s 200. Free.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
assetIdrequired | string | id of the asset to delete |
confirm | boolean | true required; without it nothing is deleted |
send_reddit_ads_conversions#
Send conversions to Reddit (Conversions API) destructive
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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
pixelId | string | from list_reddit_ads_pixels — only needed when the account has more than one |
testId | string | a test id from Events Manager > Testing — events sent with it are visible there and NEVER counted in reporting |
eventsrequired | object[] | 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
name | string | filter by name |
limit | number | default 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
namerequired | string | what this list is, e.g. "Purchasers – last 180 days" |
update_reddit_ads_audience_users#
Add or remove people in a Reddit custom audience destructive
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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
customAudienceIdrequired | string | from create_reddit_ads_audience or list_reddit_ads_audiences |
action | "ADD" | "REMOVE" | default ADD |
usersrequired | object[] | 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
customAudienceIdrequired | string | id of the custom audience to delete, from list_reddit_ads_audiences |
confirm | boolean | REQUIRED 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
limit | number | default 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
namerequired | string | saved audience name |
targetingrequired | object | same 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 destructive
Rename a Reddit saved audience or replace its targeting. Targeting MERGES KEY BY KEY — measured live 2026-08-05, not a wholesale replace: a key you send replaces that whole list, a key you LEAVE OUT is kept as it was, and an explicit empty array (geolocations: []) is the only way to clear one — so name every key you want gone. 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
savedAudienceIdrequired | string | id of the saved audience to edit |
name | string | new saved audience name |
targeting | object | the targeting keys to change: each key sent replaces that whole list, keys left out are kept, and an empty list clears one |
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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
savedAudienceIdrequired | string | id of the saved audience to delete |
confirm | boolean | REQUIRED true |
confirmName | string | the audience’s EXACT name, required when live ad groups use it |
confirmChildren | number | the exact number of live ad groups reported by the unconfirmed call |
list_reddit_ads_lead_forms#
List Reddit lead generation forms read-only
Listing and reading forms KEEPS WORKING past the 2026-09-21 sunset — Reddit deliberately leaves GET/LIST up so advertisers can retain their records — so use this to EXPORT what exists before the deadline. Reddit exposes no API for the LEADS a form collected; those are downloaded from Ads Manager, and existing onsite-form ads are paused on 2026-09-30. 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
limit | number | default 50, max 100 |
create_reddit_ads_lead_form#
Create a Reddit lead generation form
REDDIT HAS RETIRED THIS, SO IT NOW REFUSES, AND IT CANNOT BE REPLACED LIKE FOR LIKE. Reddit removed onsite lead forms entirely: the API sunset on 2026-09-21 by Reddit’s changelog (their migration guide said 2026-09-30 for the same change; Hermoso stopped creating on the earlier date), Reddit’s API reference now marks the create call deprecated and it answers 410 Gone from 2026-09-30, and Reddit paused every ad still using an onsite form on 2026-09-30. So this tool refuses for free, before calling Reddit, and says what to do instead. DO NOT BUILD A NEW FUNNEL ON IT: Reddit’s replacement is an OFFLINE form on the advertiser’s own landing page, measured with the Reddit Pixel or the Conversions API — list_reddit_ads_pixels and send_reddit_ads_conversions, both of which work here today — inside an offsite lead generation campaign (create_reddit_ads_campaign with objective LEAD_GENERATION). Existing forms stay readable with list_reddit_ads_lead_forms. What it used to do: 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
namerequired | string | internal name — redditors do not see it |
promptrequired | string | the line shown above the form telling people what they are signing up for |
privacyLinkrequired | string | full https:// URL to your privacy policy — Reddit requires it |
questionsrequired | object[] | 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.
| Parameter | Type | Description |
|---|
adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
since | string | YYYY-MM-DD or full ISO timestamp |
until | string | YYYY-MM-DD or full ISO timestamp |
changeTypes | "AD_ACCOUNT" | "AD" | "AD_GROUP" | "AUDIENCE" | "BID" | "BUDGET" | "CAMPAIGN" | "STATUS" | "TARGETING"[] | limit to these change kinds, e.g. BUDGET, BID, STATUS, TARGETING |
entityType | "AD" | "AD_GROUP" | "CAMPAIGN" | required when you pass entityIds |
entityIds | string[] | restrict to these objects |
includeChildEntities | boolean | also return changes to what lives under those objects |
memberIds | string[] | restrict to changes made by these Reddit members |
limit | number | default 50, max 200 |
THE 2026-10-26 SOCIAL GAP SWEEP
update_tiktok_ads_ad#
Edit a TikTok ad destructive
EDIT an existing TikTok ad in place — its ad text, call-to-action, landing URL, URL parameters (UTMs), name, video and cover, carousel sound or Instant Form — instead of deleting and rebuilding it, which throws away its review history and delivery learning. Only the fields you pass change. TikTok blanks any field a full update leaves out, so Hermoso rebuilds the edit from TikTok’s own stored copy of the ad and lays your changes over it; a text/name/media-only edit uses TikTok’s incremental mode. An ad that is SWITCHED ON needs confirm:true, because the change reaches real viewers once TikTok accepts it (and TikTok reviews it again); a paused ad edits freely. A CTA change also clears a dynamic-CTA portfolio, which TikTok would otherwise show instead, and the reply says so. The result is READ BACK from TikTok field by field — relay `verified` and the note as given. Free.
| Parameter | Type | Description |
|---|
advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
adIdrequired | string | the ad to edit, from list_tiktok_ads_campaigns |
name | string | the ad name in TikTok Ads Manager |
adText | string | the copy people read, 1-100 characters, no emoji |
callToAction | string | TikTok’s CTA value, e.g. LEARN_MORE, SHOP_NOW, SIGN_UP, DOWNLOAD |
landingPageUrl | string | full http(s) URL |
utmParams | object | URL parameters TikTok appends to the landing URL, e.g. {"utm_source":"tiktok","utm_campaign":"__CAMPAIGN_NAME__"}; up to 14. Values may be TikTok macros: __CAMPAIGN_NAME__, __CAMPAIGN_ID__, __AID_NAME__, __AID__, __CID_NAME__, __CID__, __PLACEMENT__. Replaces the whole set. |
videoId | string | swap the video (from upload_tiktok_ads_creative); pass its coverImageId as imageIds too |
imageIds | string[] | image ids for the ad: the video's cover (coverImageId from upload_tiktok_ads_creative), or the slides for an image ad |
musicId | string | carousel sound, from list_tiktok_ads_music |
tiktokItemId | string | swap the Spark post |
pageId | string | Instant Form id for a lead ad (checked first) |
confirm | boolean | required when the ad is switched on |
preview_tiktok_ads_ad#
Preview a TikTok ad read-only
See a TikTok ad the way it renders on TikTok: pass adId for an EXISTING ad (the link works for 30 days), or a videoId with displayName and adText to preview a video BEFORE any ad is built (24 hours). TikTok also returns its own fix-it tips per placement (for example a resolution that is too low) — relay them. Read-only, free, spends nothing.
| Parameter | Type | Description |
|---|
advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
adId | string | preview an existing ad |
videoId | string | preview a video before building the ad (from upload_tiktok_ads_creative) |
displayName | string | the brand name shown on the ad, 1-40 characters (video preview) |
adText | string | 1-100 characters (video preview) |
callToAction | string | default LEARN_MORE (video preview) |
placement | "PLACEMENT_TIKTOK" | "PLACEMENT_PANGLE" | "ALL" | existing ad only; default TikTok |
language | "ENGLISH" | "CHINESE" | "JAPANESE" | ENGLISH, CHINESE or JAPANESE; language of the preview |
tiktok_ads_review_info#
Why a TikTok ad was rejected read-only
TikTok’s own review verdict for up to 100 ads: approved everywhere (ALL_AVAILABLE), approved for only some of its targeting (PART_AVAILABLE — with the placements, locations, ages or operating systems it is barred from), or rejected (UNAVAILABLE) — with TikTok’s REASONS and its SUGGESTED FIX per piece of content. Use it whenever an ad is not running or the user asks why it was rejected; fix it with update_tiktok_ads_ad. list_tiktok_ads_campaigns(includeReview:true) attaches the same verdict to every ad. Read-only, free.
| Parameter | Type | Description |
|---|
advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
adIdsrequired | string[] | 1-100 ad ids from list_tiktok_ads_campaigns |
tiktok_ads_creative_fatigue#
TikTok creative fatigue read-only
Is a creative WEARING OUT? TikTok’s own creative-fatigue verdict per creative per day over the last 14 days, with spend and cost per conversion — the signal to make a fresh variation and swap it in. TikTok computes it for Upgraded Smart+ ad groups: pass the adgroupId and Hermoso reads every creative (ad material) in it, or name adMaterialIds. Read-only, free.
| Parameter | Type | Description |
|---|
advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
adgroupIdrequired | string | a Smart+ ad group (list_tiktok_smart_campaigns) |
adMaterialIds | string[] | optional; default every creative in the ad group |
startDate | string | YYYY-MM-DD, within the last 14 days (default 7 days ago) |
endDate | string | YYYY-MM-DD (default today) |
tiktok_ads_video_retention#
TikTok per-second video retention read-only
Per-SECOND performance for TikTok ad videos — which second viewers swipe away on, the share still watching at 2s and 3s, and the biggest single drop: the hook read for the next variation. reportType AD (default) reads ads by adIds, adgroupIds or campaignIds with per-second views, clicks, drop-offs, likes and shares; reportType VIDEO reads ONE video’s lifetime retain/drop-off curve by videoId. Covers the first 60 seconds; data lags about an hour; an ad that never served has none. Read-only, free.
| Parameter | Type | Description |
|---|
advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
reportType | "AD" | "VIDEO" | AD (default) reads ads by adIds, adgroupIds or campaignIds; VIDEO reads one video by videoId |
adIds | string[] | ad ids to read (AD report) |
adgroupIds | string[] | ad group ids to read (AD report) |
campaignIds | string[] | campaign ids to read (AD report) |
videoId | string | VIDEO report: one video id from upload_tiktok_ads_creative |
materialId | string | creative material id to read one specific material |
list_tiktok_ads_music#
Find TikTok ad music read-only
TikTok’s ad-cleared music for the ad account — the sound a CAROUSEL ad needs (musicId on create_tiktok_ads_ad with adFormat CAROUSEL_ADS). Search by keyword, get recommendations for 2-35 already-uploaded carousel images (imageUrls), look up musicIds, or read the liked / history / your-uploads lists (searchType); scene CREATIVE_ASSET lists the creative-tools library instead. Preview URLs last 12 hours. Read-only, free.
| Parameter | Type | Description |
|---|
advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
scene | "CAROUSEL_ADS" | "CREATIVE_ASSET" | CAROUSEL_ADS when searching; CREATIVE_ASSET for the creative-tools library |
keyword | string | search text for the music library |
imageUrls | string[] | 2-35 URLs of images already uploaded to the ad account |
musicIds | string[] | music ids to look up |
searchType | "SEARCH_BY_KEYWORD" | "SEARCH_BY_RECOMMEND" | "SEARCH_BY_LIKED" | "SEARCH_BY_HISTORY" | "SEARCH_BY_MUSIC_ID" | "SEARCH_BY_SOURCE" | SEARCH_BY_KEYWORD, SEARCH_BY_RECOMMEND, SEARCH_BY_LIKED, SEARCH_BY_HISTORY, SEARCH_BY_MUSIC_ID or SEARCH_BY_SOURCE |
pageSize | number | rows to fetch from TikTok; the reply shows the first 30 |
tiktok_ads_trending_hashtags#
TikTok trending hashtags read-only
TikTok’s OWN first-party trending hashtags, through the TikTok Ads connection: kind "hashtags" lists the top hashtags by country, industry category (e.g. FOOD_AND_BEVERAGE, BEAUTY_AND_PERSONAL_CARE, TECHNOLOGY, or ALL) and period (1DAY/7DAY/30DAY/120DAY) with rank, rank change, views, posts and top countries; "hashtag_detail" gives one hashtag’s 30-day trend and audience ages; "hashtag_videos" gives the top videos using up to 10 hashtags. A detail or video read must use the SAME countryCode and dateRange the hashtag was listed with. Read-only, free.
| Parameter | Type | Description |
|---|
advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
kind | "hashtags" | "hashtag_detail" | "hashtag_videos" | default hashtags |
countryCode | string | 2-letter, default US |
category | string | industry category, default ALL |
dateRange | "1DAY" | "7DAY" | "30DAY" | "120DAY" | trend window: 1DAY, 7DAY, 30DAY or 120DAY |
hashtagIds | string[] | for hashtag_detail (first id) and hashtag_videos (up to 10) |
tiktok_trends#
TikTok trending sounds and searches read-only
TikTok’s own trend data through the connected TikTok ACCOUNT: kind "sounds" lists trending Commercial Music Library tracks — pre-cleared for organic posts AND ads — by genre (POP, HIP_HOP/RAP, ELECTRONIC … or ALL), country and period (1DAY/7DAY/30DAY/90DAY); "sound_videos" the top videos using one sound; "search_terms" the top 20 trending TikTok search terms; "search_suggestions" TikTok search keywords for a product, service or brand (query). Read-only, free.
| Parameter | Type | Description |
|---|
kindrequired | "sounds" | "sound_videos" | "search_terms" | "search_suggestions" | sounds (trending tracks), sound_videos (videos using soundId), search_terms, or search_suggestions (needs query) |
countryCode | string | 2-letter country code the trend data is for, e.g. US |
genre | string | music genre for kind sounds, e.g. POP or HIP_HOP/RAP, or ALL |
dateRange | "1DAY" | "7DAY" | "30DAY" | "90DAY" | trend window: 1DAY, 7DAY, 30DAY or 90DAY |
soundId | string | for sound_videos — soundId from kind sounds |
query | string | for search_suggestions |
personalized | boolean | search terms personalized for this account |
tiktok_video_insights#
TikTok post insights read-only
Per-POST analytics for the connected TikTok account: views, reach, likes, comments, shares, saves, average watch time, full-watch rate, the RETENTION CURVE (share still watching at each second — the hook read), traffic sources (For You, Follow, Search, Sound, profile…), new followers, profile views, website clicks, lead submissions and the audience by country, gender and new-vs-returning. Newest posts first, up to 20 a page (cursor for more), or name videoIds. Data lags 24-48 hours. Needs the TikTok ACCOUNT authorization; an older one says which permission to re-authorize for. Read-only, free.
| Parameter | Type | Description |
|---|
brand | string | WHICH PROFILE the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a profile this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no profile, or two, is REFUSED and nothing is done. |
videoIds | string[] | post ids to read; omit for the newest posts |
maxCount | number | 1-20, default 20 |
cursor | string | pagination cursor from the previous reply, to fetch the next page |
insights | boolean | false = counters only (no retention/traffic/audience) |
tiktok_spark_recommendations#
Which TikTok posts to boost read-only
Which organic TikTok posts are worth running as Spark Ads: TikTok rates the connected account’s posts HIGH / MEDIUM / LOW, optionally for an objective (REACH, TRAFFIC, VIDEO_VIEWS, ENGAGEMENT, APP_PROMOTION, LEAD_GENERATION, WEB_CONVERSIONS, PRODUCT_SALES) and period (PAST_ONE_MONTH / THREE / SIX). Pass ttoAccountId to rate TikTok One creator-marketplace posts instead. Run a HIGH one with create_tiktok_ads_ad(tiktokItemId). Read-only, free.
| Parameter | Type | Description |
|---|
objective | string | campaign objective to rate posts for, e.g. REACH, TRAFFIC or VIDEO_VIEWS; omit for all |
time | "PAST_ONE_MONTH" | "PAST_THREE_MONTHS" | "PAST_SIX_MONTHS" | PAST_ONE_MONTH, PAST_THREE_MONTHS or PAST_SIX_MONTHS |
videoIds | string[] | rate only these (up to 20) |
excludeVideoIds | string[] | post ids to leave out of the ratings |
ttoAccountId | string | rate TikTok One creator posts instead of your own |
tiktok_hashtag_suggestions#
TikTok hashtag ideas read-only
Hashtag ideas for a TikTok caption: 10 suggestions for a keyword, exact matches first and then by how many posts use them, with each tag’s post count. Read-only, free.
| Parameter | Type | Description |
|---|
keywordrequired | string | ONE word, no spaces (TikTok refuses a keyword with a space) |
language | string | TikTok language code, default en (e.g. es, fr, de-DE, pt-BR) |
TIKTOK ONE
list_tiktok_tto_accounts#
List the TikTok One (Creator Marketplace) accounts on this connection read-only
The TikTok One accounts this TikTok connection can act on. TikTok One is TikTok’s influencer marketplace: find creators, invite them to a campaign, get their videos tagged to it, read organic-versus-paid performance on those videos, and ask them for Spark Ads authorization so the brand can put money behind their post. THIS IS THE ONE PIECE OF STRUCTURE THAT MAKES THE CREATOR TOOLS DIFFERENT: a TikTok One account id is a THIRD id space beside an advertiser id and a Business Center id, and every creator-marketplace tool needs one from here. With exactly one reachable the other tools resolve it themselves; with several they refuse and name them. It rides the SAME connection and the SAME OAuth flow as TikTok Ads and there is nothing extra to apply for; but a connection authorized before 2026-08-19 does not carry the TikTok One permission and its owner has to reconnect. Read-only, free.
No parameters.
list_tiktok_creator_labels#
List TikTok creator category labels read-only
TikTok’s creator category labels, in two sets that are NOT interchangeable. labelType "SEARCH" gives the tags discover_tiktok_creators filters on: contentLabelIds is what a creator POSTS about, industryLabelIds the commercial categories they have actually worked in. labelType "RANKING" gives the labels tiktok_creator_leaderboard needs, and there the pairing is strict: BRANDED_CONTENT ranks by an INDUSTRY label, ORGANIC_CONTENT by a CONTENT label. Both arrive as bare numeric ids that look identical, and TikTok reports a mismatch as an invalid field rather than as the wrong kind of label, so read this first rather than guessing. Read-only, free.
| Parameter | Type | Description |
|---|
ttoAccountId | string | from list_tiktok_tto_accounts: omit only when exactly one is reachable |
labelType | "SEARCH" | "RANKING" | default SEARCH |
discover_tiktok_creators#
Search TikTok One for creators to work with read-only
Search TikTok One for creators, filtered by audience size, engagement rate, median and average views, starting price, language, content and industry category, and by their FOLLOWERS’ country, gender split and age band. FOUR THINGS THAT ARE EASY TO GET WRONG AND ARE ALL REFUSED FOR FREE BEFORE THE CALL: countryCodes is required; every country in ONE search must come from the same regional category, which TikTok defines as the US, Europe (DE/ES/FR/GB/IT) and everywhere else, so a US+UK search has to be two searches; engagement rates are 0-1 rather than percentages, so 5% is 0.05; and stateProvinces works only when the country is exactly US. THIS IS NOT A TIKTOK SEARCH: it sees only creators who have JOINED the Creator Marketplace, so a narrow filter set empties fast and the fix is to widen the follower or engagement range rather than to conclude the category is empty. Profile image URLs carry their own expiry, so show them and do not store them. Read-only, free.
| Parameter | Type | Description |
|---|
ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
countryCodesrequired | string[] | REQUIRED, and all from ONE regional category: US | Europe (DE ES FR GB IT) | other (AE AR AU BR CA CO EG ID IL JP KR MX MY PH SA SG TH TR TW VN) |
stateProvinces | string[] | US only, and only when countryCodes is exactly ["US"] |
keywordSearch | string | fuzzy match, ≤100 characters |
contentLabelIds | string[] | what the creator posts about: from list_tiktok_creator_labels(labelType:"SEARCH") |
industryLabelIds | string[] | commercial categories they have worked in: same source |
languages | string[] | language codes the creator posts in; optional |
minFollowers | number | minimum follower count |
maxFollowers | number | maximum follower count |
minEngagementRate | number | 0-1, NOT a percentage |
maxEngagementRate | number | maximum engagement rate as a fraction, 0-1 (0.05 = 5%) |
minMedianViews | number | minimum median views per post |
maxMedianViews | number | maximum median views per post |
minAvgViews | number | minimum average views per post |
maxAvgViews | number | maximum average views per post |
minCreatorPrice | number | the creator’s starting price, USD |
maxCreatorPrice | number | maximum creator starting price in USD |
followerCountryCodes | string[] | where their AUDIENCE is, which is often not where they are |
followerGenderRatio | "FEMALE_50" | "FEMALE_60" | "FEMALE_70" | "MALE_50" | "MALE_60" | "MALE_70" | audience gender split bucket, e.g. FEMALE_60 or MALE_50 |
followerAge | "18-24" | "25-34" | "35-44" | "45-54" | "55+" | audience age band: 18-24, 25-34, 35-44, 45-54 or 55+ |
sortField | "RELEVANCE" | "FOLLOWERS" | "MEDIAN_VIEWS" | "ENGAGEMENT_RATE" | RELEVANCE, FOLLOWERS, MEDIAN_VIEWS or ENGAGEMENT_RATE |
sortOrder | "ASC" | "DESC" | ASC or DESC |
page | number | page number, from 1 |
pageSize | number | 1-200, default 24 |
tiktok_creator_leaderboard#
Read TikTok’s Creator Leaderboard read-only
The creators ranking highest on TikTok’s own Creator Leaderboard for one category over one week or month; up to 100, best first, with how far each moved since the previous period. US ONLY: TikTok publishes ranking data for no other country. labelId must MATCH rankingType: BRANDED_CONTENT takes an INDUSTRY label and ORGANIC_CONTENT takes a CONTENT label, both from list_tiktok_creator_labels(labelType:"RANKING"): and the mismatch is refused here rather than at TikTok, which reports it only as an invalid field. A leaderboard is a SNAPSHOT taken when the rank was generated, so TikTok warns a creator may still appear under a label they have since lost. Read-only, free.
| Parameter | Type | Description |
|---|
ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
rankingTyperequired | "BRANDED_CONTENT" | "ORGANIC_CONTENT" | BRANDED_CONTENT needs an industry label, ORGANIC_CONTENT a content label |
timePeriodrequired | "WEEK" | "MONTH" | ranking period: WEEK or MONTH |
lookbackrequired | "ONE" | "TWO" | "THREE" | ONE is the most recent completed period |
labelIdrequired | string | from list_tiktok_creator_labels(labelType:"RANKING"), matching rankingType |
countryCode | string | US: the only country TikTok ranks |
page | number | page number, from 1 |
pageSize | number | 1-100, default 20 |
check_tiktok_creator_status#
Check whether TikTok handles have joined TikTok One read-only
Whether particular TikTok handles have joined TikTok One, up to 20 at a time, with each answered IN (can be invited), NOT_IN (a real account that has not joined) or INVALID (not a handle TikTok knows). WORTH CALLING BEFORE INVITING ANYONE: a creator who has not joined cannot be invited, and TikTok reports that as a 200 with the handle in a failed list rather than as an error; so an unchecked invite looks like it worked and quietly reached nobody. Read-only, free.
| Parameter | Type | Description |
|---|
ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
handlesrequired | string[] | TikTok usernames WITHOUT the @, max 20 |
list_tiktok_tto_brand_profiles#
List TikTok One brand profiles read-only
The Brand Profiles on a TikTok One account. A Brand Profile is what a creator sees when they open an invite link; name, industry, logo, website and optionally the brand’s own TikTok account; and a brand-level campaign cannot exist without one. Read-only, free.
| Parameter | Type | Description |
|---|
ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
brandProfileIds | string[] | filter, max 20 |
page | number | page number, from 1 |
pageSize | number | rows per page |
create_tiktok_tto_brand_profile#
Create a TikTok One brand profile
Create a Brand Profile on a TikTok One account; the identity creators see when they open an invite link. THE NAME, INDUSTRY AND LOGO ARE PERMANENT: TikTok marks all three "cannot be updated" and publishes no brand-profile update endpoint anywhere in its API, so a profile is authored exactly once and a typo has to be lived with. Confirm all three with the user before calling. The logo must be a 1:1 image under 5MB at a publicly reachable URL, and the website must start with https. Adding tiktokAccountUrl lets creators see the brand’s own TikTok account on the profile, which TikTok says improves how many of them take the invitation seriously. Free.
| Parameter | Type | Description |
|---|
ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
brandNamerequired | string | PERMANENT: ≤60 characters |
brandIndustryIdrequired | string | PERMANENT: TikTok’s Brand Profile industry id |
brandWebsiterequired | string | must start with https:// |
logoUrlrequired | string | PERMANENT: JPG/PNG/GIF, 1:1, ≤5MB, public URL |
tiktokAccountUrl | string | https://www.tiktok.com/@handle |
list_tiktok_tto_campaigns#
List TikTok One creator campaigns read-only
The TikTok One creator campaigns on this account, each with its invite link, the creators invited, the videos linked so far, the ad accounts that automatically receive Spark Ads rights, and the COUNTRY CODES that tiktok_tto_campaign_report needs. TWO TIKTOK BEHAVIOURS WORTH KNOWING: it pages five at a time, which is its own maximum rather than ours, and it defaults to campaignType CAMPAIGN: so a brand-level campaign is invisible here unless you ask for BRAND_LINK explicitly. Read-only, free.
| Parameter | Type | Description |
|---|
ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
campaignIds | string[] | max 5 |
campaignType | "CAMPAIGN" | "BRAND_LINK" | TikTok defaults to CAMPAIGN: ask for BRAND_LINK to see brand-level ones |
page | number | page number, from 1 |
pageSize | number | 1-5 |
create_tiktok_tto_campaign#
Create a TikTok One creator campaign destructive
Create a TikTok One creator campaign and get the invite link creators tag their videos with. TWO SHAPES, AND THEY NEED DIFFERENT FIELDS: campaignType "CAMPAIGN" invites named creators by handle and produces a campaign-level link; campaignType "BRAND_LINK" requires a brandProfileId, takes no handles at all, and produces one open link anyone can tag a video with. THE FIELD WITH LASTING CONSEQUENCES IS advertiserIds: naming ad accounts means every video a creator links to this campaign AUTOMATICALLY grants those accounts Spark Ads rights for sparkAdsAuthorizationDays and is synced into their creative library; that is standing permission rather than a one-off, and TikTok requires the days whenever advertiserIds is given. sendNotification puts an invitation in a real creator’s TikTok inbox and is OFF unless you ask for it; without it the creators see the invitation only if you share the link yourself. Handles TikTok rejects come back in a failed list rather than as an error, so the reply reports which ones were ACTUALLY invited rather than which ones were asked for. Free.
| Parameter | Type | Description |
|---|
ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
campaignType | "CAMPAIGN" | "BRAND_LINK" | default CAMPAIGN |
campaignName | string | required for CAMPAIGN; shown to creators, ≤120 characters: TikTok suggests [product name - product description] |
campaignDescription | string | ≤1000 characters: positioning, audience, core features |
brandProfileId | string | required for BRAND_LINK; optional alternative to brandName on CAMPAIGN |
brandName | string | ≤60 characters, when there is no Brand Profile |
handleNames | string[] | required for CAMPAIGN: TikTok usernames without the @, max 100 |
advertiserIds | string[] | max 50: these ad accounts get AUTOMATIC Spark Ads rights on every linked video |
sparkAdsAuthorizationDays | number | 0-365; REQUIRED when advertiserIds is given |
anchorId | string | from manage_tiktok_tto_anchor |
sendNotification | boolean | default false: true puts an invitation in each creator’s TikTok inbox |
businessAccountHandle | string | allowlist-only at TikTok, and non-EU only: lets creators message the brand from the invite link |
update_tiktok_tto_campaign#
Add creators or ad accounts to a TikTok One campaign destructive
Add creators or ad accounts to an existing TikTok One campaign. IT ONLY EVER ADDS: TikTok appends what you pass to what the campaign already holds and publishes nothing that removes either, so there is no way to un-invite a creator or unlink an ad account through the API: the reply therefore reports what the campaign NOW HOLDS rather than what you sent. The name, description, brand and anchor cannot be changed after creation at all; TikTok’s own create endpoint accepts a campaign_id and then ignores every field except the account and the handles, which is why a rename is impossible rather than merely unsupported here. handleNames is not accepted on a BRAND_LINK campaign, because a brand-level link is open to whoever opens it. Free.
| Parameter | Type | Description |
|---|
ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
campaignIdrequired | string | TikTok One campaign id from list_tiktok_tto_campaigns |
campaignType | "CAMPAIGN" | "BRAND_LINK" | default CAMPAIGN |
handleNames | string[] | creators to ADD: max 100, CAMPAIGN only |
advertiserIds | string[] | ad accounts to ADD: max 50 |
sendNotification | boolean | default false |
link_tiktok_tto_video#
Ask a creator to link a video to a TikTok One campaign destructive
Ask a creator to link one of their public videos to a TikTok One campaign, or withdraw that request. THIS PUTS A NOTIFICATION IN A REAL PERSON’S TIKTOK INBOX, AND REPEATING IT IS A REMINDER RATHER THAN A RETRY: TikTok refuses a second LINK within 24 hours of the last, allows at most two reminders in total, and counts them; so never re-send after a transient failure without reading list_tiktok_tto_link_requests first. REVOKE has its own one-way rule: it works only while the creator has neither accepted nor rejected, TikTok allows it once per request, and there is no un-revoke. TikTok also refuses a video already linked to any other campaign, and refuses one whose creator was never invited. Free.
| Parameter | Type | Description |
|---|
ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
campaignIdrequired | string | TikTok One campaign the video is linked to, from list_tiktok_tto_campaigns |
videoIdrequired | string | the creator’s public video: from list_tiktok_tto_campaigns or tiktok_tto_campaign_report |
action | "LINK" | "REVOKE" | default LINK; a second LINK is a REMINDER |
list_tiktok_tto_link_requests#
List TikTok One video-linking requests read-only
Every video-linking request on this TikTok One account and where each one stands; waiting on the creator, approved, rejected or withdrawn; plus how many reminders are left on it. READ THIS BEFORE RE-SENDING ANYTHING: a second LINK is a reminder into a stranger’s inbox and TikTok caps them at two. Rows also carry the link request id, which the invited creator needs if they want to answer through their own TikTok One tools. Read-only, free.
| Parameter | Type | Description |
|---|
ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
campaignIds | string[] | max 50 |
handles | string[] | max 50 |
campaignType | "CAMPAIGN" | "BRAND_LINK" | omit to see every campaign |
page | number | page number, from 1 |
pageSize | number | 1-50, default 10 |
tiktok_tto_campaign_report#
Report on a TikTok One creator campaign read-only
How each creator video on a TikTok One campaign actually performed, with EVERY HEADLINE METRIC SPLIT ORGANIC VERSUS PAID: views, reach, engagement rate, likes, comments, shares, favourites, completion rate, average view time; plus audience breakdowns and any anchor clicks. That split is the question an influencer campaign exists to answer: did the creator’s own audience carry the video, or did the media spend. Give startDate and endDate TOGETHER to also get day-by-day figures; giving one without the other is refused, because TikTok requires each whenever the other is present. TikTok reports one country per call and will only accept one of the CAMPAIGN’s own countries, so it is read off the campaign rather than guessed. Two TikTok behaviours worth passing on: reporting is backfilled if a creator tags a video retroactively, and campaigns created through the API do NOT show their reporting on the TikTok One website, so this is the only place it appears. Read-only, free.
| Parameter | Type | Description |
|---|
ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
campaignIdrequired | string | TikTok One campaign id from list_tiktok_tto_campaigns |
countryCode | string | one of the campaign’s own country codes: resolved automatically when it has only one |
startDate | string | YYYY-MM-DD (UTC+0), paired with endDate: together they add per-day figures |
endDate | string | YYYY-MM-DD (UTC+0), paired with startDate |
campaignType | "CAMPAIGN" | "BRAND_LINK" | CAMPAIGN or BRAND_LINK; match the campaign's own type |
page | number | page number, from 1 |
pageSize | number | 1-100, default 25 |
request_tiktok_tto_spark_authorization#
Ask a TikTok One creator for Spark Ads authorization
Ask the creator of a campaign video for Spark Ads authorization, so the brand can run their organic post as an ad. THIS IS THE ONLY WAY TO OBTAIN A SPARK ADS CODE WITHOUT THE CREATOR PASTING ONE OUT OF THE TIKTOK APP BY HAND: once they accept, get_tiktok_tto_spark_authorization hands back the code that authorize_tiktok_ads_spark_post takes, which then makes the post usable by create_tiktok_ads_ad. THE NUMBER OF DAYS IS A REQUEST, NOT A SETTING: TikTok says the creator picks the actual window when approving, so read the granted dates back rather than assuming what was asked for. action "EXTEND" lengthens an authorization the creator has already approved. TikTok returns no confirmation body for this call, so the state in the reply was read back separately rather than echoed. Free.
| Parameter | Type | Description |
|---|
ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
videoIdrequired | string | a video the creator has already linked to the campaign |
authorizationDays | number | 1-365, TikTok defaults to 30: a REQUEST, not a guarantee |
action | "EXTEND" | EXTEND adds the days to an authorization already approved |
get_tiktok_tto_spark_authorization#
Check a TikTok One Spark Ads authorization read-only
Whether a creator has approved Spark Ads authorization for their video, and if so the authorization CODE, the window it covers, and whether that code is already bound to an ad account. The code is what authorize_tiktok_ads_spark_post takes. It appears only once the creator accepts: nothing on this side can produce one; and TikTok also reports how many further requests are left. Read-only, free.
| Parameter | Type | Description |
|---|
ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
videoIdrequired | string | the creator's video the authorization was requested for |
manage_tiktok_tto_anchor#
List, create or delete TikTok webpage anchors destructive
Webpage anchors: the link that appears above a creator’s video description and in its comment section, sending viewers to a product or service page. action "list", "create" or "delete". AN ANCHOR CREATED HERE CAN NEVER BE DELETED: TikTok removes only DRAFT anchors, an anchor created through the API is born IN_REVIEW, and no API path produces a draft; so treat creating one as permanent, exactly like a TikTok pixel. An anchor is visible ONLY to viewers in its own country, and TikTok measures its performance only when the creator’s country matches too; the video still reaches everyone, the anchor does not. The landing page must be a product or service DETAIL page or TikTok rejects it during review. The thumbnail is passed as a public URL: TikTok also accepts a file upload, and that transport is deliberately not built because the URL form is documented as equivalent. Free.
| Parameter | Type | Description |
|---|
ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
action | "list" | "create" | "delete" | default list |
anchorId | string | required for delete |
anchorIds | string[] | filters a list, max 100 |
categoryLabelId | string | required for create: TikTok’s product/service category, and it decides which countries the anchor may use |
countryCode | string | required for create: the anchor is invisible outside it |
landingPageUrl | string | required for create: a product or service DETAIL page |
anchorTitle | "WATCH_NOW" | "LISTEN_NOW" | "READ_MORE" | "SHOW_NOW" | "GET_OFFER" | "LEARN_MORE" | "CONTACT_US" | "JOIN_NOW" | "APPLY_NOW" | required for create: the call-to-action wording |
anchorName | string | required for create: an internal label TikTok never shows viewers, ≤32 characters |
thumbnailUrl | string | required for create: public URL, exactly 210x375, under 2MB |
page | number | page number, from 1 |
pageSize | number | 1-50 |